From: Mike Bayer Date: Mon, 5 Nov 2007 17:18:21 +0000 (+0000) Subject: - base_columns on ColumnElement becomes a list; as usual, because columns in Compound... X-Git-Tag: rel_0_4_1~60 X-Git-Url: http://git.ipfire.org/gitweb/gitweb.cgi?a=commitdiff_plain;h=977e86dc9be38d9f8a16e1f17141486b0f84a43a;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - base_columns on ColumnElement becomes a list; as usual, because columns in CompoundSelects may extend from more than one root column. - keys_ok argument from corresponding_column() removed. no more name-based matching of columns anywhere. - DictDecorator is gone. all row translators provided by orm.util.create_row_adapter(). Mapper and contains_alias() cache the adapters on target mapper to avoid re-computation of adapters. - create_row_adapter() accepts an "equivalent_columns" map as produced by Mapper, so that row adapters can take join conditions into account (as usual again, to help with the CompoundSelects produced by polymorphic_union). - simplified TableSingleton to just provide lookup; moved all initialization into Table. - the "properties" accessor on Mapper is removed; it now throws an informative exception explaining the usage of mapper.get_property() and mapper.iterate_properties --- diff --git a/CHANGES b/CHANGES index 95ff2a4108..7c0c77296e 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 553bd57baf..90a172e624 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 efc5097252..3cfe7b3f61 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 87b74c3c27..de9694bb2f 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 5ddca718a8..a7f24a211a 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 51bd176c37..53364ba52d 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 eed06cfc32..ecf4f3c163 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 9ad7e113c4..e8c34b513e 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 b37c985a60..4402fe48f0 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: