diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-02-01 18:21:04 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-02-01 18:21:04 -0500 |
| commit | 5b0919f3f5c7678c587858a47e38acd4a5b82f25 (patch) | |
| tree | 236ff9c4c4e8688aa9b1b5b5a9ea7f20ea3807fd /lib/sqlalchemy/sql | |
| parent | 32a1db368599f6f3dbb3765ef5f11640d1725672 (diff) | |
| download | sqlalchemy-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
Diffstat (limited to 'lib/sqlalchemy/sql')
| -rw-r--r-- | lib/sqlalchemy/sql/__init__.py | 3 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/naming.py | 110 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/schema.py | 84 |
3 files changed, 189 insertions, 8 deletions
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) |
