summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2014-02-01 18:21:04 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2014-02-01 18:21:04 -0500
commit5b0919f3f5c7678c587858a47e38acd4a5b82f25 (patch)
tree236ff9c4c4e8688aa9b1b5b5a9ea7f20ea3807fd
parent32a1db368599f6f3dbb3765ef5f11640d1725672 (diff)
downloadsqlalchemy-5b0919f3f5c7678c587858a47e38acd4a5b82f25.tar.gz
- Added a new feature which allows automated naming conventions to be
applied to :class:`.Constraint` and :class:`.Index` objects. Based on a recipe in the wiki, the new feature uses schema-events to set up names as various schema objects are associated with each other. The events then expose a configuration system through a new argument :paramref:`.MetaData.naming_convention`. This system allows production of both simple and custom naming schemes for constraints and indexes on a per-:class:`.MetaData` basis. [ticket:2923] commit 7e65e52c086652de3dd3303c723f98f09af54db8 Author: Mike Bayer <mike_mp@zzzcomputing.com> Date: Sat Feb 1 15:09:04 2014 -0500 - first pass at new naming approach
-rw-r--r--doc/build/changelog/changelog_09.rst17
-rw-r--r--doc/build/core/constraints.rst160
-rw-r--r--lib/sqlalchemy/sql/__init__.py3
-rw-r--r--lib/sqlalchemy/sql/naming.py110
-rw-r--r--lib/sqlalchemy/sql/schema.py84
-rw-r--r--lib/sqlalchemy/testing/fixtures.py18
-rw-r--r--test/engine/test_reflection.py7
-rw-r--r--test/sql/test_metadata.py82
8 files changed, 460 insertions, 21 deletions
diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst
index 8995f7d39..a218e0f54 100644
--- a/doc/build/changelog/changelog_09.rst
+++ b/doc/build/changelog/changelog_09.rst
@@ -15,6 +15,23 @@
:version: 0.9.2
.. change::
+ :tags: feature, sql
+ :tickets: 2923
+
+ Added a new feature which allows automated naming conventions to be
+ applied to :class:`.Constraint` and :class:`.Index` objects. Based
+ on a recipe in the wiki, the new feature uses schema-events to set up
+ names as various schema objects are associated with each other. The
+ events then expose a configuration system through a new argument
+ :paramref:`.MetaData.naming_convention`. This system allows production
+ of both simple and custom naming schemes for constraints and indexes
+ on a per-:class:`.MetaData` basis.
+
+ .. seealso::
+
+ :ref:`constraint_naming_conventions`
+
+ .. change::
:tags: bug, orm
:tickets: 2921
diff --git a/doc/build/core/constraints.rst b/doc/build/core/constraints.rst
index 13ead6fbf..d9a8fa98a 100644
--- a/doc/build/core/constraints.rst
+++ b/doc/build/core/constraints.rst
@@ -266,6 +266,166 @@ To apply table-level constraint objects such as :class:`.ForeignKeyConstraint`
to a table defined using Declarative, use the ``__table_args__`` attribute,
described at :ref:`declarative_table_args`.
+.. _constraint_naming_conventions:
+
+Configuring Constraint Naming Conventions
+-----------------------------------------
+
+Relational databases typically assign explicit names to all constraints and
+indexes. In the common case that a table is created using ``CREATE TABLE``
+where constraints such as CHECK, UNIQUE, and PRIMARY KEY constraints are
+produced inline with the table definition, the database usually has a system
+in place in which names are automatically assigned to these constraints, if
+a name is not otherwise specified. When an existing database table is altered
+in a database using a command such as ``ALTER TABLE``, this command typically
+needs to specify expicit names for new constraints as well as be able to
+specify the name of an existing constraint that is to be dropped or modified.
+
+Constraints can be named explicitly using the :paramref:`.Constraint.name` parameter,
+and for indexes the :paramref:`.Index.name` parameter. However, in the
+case of constraints this parameter is optional. There are also the use
+cases of using the :paramref:`.Column.unique` and :paramref:`.Column.index`
+parameters which create :class:`.UniqueConstraint` and :class:`.Index` objects
+without an explicit name being specified.
+
+The use case of alteration of existing tables and constraints can be handled
+by schema migration tools such as `Alembic <http://http://alembic.readthedocs.org/>`_.
+However, neither Alembic nor SQLAlchemy currently create names for constraint
+objects where the name is otherwise unspecified, leading to the case where
+being able to alter existing constraints means that one must reverse-engineer
+the naming system used by the relational database to auto-assign names,
+or that care must be taken to ensure that all constraints are named.
+
+In contrast to having to assign explicit names to all :class:`.Constraint`
+and :class:`.Index` objects, automated naming schemes can be constructed
+using events. This approach has the advantage that constraints will get
+a consistent naming scheme without the need for explicit name parameters
+throughout the code, and also that the convention takes place just as well
+for those constraints and indexes produced by the :paramref:`.Column.unique`
+and :paramref:`.Column.index` parameters. As of SQLAlchemy 0.9.2 this
+event-based approach is included, and can be configured using the argument
+:paramref:`.MetaData.naming_convention`.
+
+:paramref:`.MetaData.naming_convention` refers to a dictionary which accepts
+the :class:`.Index` class or individual :class:`.Constraint` classes as keys,
+and Python string templates as values. It also accepts a series of
+string-codes as alternative keys, ``"fk"``, ``"pk"``,
+``"ix"``, ``"ck"``, ``"uq"`` for foreign key, primary key, index,
+check, and unique constraint, respectively. The string templates in this
+dictionary are used whenever a constraint or index is associated with this
+:class:`.MetaData` object that does not have an existing name given (including
+one exception case where an existing name can be further embellished).
+
+An example naming convention that suits basic cases is as follows::
+
+ convention = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+ }
+
+ metadata = MetaData(naming_convention=convention)
+
+The above convention will establish names for all constraints within
+the target :class:`.MetaData` collection.
+For example, we can observe the name produced when we create an unnamed
+:class:`.UniqueConstraint`::
+
+ >>> user_table = Table('user', metadata,
+ ... Column('id', Integer, primary_key=True),
+ ... Column('name', String(30), nullable=False),
+ ... UniqueConstraint('name')
+ ... )
+ >>> list(user_table.constraints)[1].name
+ 'uq_user_name'
+
+This same feature takes effect even if we just use the :paramref:`.Column.unique`
+flag::
+
+ >>> user_table = Table('user', metadata,
+ ... Column('id', Integer, primary_key=True),
+ ... Column('name', String(30), nullable=False, unique=True)
+ ... )
+ >>> list(user_table.constraints)[1].name
+ 'uq_user_name'
+
+A key advantage to the naming convention approach is that the names are established
+at Python construction time, rather than at DDL emit time. The effect this has
+when using Alembic's ``--autogenerate`` feature is that the naming convention
+will be explicit when a new migration script is generated::
+
+ def upgrade():
+ op.create_unique_constraint("uq_user_name", "user", ["name"])
+
+The above ``"uq_user_name"`` string was copied from the :class:`.UniqueConstraint`
+object that ``--autogenerate`` located in our metadata.
+
+The default value for :paramref:`.MetaData.naming_convention` handles
+the long-standing SQLAlchemy behavior of assigning a name to a :class:`.Index`
+object that is created using the :paramref:`.Column.index` parameter::
+
+ >>> from sqlalchemy.sql.schema import DEFAULT_NAMING_CONVENTION
+ >>> DEFAULT_NAMING_CONVENTION
+ immutabledict({'ix': 'ix_%(column_0_label)s'})
+
+The tokens available include ``%(table_name)s``,
+``%(referred_table_name)s``, ``%(column_0_name)s``, ``%(column_0_label)s``,
+``%(column_0_key)s``, ``%(referred_column_0_name)s``, and ``%(constraint_name)s``;
+the documentation for :paramref:`.MetaData.naming_convention` describes each
+individually. New tokens can also be added, by specifying an additional
+token and a callable within the naming_convention dictionary. For example,
+if we wanted to name our foreign key constraints using a GUID scheme,
+we could do that as follows::
+
+ import uuid
+
+ def fk_guid(constraint, table):
+ str_tokens = [
+ table.name,
+ ] + [
+ element.parent.name for element in constraint.elements
+ ] + [
+ element.target_fullname for element in constraint.elements
+ ]
+ guid = uuid.uuid5(uuid.NAMESPACE_OID, "_".join(str_tokens).encode('ascii'))
+ return str(guid)
+
+ convention = {
+ "fk_guid": fk_guid,
+ "ix": 'ix_%(column_0_label)s',
+ "fk": "fk_%(fk_guid)s",
+ }
+
+Above, when we create a new :class:`.ForeignKeyConstraint`, we will get a
+name as follows::
+
+ >>> metadata = MetaData(naming_convention=convention)
+
+ >>> user_table = Table('user', metadata,
+ ... Column('id', Integer, primary_key=True),
+ ... Column('version', Integer, primary_key=True),
+ ... Column('data', String(30))
+ ... )
+ >>> address_table = Table('address', metadata,
+ ... Column('id', Integer, primary_key=True),
+ ... Column('user_id', Integer),
+ ... Column('user_version_id', Integer)
+ ... )
+ >>> fk = ForeignKeyConstraint(['user_id', 'user_version_id'],
+ ... ['user.id', 'user.version'])
+ >>> address_table.append_constraint(fk)
+ >>> fk.name
+ fk_0cd51ab5-8d70-56e8-a83c-86661737766d
+
+.. seealso::
+
+ :paramref:`.MetaData.naming_convention` - for additional usage details
+ as well as a listing of all avaiable naming components.
+
+.. versionadded:: 0.9.2 Added the :paramref:`.MetaData.naming_convention` argument.
+
Constraints API
---------------
.. autoclass:: Constraint
diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py
index 9ed6049af..95dae5aa3 100644
--- a/lib/sqlalchemy/sql/__init__.py
+++ b/lib/sqlalchemy/sql/__init__.py
@@ -66,7 +66,6 @@ from .expression import (
from .visitors import ClauseVisitor
-
def __go(lcls):
global __all__
from .. import util as _sa_util
@@ -85,5 +84,7 @@ def __go(lcls):
_sa_util.dependencies.resolve_all("sqlalchemy.sql")
+ from . import naming
+
__go(locals())
diff --git a/lib/sqlalchemy/sql/naming.py b/lib/sqlalchemy/sql/naming.py
new file mode 100644
index 000000000..b2cf1e9a5
--- /dev/null
+++ b/lib/sqlalchemy/sql/naming.py
@@ -0,0 +1,110 @@
+# sqlalchemy/naming.py
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"""Establish constraint and index naming conventions.
+
+
+"""
+
+from .schema import Constraint, ForeignKeyConstraint, PrimaryKeyConstraint, \
+ UniqueConstraint, CheckConstraint, Index, Table
+from .. import event, events
+from .. import exc
+from .elements import _truncated_label
+import re
+
+class ConventionDict(object):
+ def __init__(self, const, table, convention):
+ self.const = const
+ self._is_fk = isinstance(const, ForeignKeyConstraint)
+ self.table = table
+ self.convention = convention
+ self._const_name = const.name
+
+ def _key_table_name(self):
+ return self.table.name
+
+ def _column_X(self, idx):
+ if self._is_fk:
+ fk = self.const.elements[idx]
+ return fk.parent
+ else:
+ return list(self.const.columns)[idx]
+
+ def _key_constraint_name(self):
+ if not self._const_name:
+ raise exc.InvalidRequestError(
+ "Naming convention including "
+ "%(constraint_name)s token requires that "
+ "constraint is explicitly named."
+ )
+ # they asked for a name that's derived from the existing
+ # name, so set the existing name to None
+ self.const.name = None
+ return self._const_name
+
+ def _key_column_X_name(self, idx):
+ return self._column_X(idx).name
+
+ def _key_column_X_label(self, idx):
+ return self._column_X(idx)._label
+
+ def _key_referred_table_name(self):
+ fk = self.const.elements[0]
+ reftable, refcol = fk.target_fullname.split(".")
+ return reftable
+
+ def _key_referred_column_X_name(self, idx):
+ fk = self.const.elements[idx]
+ reftable, refcol = fk.target_fullname.split(".")
+ return refcol
+
+ def __getitem__(self, key):
+ if key in self.convention:
+ return self.convention[key](self.const, self.table)
+ elif hasattr(self, '_key_%s' % key):
+ return getattr(self, '_key_%s' % key)()
+ else:
+ col_template = re.match(r".*_?column_(\d+)_.+", key)
+ if col_template:
+ idx = col_template.group(1)
+ attr = "_key_" + key.replace(idx, "X")
+ idx = int(idx)
+ if hasattr(self, attr):
+ return getattr(self, attr)(idx)
+ raise KeyError(key)
+
+_prefix_dict = {
+ Index: "ix",
+ PrimaryKeyConstraint: "pk",
+ CheckConstraint: "ck",
+ UniqueConstraint: "uq",
+ ForeignKeyConstraint: "fk"
+}
+
+def _get_convention(dict_, key):
+
+ for super_ in key.__mro__:
+ if super_ in _prefix_dict and _prefix_dict[super_] in dict_:
+ return dict_[_prefix_dict[super_]]
+ elif super_ in dict_:
+ return dict_[super_]
+ else:
+ return None
+
+
+@event.listens_for(Constraint, "after_parent_attach")
+@event.listens_for(Index, "after_parent_attach")
+def _constraint_name(const, table):
+ if isinstance(table, Table):
+ metadata = table.metadata
+ convention = _get_convention(metadata.naming_convention, type(const))
+ if convention is not None:
+ newname = _truncated_label(
+ convention % ConventionDict(const, table, metadata.naming_convention)
+ )
+ if const.name is None:
+ const.name = newname
diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py
index ba38b5070..621ac20e8 100644
--- a/lib/sqlalchemy/sql/schema.py
+++ b/lib/sqlalchemy/sql/schema.py
@@ -1130,8 +1130,7 @@ class Column(SchemaItem, ColumnClause):
"The 'index' keyword argument on Column is boolean only. "
"To create indexes with a specific name, create an "
"explicit Index object external to the Table.")
- Index(_truncated_label('ix_%s' % self._label),
- self, unique=bool(self.unique))
+ Index(None, self, unique=bool(self.unique))
elif self.unique:
if isinstance(self.unique, util.string_types):
raise exc.ArgumentError(
@@ -2240,12 +2239,12 @@ class ColumnCollectionConstraint(ColumnCollectionMixin, Constraint):
arguments are propagated to the :class:`.Constraint` superclass.
"""
- ColumnCollectionMixin.__init__(self, *columns)
Constraint.__init__(self, **kw)
+ ColumnCollectionMixin.__init__(self, *columns)
def _set_parent(self, table):
- ColumnCollectionMixin._set_parent(self, table)
Constraint._set_parent(self, table)
+ ColumnCollectionMixin._set_parent(self, table)
def __contains__(self, x):
return x in self.columns
@@ -2839,6 +2838,11 @@ class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem):
))
+DEFAULT_NAMING_CONVENTION = util.immutabledict({
+ "ix": 'ix_%(column_0_label)s'
+})
+
+
class MetaData(SchemaItem):
"""A collection of :class:`.Table` objects and their associated schema
constructs.
@@ -2865,7 +2869,9 @@ class MetaData(SchemaItem):
__visit_name__ = 'metadata'
def __init__(self, bind=None, reflect=False, schema=None,
- quote_schema=None):
+ quote_schema=None,
+ naming_convention=DEFAULT_NAMING_CONVENTION
+ ):
"""Create a new MetaData object.
:param bind:
@@ -2890,12 +2896,76 @@ class MetaData(SchemaItem):
:class:`.Sequence`, and other objects which make usage of the
local ``schema`` name.
- .. versionadded:: 0.7.4
- ``schema`` and ``quote_schema`` parameters.
+ :param naming_convention: a dictionary referring to values which
+ will establish default naming conventions for :class:`.Constraint`
+ and :class:`.Index` objects, for those objects which are not given
+ a name explicitly.
+
+ The keys of this dictionary may be:
+
+ * a constraint or Index class, e.g. the :class:`.UniqueConstraint`,
+ :class:`.ForeignKeyConstraint` class, the :class:`.Index` class
+
+ * a string mnemonic for one of the known constraint classes;
+ ``"fk"``, ``"pk"``, ``"ix"``, ``"ck"``, ``"uq"`` for foreign key,
+ primary key, index, check, and unique constraint, respectively.
+
+ * the string name of a user-defined "token" that can be used
+ to define new naming tokens.
+
+ The values associated with each "constraint class" or "constraint
+ mnemonic" key are string naming templates, such as
+ ``"uq_%(table_name)s_%(column_0_name)s"``,
+ which decribe how the name should be composed. The values associated
+ with user-defined "token" keys should be callables of the form
+ ``fn(constraint, table)``, which accepts the constraint/index
+ object and :class:`.Table` as arguments, returning a string
+ result.
+
+ The built-in names are as follows, some of which may only be
+ available for certain types of constraint:
+
+ * ``%(table_name)s`` - the name of the :class:`.Table` object
+ associated with the constraint.
+
+ * ``%(referred_table_name)s`` - the name of the :class:`.Table`
+ object associated with the referencing target of a
+ :class:`.ForeignKeyConstraint`.
+
+ * ``%(column_0_name)s`` - the name of the :class:`.Column` at
+ index position "0" within the constraint.
+
+ * ``%(column_0_label)s`` - the label of the :class:`.Column` at
+ index position "0", e.g. :attr:`.Column.label`
+
+ * ``%(column_0_key)s`` - the key of the :class:`.Column` at
+ index position "0", e.g. :attr:`.Column.key`
+
+ * ``%(referred_column_0_name)s`` - the name of a :class:`.Column`
+ at index position "0" referenced by a :class:`.ForeignKeyConstraint`.
+
+ * ``%(constraint_name)s`` - a special key that refers to the existing
+ name given to the constraint. When this key is present, the
+ :class:`.Constraint` object's existing name will be replaced with
+ one that is composed from template string that uses this token.
+ When this token is present, it is required that the :class:`.Constraint`
+ is given an expicit name ahead of time.
+
+ * user-defined: any additional token may be implemented by passing
+ it along with a ``fn(constraint, table)`` callable to the
+ naming_convention dictionary.
+
+ .. versionadded:: 0.9.2
+
+ .. seealso::
+
+ :ref:`constraint_naming_conventions` - for detailed usage
+ examples.
"""
self.tables = util.immutabledict()
self.schema = quoted_name(schema, quote_schema)
+ self.naming_convention = naming_convention
self._schemas = set()
self._sequences = {}
self._fk_memos = collections.defaultdict(list)
diff --git a/lib/sqlalchemy/testing/fixtures.py b/lib/sqlalchemy/testing/fixtures.py
index 28541b14b..8717ce764 100644
--- a/lib/sqlalchemy/testing/fixtures.py
+++ b/lib/sqlalchemy/testing/fixtures.py
@@ -210,6 +210,24 @@ class TablesTest(TestBase):
[dict(zip(headers[table], column_values))
for column_values in rows[table]])
+from sqlalchemy import event
+class RemovesEvents(object):
+ @util.memoized_property
+ def _event_fns(self):
+ return set()
+
+ def event_listen(self, target, name, fn):
+ self._event_fns.add((target, name, fn))
+ event.listen(target, name, fn)
+
+ def teardown(self):
+ for key in self._event_fns:
+ event.remove(*key)
+ super_ = super(RemovesEvents, self)
+ if hasattr(super_, "teardown"):
+ super_.teardown()
+
+
class _ORMTest(object):
diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py
index 2f311f7e7..77e754475 100644
--- a/test/engine/test_reflection.py
+++ b/test/engine/test_reflection.py
@@ -1501,7 +1501,7 @@ class CaseSensitiveTest(fixtures.TablesTest):
-class ColumnEventsTest(fixtures.TestBase):
+class ColumnEventsTest(fixtures.RemovesEvents, fixtures.TestBase):
@classmethod
def setup_class(cls):
@@ -1526,9 +1526,6 @@ class ColumnEventsTest(fixtures.TestBase):
def teardown_class(cls):
cls.metadata.drop_all(testing.db)
- def teardown(self):
- events.SchemaEventTarget.dispatch._clear()
-
def _do_test(self, col, update, assert_, tablename="to_reflect"):
# load the actual Table class, not the test
# wrapper
@@ -1545,7 +1542,7 @@ class ColumnEventsTest(fixtures.TestBase):
assert_(t)
m = MetaData(testing.db)
- event.listen(Table, 'column_reflect', column_reflect)
+ self.event_listen(Table, 'column_reflect', column_reflect)
t2 = Table(tablename, m, autoload=True)
assert_(t2)
diff --git a/test/sql/test_metadata.py b/test/sql/test_metadata.py
index f933a2494..36c777c9a 100644
--- a/test/sql/test_metadata.py
+++ b/test/sql/test_metadata.py
@@ -2024,10 +2024,7 @@ class ColumnOptionsTest(fixtures.TestBase):
c.info['bar'] = 'zip'
assert c.info['bar'] == 'zip'
-class CatchAllEventsTest(fixtures.TestBase):
-
- def teardown(self):
- events.SchemaEventTarget.dispatch._clear()
+class CatchAllEventsTest(fixtures.RemovesEvents, fixtures.TestBase):
def test_all_events(self):
canary = []
@@ -2038,8 +2035,8 @@ class CatchAllEventsTest(fixtures.TestBase):
def after_attach(obj, parent):
canary.append("%s->%s" % (obj.__class__.__name__, parent))
- event.listen(schema.SchemaItem, "before_parent_attach", before_attach)
- event.listen(schema.SchemaItem, "after_parent_attach", after_attach)
+ self.event_listen(schema.SchemaItem, "before_parent_attach", before_attach)
+ self.event_listen(schema.SchemaItem, "after_parent_attach", after_attach)
m = MetaData()
Table('t1', m,
@@ -2074,8 +2071,8 @@ class CatchAllEventsTest(fixtures.TestBase):
def after_attach(obj, parent):
assert hasattr(obj, 'name') # so we can change it
canary.append("%s->%s" % (target.__name__, parent))
- event.listen(target, "before_parent_attach", before_attach)
- event.listen(target, "after_parent_attach", after_attach)
+ self.event_listen(target, "before_parent_attach", before_attach)
+ self.event_listen(target, "after_parent_attach", after_attach)
for target in [
schema.ForeignKeyConstraint, schema.PrimaryKeyConstraint,
@@ -2384,3 +2381,72 @@ class DialectKWArgTest(fixtures.TestBase):
"participating_y": True,
'participating2_y': "p2y",
"participating_z_one": "default"})
+
+class NamingConventionTest(fixtures.TestBase):
+ def _fixture(self, naming_convention):
+ m1 = MetaData(naming_convention=naming_convention)
+
+ u1 = Table('user', m1,
+ Column('id', Integer, primary_key=True),
+ Column('version', Integer, primary_key=True),
+ Column('data', String(30))
+ )
+
+ return u1
+
+ def test_uq_name(self):
+ u1 = self._fixture(naming_convention={
+ "uq": "uq_%(table_name)s_%(column_0_name)s"
+ })
+ uq = UniqueConstraint(u1.c.data)
+ eq_(uq.name, "uq_user_data")
+
+ def test_ck_name(self):
+ u1 = self._fixture(naming_convention={
+ "ck": "ck_%(table_name)s_%(constraint_name)s"
+ })
+ ck = CheckConstraint(u1.c.data == 'x', name='mycheck')
+ eq_(ck.name, "ck_user_mycheck")
+
+ assert_raises_message(
+ exc.InvalidRequestError,
+ r"Naming convention including %\(constraint_name\)s token "
+ "requires that constraint is explicitly named.",
+ CheckConstraint, u1.c.data == 'x'
+ )
+
+ def test_fk_attrs(self):
+ u1 = self._fixture(naming_convention={
+ "fk": "fk_%(table_name)s_%(column_0_name)s_"
+ "%(referred_table_name)s_%(referred_column_0_name)s"
+ })
+ m1 = u1.metadata
+ a1 = Table('address', m1,
+ Column('id', Integer, primary_key=True),
+ Column('user_id', Integer),
+ Column('user_version_id', Integer)
+ )
+ fk = ForeignKeyConstraint(['user_id', 'user_version_id'],
+ ['user.id', 'user.version'])
+ a1.append_constraint(fk)
+ eq_(fk.name, "fk_address_user_id_user_id")
+
+
+ def test_custom(self):
+ def key_hash(const, table):
+ return "HASH_%s" % table.name
+
+ u1 = self._fixture(naming_convention={
+ "fk": "fk_%(table_name)s_%(key_hash)s",
+ "key_hash": key_hash
+ })
+ m1 = u1.metadata
+ a1 = Table('address', m1,
+ Column('id', Integer, primary_key=True),
+ Column('user_id', Integer),
+ Column('user_version_id', Integer)
+ )
+ fk = ForeignKeyConstraint(['user_id', 'user_version_id'],
+ ['user.id', 'user.version'])
+ a1.append_constraint(fk)
+ eq_(fk.name, "fk_address_HASH_address")