summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES4
-rw-r--r--lib/sqlalchemy/orm/__init__.py16
-rw-r--r--lib/sqlalchemy/orm/mapper.py47
-rw-r--r--lib/sqlalchemy/orm/util.py25
-rw-r--r--lib/sqlalchemy/schema.py93
-rw-r--r--lib/sqlalchemy/sql/expression.py50
-rw-r--r--lib/sqlalchemy/sql/util.py4
-rw-r--r--lib/sqlalchemy/util.py21
-rw-r--r--test/orm/mapper.py10
9 files changed, 127 insertions, 143 deletions
diff --git a/CHANGES b/CHANGES
index 95ff2a410..7c0c77296 100644
--- a/CHANGES
+++ b/CHANGES
@@ -48,6 +48,10 @@ CHANGES
with arbitrary sementics. The orm now handles all mapped instances on
an identity-only basis. (e.g. 'is' vs '==') [ticket:676]
+ - the "properties" accessor on Mapper is removed; it now throws an informative
+ exception explaining the usage of mapper.get_property() and
+ mapper.iterate_properties
+
- The behavior of query.options() is now fully based on paths, i.e. an
option such as eagerload_all('x.y.z.y.x') will apply eagerloading to
only those paths, i.e. and not 'x.y.x'; eagerload('children.children')
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py
index 553bd57ba..90a172e62 100644
--- a/lib/sqlalchemy/orm/__init__.py
+++ b/lib/sqlalchemy/orm/__init__.py
@@ -17,7 +17,7 @@ from sqlalchemy.orm.properties import PropertyLoader, ColumnProperty, CompositeP
from sqlalchemy.orm import mapper as mapperlib
from sqlalchemy.orm import strategies
from sqlalchemy.orm.query import Query
-from sqlalchemy.orm.util import polymorphic_union
+from sqlalchemy.orm.util import polymorphic_union, create_row_adapter
from sqlalchemy.orm.session import Session as _Session
from sqlalchemy.orm.session import object_session, attribute_manager, sessionmaker
from sqlalchemy.orm.scoping import ScopedSession
@@ -621,18 +621,18 @@ def contains_alias(alias):
self.selectable = None
else:
self.selectable = alias
+ self._row_translators = {}
def get_selectable(self, mapper):
if self.selectable is None:
self.selectable = mapper.mapped_table.alias(self.alias)
return self.selectable
def translate_row(self, mapper, context, row):
- newrow = sautil.DictDecorator(row)
- selectable = self.get_selectable(mapper)
- for c in mapper.mapped_table.c:
- c2 = selectable.corresponding_column(c, keys_ok=True, raiseerr=False)
- if c2 and c2 in row:
- newrow[c] = row[c2]
- return newrow
+ if mapper in self._row_translators:
+ return self._row_translators[mapper](row)
+ else:
+ translator = create_row_adapter(self.get_selectable(mapper), mapper.mapped_table)
+ self._row_translators[mapper] = translator
+ return translator(row)
return ExtensionOption(AliasedRow(alias))
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index efc509725..3cfe7b3f6 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -9,7 +9,7 @@ from sqlalchemy import sql, util, exceptions, logging
from sqlalchemy.sql import expression
from sqlalchemy.sql import util as sqlutil
from sqlalchemy.orm import util as mapperutil
-from sqlalchemy.orm.util import ExtensionCarrier
+from sqlalchemy.orm.util import ExtensionCarrier, create_row_adapter
from sqlalchemy.orm import sync
from sqlalchemy.orm.interfaces import MapperProperty, EXT_CONTINUE, SynonymProperty, PropComparator
deferred_load = None
@@ -102,7 +102,7 @@ class Mapper(object):
self.inherit_condition = inherit_condition
self.inherit_foreign_keys = inherit_foreign_keys
self.extension = extension
- self.properties = properties or {}
+ self._init_properties = properties or {}
self.allow_column_override = allow_column_override
self.allow_null_pks = allow_null_pks
self.delete_orphans = []
@@ -110,7 +110,8 @@ class Mapper(object):
self.column_prefix = column_prefix
self.polymorphic_on = polymorphic_on
self._eager_loaders = util.Set()
-
+ self._row_translators = {}
+
# our 'polymorphic identity', a string name that when located in a result set row
# indicates this Mapper should be used to construct the object instance for that row.
self.polymorphic_identity = polymorphic_identity
@@ -195,6 +196,10 @@ class Mapper(object):
return self.__props.itervalues()
iterate_properties = property(iterate_properties, doc="returns an iterator of all MapperProperty objects.")
+ def properties(self):
+ raise NotImplementedError("Public collection of MapperProperty objects is provided by the get_property() and iterate_properties accessors.")
+ properties = property(properties)
+
def dispose(self):
# disaable any attribute-based compilation
self.__props_init = True
@@ -324,7 +329,7 @@ class Mapper(object):
self.inherits._add_polymorphic_mapping(self.polymorphic_identity, self)
if self.polymorphic_on is None:
if self.inherits.polymorphic_on is not None:
- self.polymorphic_on = self.mapped_table.corresponding_column(self.inherits.polymorphic_on, keys_ok=True, raiseerr=False)
+ self.polymorphic_on = self.mapped_table.corresponding_column(self.inherits.polymorphic_on, raiseerr=False)
else:
raise exceptions.ArgumentError("Mapper '%s' specifies a polymorphic_identity of '%s', but no mapper in it's hierarchy specifies the 'polymorphic_on' column argument" % (str(self), self.polymorphic_identity))
@@ -416,19 +421,20 @@ class Mapper(object):
if self.inherits is not None and not self.concrete and not self.primary_key_argument:
self.primary_key = self.inherits.primary_key
self._get_clause = self.inherits._get_clause
+ self._equivalent_columns = {}
else:
# create the "primary_key" for this mapper. this will flatten "equivalent" primary key columns
# into one column, where "equivalent" means that one column references the other via foreign key, or
# multiple columns that all reference a common parent column. it will also resolve the column
# against the "mapped_table" of this mapper.
- equivalent_columns = self._get_equivalent_columns()
+ self._equivalent_columns = self._get_equivalent_columns()
primary_key = expression.ColumnSet()
for col in (self.primary_key_argument or self.pks_by_table[self.mapped_table]):
c = self.mapped_table.corresponding_column(col, raiseerr=False)
if c is None:
- for cc in equivalent_columns[col]:
+ for cc in self._equivalent_columns[col]:
c = self.mapped_table.corresponding_column(cc, raiseerr=False)
if c is not None:
break
@@ -494,9 +500,6 @@ class Mapper(object):
set([tabled.col2])
}
- this method is called repeatedly during the compilation process as
- the resulting dictionary contains more equivalents as more inheriting
- mappers are compiled. the repetition process may be open to some optimization.
"""
result = {}
@@ -565,8 +568,8 @@ class Mapper(object):
self._columntoproperty = mapperutil.TranslatingDict(self.mapped_table)
# load custom properties
- if self.properties is not None:
- for key, prop in self.properties.iteritems():
+ if self._init_properties is not None:
+ for key, prop in self._init_properties.iteritems():
self._compile_property(key, prop, False)
# pull properties from the inherited mapper if any.
@@ -641,7 +644,7 @@ class Mapper(object):
if isinstance(prop, ColumnProperty):
# relate the mapper's "select table" to the given ColumnProperty
- col = self.select_table.corresponding_column(prop.columns[0], keys_ok=True, raiseerr=False)
+ col = self.select_table.corresponding_column(prop.columns[0], raiseerr=False)
# col might not be present! the selectable given to the mapper need not include "deferred"
# columns (included in zblog tests)
if col is None:
@@ -680,8 +683,8 @@ class Mapper(object):
if self.select_table is not self.mapped_table:
props = {}
- if self.properties is not None:
- for key, prop in self.properties.iteritems():
+ if self._init_properties is not None:
+ for key, prop in self._init_properties.iteritems():
if expression.is_column(prop):
props[key] = self.select_table.corresponding_column(prop)
elif (isinstance(prop, list) and expression.is_column(prop[0])):
@@ -779,7 +782,7 @@ class Mapper(object):
the given MapperProperty is compiled immediately.
"""
- self.properties[key] = prop
+ self._init_properties[key] = prop
self._compile_property(key, prop, init=self.__props_init)
def __str__(self):
@@ -1469,13 +1472,13 @@ class Mapper(object):
This can be used in conjunction with populate_instance to
populate an instance using an alternate mapper.
"""
-
- newrow = util.DictDecorator(row)
- for c in tomapper.mapped_table.c:
- c2 = self.mapped_table.corresponding_column(c, keys_ok=True, raiseerr=False)
- if c2 and c2 in row:
- newrow[c] = row[c2]
- return newrow
+
+ if tomapper in self._row_translators:
+ return self._row_translators[tomapper](row)
+ else:
+ translator = create_row_adapter(self.mapped_table, tomapper.mapped_table, equivalent_columns=self._equivalent_columns)
+ self._row_translators[tomapper] = translator
+ return translator(row)
def populate_instance(self, selectcontext, instance, row, ispostselect=None, isnew=False, **flags):
"""populate an instance from a result row."""
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 87b74c3c2..de9694bb2 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -91,7 +91,7 @@ class TranslatingDict(dict):
self.selectable = selectable
def __translate_col(self, col):
- ourcol = self.selectable.corresponding_column(col, keys_ok=False, raiseerr=False)
+ ourcol = self.selectable.corresponding_column(col, raiseerr=False)
if ourcol is None:
return col
else:
@@ -226,10 +226,27 @@ class AliasedClauses(object):
"""
return create_row_adapter(self.alias, self.mapped_table)
-def create_row_adapter(from_, to):
- map = {}
+def create_row_adapter(from_, to, equivalent_columns=None):
+ """create a row adapter between two selectables.
+
+ The returned adapter is a class that can be instantiated repeatedly for any number
+ of rows; this is an inexpensive process. However, the creation of the row
+ adapter class itself *is* fairly expensive so caching should be used to prevent
+ repeated calls to this function.
+ """
+
+ map = {}
for c in to.c:
- map[c] = from_.corresponding_column(c)
+ corr = from_.corresponding_column(c, raiseerr=False)
+ if corr:
+ map[c] = corr
+ elif equivalent_columns:
+ if c in equivalent_columns:
+ for c2 in equivalent_columns[c]:
+ corr = from_.corresponding_column(c2, raiseerr=False)
+ if corr:
+ map[c] = corr
+ break
class AliasedRow(object):
def __init__(self, row):
diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py
index 5ddca718a..a7f24a211 100644
--- a/lib/sqlalchemy/schema.py
+++ b/lib/sqlalchemy/schema.py
@@ -86,11 +86,8 @@ class _TableSingleton(expression._FigureVisitName):
def __call__(self, name, metadata, *args, **kwargs):
schema = kwargs.get('schema', None)
- autoload = kwargs.pop('autoload', False)
- autoload_with = kwargs.pop('autoload_with', False)
- mustexist = kwargs.pop('mustexist', False)
useexisting = kwargs.pop('useexisting', False)
- include_columns = kwargs.pop('include_columns', None)
+ mustexist = kwargs.pop('mustexist', False)
key = _get_table_key(name, schema)
try:
table = metadata.tables[key]
@@ -101,57 +98,23 @@ class _TableSingleton(expression._FigureVisitName):
except KeyError:
if mustexist:
raise exceptions.ArgumentError("Table '%s' not defined" % (key))
- table = type.__call__(self, name, metadata, **kwargs)
- table._set_parent(metadata)
- # load column definitions from the database if 'autoload' is defined
- # we do it after the table is in the singleton dictionary to support
- # circular foreign keys
- if autoload:
- try:
- if autoload_with:
- autoload_with.reflecttable(table, include_columns=include_columns)
- else:
- metadata._get_bind(raiseerr=True).reflecttable(table, include_columns=include_columns)
- except exceptions.NoSuchTableError:
+ try:
+ return type.__call__(self, name, metadata, *args, **kwargs)
+ except:
+ if key in metadata.tables:
del metadata.tables[key]
- raise
- # initialize all the column, etc. objects. done after
- # reflection to allow user-overrides
- table._init_items(*args)
- return table
+ raise
class Table(SchemaItem, expression.TableClause):
- """Represent a relational database table.
-
- This subclasses ``expression.TableClause`` to provide a table that is
- associated with an instance of ``MetaData``, which in turn
- may be associated with an instance of ``Engine``.
-
- Whereas ``TableClause`` represents a table as its used in an SQL
- expression, ``Table`` represents a table as it exists in a
- database schema.
-
- If this ``Table`` is ultimately associated with an engine,
- the ``Table`` gains the ability to access the database directly
- without the need for dealing with an explicit ``Connection`` object;
- this is known as "implicit execution".
-
- Implicit operation allows the ``Table`` to access the database to
- reflect its own properties (via the autoload=True flag), it allows
- the create() and drop() methods to be called without passing
- a connectable, and it also propigates the underlying engine to
- constructed SQL objects so that they too can be executed via their
- execute() method without the need for a ``Connection``.
- """
+ """Represent a relational database table."""
__metaclass__ = _TableSingleton
- def __init__(self, name, metadata, **kwargs):
+ def __init__(self, name, metadata, *args, **kwargs):
"""Construct a Table.
- Table objects can be constructed directly. The init method is
- actually called via the TableSingleton metaclass. Arguments
+ Table objects can be constructed directly. Arguments
are:
name
@@ -168,8 +131,8 @@ class Table(SchemaItem, expression.TableClause):
Should contain a listing of the Column objects for this table.
\**kwargs
- Options include:
-
+ kwargs include:
+
schema
The *schema name* for this table, which is
required if the table resides in a schema other than the
@@ -185,14 +148,14 @@ class Table(SchemaItem, expression.TableClause):
if autoload==True, this is an optional Engine or Connection
instance to be used for the table reflection. If ``None``,
the underlying MetaData's bound connectable will be used.
-
+
include_columns
A list of strings indicating a subset of columns to be
loaded via the ``autoload`` operation; table columns who
aren't present in this list will not be represented on the resulting
``Table`` object. Defaults to ``None`` which indicates all
columns should be reflected.
-
+
mustexist
Defaults to False: indicates that this Table must already
have been defined elsewhere in the application, else an
@@ -236,18 +199,35 @@ class Table(SchemaItem, expression.TableClause):
else:
self.fullname = self.name
self.owner = kwargs.pop('owner', None)
+
+ autoload = kwargs.pop('autoload', False)
+ autoload_with = kwargs.pop('autoload_with', None)
+ include_columns = kwargs.pop('include_columns', None)
+ # validate remaining kwargs that they all specify DB prefixes
if len([k for k in kwargs if not re.match(r'^(?:%s)_' % '|'.join(databases.__all__), k)]):
raise TypeError("Invalid argument(s) for Table: %s" % repr(kwargs.keys()))
- # store extra kwargs, which should only contain db-specific options
self.kwargs = kwargs
+
+ self._set_parent(metadata)
+ # load column definitions from the database if 'autoload' is defined
+ # we do it after the table is in the singleton dictionary to support
+ # circular foreign keys
+ if autoload:
+ if autoload_with:
+ autoload_with.reflecttable(self, include_columns=include_columns)
+ else:
+ metadata._get_bind(raiseerr=True).reflecttable(self, include_columns=include_columns)
+
+ # initialize all the column, etc. objects. done after
+ # reflection to allow user-overrides
+ self._init_items(*args)
key = property(lambda self:_get_table_key(self.name, self.schema))
def _export_columns(self, columns=None):
- # override FromClause's collection initialization logic; TableClause and Table
- # implement it differently
+ # override FromClause's collection initialization logic; Table implements it differently
pass
def _set_primary_key(self, pk):
@@ -510,6 +490,7 @@ class Column(SchemaItem, expression._ColumnClause):
table._columns.add(self)
else:
self._pre_existing_column = None
+
if self.primary_key:
table.primary_key.add(self)
elif self.key in table.primary_key:
@@ -632,8 +613,10 @@ class ForeignKey(SchemaItem):
# locate the parent table this foreign key is attached to.
# we use the "original" column which our parent column represents
# (its a list of columns/other ColumnElements if the parent table is a UNION)
- if isinstance(self.parent.base_column, Column):
- parenttable = self.parent.base_column.table
+ for c in self.parent.base_columns:
+ if isinstance(c, Column):
+ parenttable = c.table
+ break
else:
raise exceptions.ArgumentError("Parent column '%s' does not descend from a table-attached Column" % str(self.parent))
m = re.match(r"^(.+?)(?:\.(.+?))?(?:\.(.+?))?$", self._colspec, re.UNICODE)
diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py
index 51bd176c3..53364ba52 100644
--- a/lib/sqlalchemy/sql/expression.py
+++ b/lib/sqlalchemy/sql/expression.py
@@ -1393,15 +1393,13 @@ class ColumnElement(ClauseElement, _CompareMixin):
foreign_key = property(_one_fkey)
- def base_column(self):
- if hasattr(self, '_base_column'):
- return self._base_column
- p = self
- while hasattr(p, 'proxies'):
- p = p.proxies[0]
- self._base_column = p
- return p
- base_column = property(base_column)
+ def base_columns(self):
+ if hasattr(self, '_base_columns'):
+ return self._base_columns
+ self._base_columns = util.Set([c for c in self.proxy_set if not hasattr(c, 'proxies')])
+ return self._base_columns
+
+ base_columns = property(base_columns)
def proxy_set(self):
if hasattr(self, '_proxy_set'):
@@ -1578,7 +1576,7 @@ class FromClause(Selectable):
from sqlalchemy.sql import util
return util.ClauseAdapter(alias).traverse(self, clone=True)
- def corresponding_column(self, column, raiseerr=True, keys_ok=False, require_embedded=False):
+ def corresponding_column(self, column, raiseerr=True, require_embedded=False):
"""Given a ``ColumnElement``, return the exported ``ColumnElement``
object from this ``Selectable`` which corresponds to that
original ``Column`` via a common anscestor column.
@@ -1590,11 +1588,6 @@ class FromClause(Selectable):
if True, raise an error if the given ``ColumnElement`` could
not be matched. if False, non-matches will return None.
- keys_ok
- if the ``ColumnElement`` cannot be matched, attempt to match
- based on the string "key" property of the column alone. This
- makes the search much more liberal.
-
require_embedded
only return corresponding columns for the given
``ColumnElement``, if the given ``ColumnElement`` is
@@ -1618,18 +1611,17 @@ class FromClause(Selectable):
col, intersect = c, i
if col:
return col
+
+ if not raiseerr:
+ return None
else:
- if keys_ok:
- try:
- return self.c[column.name]
- except KeyError:
- pass
- if not raiseerr:
- return None
- else:
- raise exceptions.InvalidRequestError("Given column '%s', attached to table '%s', failed to locate a corresponding column from table '%s'" % (str(column), str(getattr(column, 'table', None)), self.description))
+ raise exceptions.InvalidRequestError("Given column '%s', attached to table '%s', failed to locate a corresponding column from table '%s'" % (str(column), str(getattr(column, 'table', None)), self.description))
def description(self):
+ """a brief description of this FromClause.
+
+ Used primarily for error message formatting.
+ """
return getattr(self, 'name', self.__class__.__name__ + " object")
description = property(description)
@@ -1669,21 +1661,19 @@ class FromClause(Selectable):
foreign_keys = property(_expr_attr_func('_foreign_keys'))
def _export_columns(self, columns=None):
- """Initialize column collections.
-
- """
+ """Initialize column collections."""
if hasattr(self, '_columns') and columns is None:
return
self._columns = ColumnCollection()
self._primary_key = ColumnSet()
self._foreign_keys = util.Set()
-
+
if columns is None:
columns = self._flatten_exportable_columns()
for co in columns:
cp = self._proxy_column(co)
-
+
def _flatten_exportable_columns(self):
"""Return the list of ColumnElements represented within this FromClause's _exportable_columns"""
export = self._exportable_columns()
@@ -2512,7 +2502,7 @@ class _Label(ColumnElement):
key = property(lambda s: s.name)
_label = property(lambda s: s.name)
proxies = property(lambda s:s.obj.proxies)
- base_column = property(lambda s:s.obj.base_column)
+ base_columns = property(lambda s:s.obj.base_columns)
proxy_set = property(lambda s:s.obj.proxy_set)
def expression_element(self):
diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py
index eed06cfc3..ecf4f3c16 100644
--- a/lib/sqlalchemy/sql/util.py
+++ b/lib/sqlalchemy/sql/util.py
@@ -235,10 +235,10 @@ class ClauseAdapter(AbstractClauseProcessor):
if self.exclude is not None:
if col in self.exclude:
return None
- newcol = self.selectable.corresponding_column(col, raiseerr=False, require_embedded=True, keys_ok=False)
+ newcol = self.selectable.corresponding_column(col, raiseerr=False, require_embedded=True)
if newcol is None and self.equivalents is not None and col in self.equivalents:
for equiv in self.equivalents[col]:
- newcol = self.selectable.corresponding_column(equiv, raiseerr=False, require_embedded=True, keys_ok=False)
+ newcol = self.selectable.corresponding_column(equiv, raiseerr=False, require_embedded=True)
if newcol:
return newcol
return newcol
diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py
index 9ad7e113c..e8c34b513 100644
--- a/lib/sqlalchemy/util.py
+++ b/lib/sqlalchemy/util.py
@@ -409,27 +409,6 @@ except ImportError:
def __setattr__(self, key, value):
self._tdict[(thread.get_ident(), key)] = value
-class DictDecorator(dict):
- """A Dictionary that delegates items not found to a second wrapped dictionary."""
-
- def __init__(self, decorate):
- self.decorate = decorate
-
- def __getitem__(self, key):
- try:
- return dict.__getitem__(self, key)
- except KeyError:
- return self.decorate[key]
-
- def __contains__(self, key):
- return dict.__contains__(self, key) or key in self.decorate
-
- def has_key(self, key):
- return key in self
-
- def __repr__(self):
- return dict.__repr__(self) + repr(self.decorate)
-
class OrderedSet(Set):
def __init__(self, d=None):
Set.__init__(self)
diff --git a/test/orm/mapper.py b/test/orm/mapper.py
index b37c985a6..4402fe48f 100644
--- a/test/orm/mapper.py
+++ b/test/orm/mapper.py
@@ -37,7 +37,15 @@ class MapperTest(MapperSuperTest):
assert False
except exceptions.ArgumentError:
pass
-
+
+ def test_prop_accessor(self):
+ mapper(User, users)
+ try:
+ class_mapper(User).properties
+ assert False
+ except NotImplementedError, uoe:
+ assert str(uoe) == "Public collection of MapperProperty objects is provided by the get_property() and iterate_properties accessors."
+
def test_badcascade(self):
mapper(Address, addresses)
try: