summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql
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 /lib/sqlalchemy/sql
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
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r--lib/sqlalchemy/sql/__init__.py3
-rw-r--r--lib/sqlalchemy/sql/naming.py110
-rw-r--r--lib/sqlalchemy/sql/schema.py84
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)