[Sqlalchemy-commits] [1473] sqlalchemy/branches/schema/test: overhaul to Mapper's "select_table" att
Brought to you by:
zzzeek
From: <co...@sq...> - 2006-05-19 00:13:25
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head><style type="text/css"><!-- #msg dl { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; } #msg dt { float: left; width: 6em; font-weight: bold; } #msg dt:after { content:':';} #msg dl, #msg dt, #msg ul, #msg li { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; } #msg dl a { font-weight: bold} #msg dl a:link { color:#fc3; } #msg dl a:active { color:#ff0; } #msg dl a:visited { color:#cc6; } h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; } #msg pre { overflow: auto; background: #ffc; border: 1px #fc0 solid; padding: 6px; } #msg ul, pre { overflow: auto; } #patch { width: 100%; } #patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;} #patch .propset h4, #patch .binary h4 {margin:0;} #patch pre {padding:0;line-height:1.2em;margin:0;} #patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;} #patch .propset .diff, #patch .binary .diff {padding:10px 0;} #patch span {display:block;padding:0 10px;} #patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;} #patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;} #patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;} #patch .lines, .info {color:#888;background:#fff;} --></style> <title>[1473] sqlalchemy/branches/schema/test: overhaul to Mapper's "select_table" attribute, now spawns a second mapper to handle all</title> </head> <body> <div id="msg"> <dl> <dt>Revision</dt> <dd>1473</dd> <dt>Author</dt> <dd>zzzeek</dd> <dt>Date</dt> <dd>2006-05-18 19:13:01 -0500 (Thu, 18 May 2006)</dd> </dl> <h3>Log Message</h3> <pre>overhaul to Mapper's "select_table" attribute, now spawns a second mapper to handle all select operations, and also is the target for properties. "select_table" no longer impacts primary mapper functions, simplifies column/property targeting issues. also some fixes to better allow inherited custom properties to propigate. new unittest for polymorph example added, runs through eight sets of options to test various things that were broken before this checkin some tweaks to schema using the name of tables 'Connectable' now has more methods that are shared between Conneciton and ComposedSQLEngine FromClause has additional "keys_ok" option on corresponding_column() which produces a more liberal lookup, used by mapper.translate_row edits to dbengine docs</pre> <h3>Modified Paths</h3> <ul> <li><a href="#sqlalchemybranchesschemadocbuildcontentdbenginetxt">sqlalchemy/branches/schema/doc/build/content/dbengine.txt</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemyenginebasepy">sqlalchemy/branches/schema/lib/sqlalchemy/engine/base.py</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemyormmapperpy">sqlalchemy/branches/schema/lib/sqlalchemy/orm/mapper.py</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemyormpropertiespy">sqlalchemy/branches/schema/lib/sqlalchemy/orm/properties.py</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemyormquerypy">sqlalchemy/branches/schema/lib/sqlalchemy/orm/query.py</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemyormsessionpy">sqlalchemy/branches/schema/lib/sqlalchemy/orm/session.py</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemyschemapy">sqlalchemy/branches/schema/lib/sqlalchemy/schema.py</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemysqlpy">sqlalchemy/branches/schema/lib/sqlalchemy/sql.py</a></li> <li><a href="#sqlalchemybranchesschematestalltestspy">sqlalchemy/branches/schema/test/alltests.py</a></li> <li><a href="#sqlalchemybranchesschematestpolymorphpy">sqlalchemy/branches/schema/test/polymorph.py</a></li> </ul> </div> <div id="patch"> <h3>Diff</h3> <a id="sqlalchemybranchesschemadocbuildcontentdbenginetxt"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/doc/build/content/dbengine.txt (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/doc/build/content/dbengine.txt 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/doc/build/content/dbengine.txt 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -76,11 +76,11 @@ </span><span class="cx"> </span><span class="cx"> ### Using Connections {@name=connections} </span><span class="cx"> </span><del>-In this section we describe the explicit interface available on Engine. Note that when using the Object Relational Mapper (ORM) as well as when dealing with only with "bound" metadata objects (described later), SQLAlchemy deals with the Engine for you and you generally don't need to know much about it; in those cases, you can skip this section and go to [metadata](rel:metadata). </del><ins>+In this section we describe the SQL execution interface available from an `Engine` instance. Note that when using the Object Relational Mapper (ORM) as well as when dealing with with "bound" metadata objects (described later), SQLAlchemy deals with the Engine for you and you generally don't need to know much about it; in those cases, you can skip this section and go to [metadata](rel:metadata). </ins><span class="cx"> </span><del>-The Engine provides methods by which literal SQL text as well as SQL clause constructs can be compiled and executed. </del><ins>+The Engine provides a `connect()` method which returns a `Connection` object. This object provides methods by which literal SQL text as well as SQL clause constructs can be compiled and executed. </ins><span class="cx"> </span><del>- {python title="Explicit Connection"} </del><ins>+ {python} </ins><span class="cx"> engine = create_engine('sqlite:///:memory:') </span><span class="cx"> connection = engine.connect() </span><span class="cx"> result = connection.execute("select * from mytable where col1=:col1", col1=5) </span><span class="lines">@@ -90,15 +90,7 @@ </span><span class="cx"> </span><span class="cx"> The `close` method on `Connection` does not actually remove the underlying connection to the database, but rather indicates that the underlying resources can be returned to the connection pool. When using the `connect()` method, the DBAPI connection referenced by the `Connection` object is not referenced anywhere else. </span><span class="cx"> </span><del>- {python title="Implicit Connection"} - engine = create_engine('sqlite:///:memory:') - result = engine.execute("select * from mytable where col1=:col1", col1=5) - for row in result: - print row['col1'], row['col2'] - result.close() </del><span class="cx"> </span><del>-When executing off the Engine directly, a Connection is created and used automatically. The returned `ResultProxy` then has a `close()` method, which will return the resources used by the `Connection`. This is a more abbreviated style of usage which is also the method used when dealing with "bound" schema and statement objects, which are described later. - </del><span class="cx"> In both execution styles above, the `Connection` object will also automatically return its resources to the connection pool when the object is garbage collected, i.e. its `__del__()` method is called. When using the standard C implementation of Python, this method is usually called immediately as soon as the object is dereferenced. With other Python implementations such as Jython, this is not so guaranteed. </span><span class="cx"> </span><span class="cx"> The execute method on `Engine` and `Connection` can also receive SQL clause constructs as well, which are described in [sql](rel:sql): </span><span class="lines">@@ -110,7 +102,7 @@ </span><span class="cx"> print row['col1'], row['col2'] </span><span class="cx"> connection.close() </span><span class="cx"> </span><del>-In many cases, the `Connection` and `Engine` can be used interchangeably; as they both provide an `engine` attribute as well as similar `execute` methods, most SQLAlchemy functions which take an `Engine` as a parameter with which to execute SQL will also accept a `Connection`: </del><ins>+Both `Connection` and `Engine` fulfill an interface known as `Connectable` which specifies common functionality between the two objects, such as getting a `Connection` and executing queries. Therefore, most SQLAlchemy functions which take an `Engine` as a parameter with which to execute SQL will also accept a `Connection`: </ins><span class="cx"> </span><span class="cx"> {python title="Specify Engine or Connection"} </span><span class="cx"> engine = create_engine('sqlite:///:memory:') </span><span class="lines">@@ -128,8 +120,19 @@ </span><span class="cx"> </span><span class="cx"> #### Implicit Connection Contexts {@name=context} </span><span class="cx"> </span><del>-"Implicit" connections refer to the example above when the `execute()` method is called directly off the `Engine` object, *without* the usage of a `Connection` object, and resources are released by calling the `close()` method on the result object. When using "implicit" connections, the user has two choices, determined when the Engine is first created, as to how the resources of this connection should be used in relation to other connections. This is determined by the `strategy` argument to `create_engine()`, which has two possible values: `plain` and `threadlocal`. In `plain`, every `execute` call uses a distinct connection from the database, which is only released when the `close()` method on the Result is called, or the result object itself and its underlying Connection falls out of scope and is garbage collected. In `threadlocal`, multiple calls to `execute()` within the same thread will use the already-checked out connection resource! if one is available, or if none is available will request a connection resource. </del><ins>+SQL operations can also be performed using the `Engine` alone without explicitly referencing a `Connection`. The operation is executed with a connection that is allocated internally. This type of connection, i.e. the automatically allocated connection when calling `execute()` directly off of an `Engine`, is called an **implicit connection**. </ins><span class="cx"> </span><ins>+ {python title="Implicit Connection"} + engine = create_engine('sqlite:///:memory:') + result = engine.execute("select * from mytable where col1=:col1", col1=5) + for row in result: + print row['col1'], row['col2'] + result.close() + +When using implicit connections, the returned `ResultProxy` has a `close()` method which will return the resources used by the `Connection`. While the implicit method of execution seems redundant in light of the explicit `Connection` object available, it is essentially the same methodology used by "bound" schema and statement objects to execute themselves, so the same rules of connection allocation apply. + +The user has two choices, determined when the Engine is first created, as to how the resources of this connection should be used in relation to other connections. This is determined by the `strategy` argument to `create_engine()`, which has two possible values: `plain` and `threadlocal`. In `plain`, every `execute` call uses a distinct connection from the database, which is only released when the `close()` method on the Result is called, or the result object itself and its underlying Connection falls out of scope and is garbage collected. In `threadlocal`, multiple calls to `execute()` within the same thread will use the already-checked out connection resource if one is available, or if none is available will request a connection resource. + </ins><span class="cx"> It is crucial to note that the `plain` and `threadlocal` contexts **do not impact the connect() method on the Engine.** If you are using explicit Connection objects returned by `connect()` method, you have full control over the connection resources used. </span><span class="cx"> </span><span class="cx"> The `plain` strategy is better suited to an application that insures the explicit releasing of the resources used by each execution. This is because each execution uses its own distinct connection resource, and as those resources remain open, multiple connections can be checked out from the pool quickly. Since the connection pool will block further requests when too many connections have been checked out, not keeping track of this can impact an application's stability. </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemyenginebasepy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/engine/base.py (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/engine/base.py 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/lib/sqlalchemy/engine/base.py 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -157,6 +157,13 @@ </span><span class="cx"> def contextual_connect(self): </span><span class="cx"> """returns a Connection object which may be part of an ongoing context.""" </span><span class="cx"> raise NotImplementedError() </span><ins>+ def create(self, entity, **kwargs): + """creates a table or index given an appropriate schema object.""" + raise NotImplementedError() + def drop(self, entity, **kwargs): + raise NotImplementedError() + def execute(self, object, *multiparams, **params): + raise NotImplementedError() </ins><span class="cx"> def _not_impl(self): </span><span class="cx"> raise NotImplementedError() </span><span class="cx"> engine = property(_not_impl, doc="returns the Engine which this Connectable is associated with.") </span><span class="lines">@@ -532,6 +539,7 @@ </span><span class="cx"> try: </span><span class="cx"> rec = self.props[key._label.lower()] </span><span class="cx"> except KeyError: </span><ins>+ print "DIDNT HAVE", key._label.lower() </ins><span class="cx"> try: </span><span class="cx"> rec = self.props[key.key.lower()] </span><span class="cx"> except KeyError: </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemyormmapperpy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/orm/mapper.py (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/orm/mapper.py 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/lib/sqlalchemy/orm/mapper.py 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -79,6 +79,8 @@ </span><span class="cx"> self.polymorphic_map = {} </span><span class="cx"> else: </span><span class="cx"> self.polymorphic_map = polymorphic_map </span><ins>+ self.__surrogate_mapper = None + self._surrogate_parent = None </ins><span class="cx"> </span><span class="cx"> if not issubclass(class_, object): </span><span class="cx"> raise exceptions.ArgumentError("Class '%s' is not a new-style class" % class_.__name__) </span><span class="lines">@@ -152,10 +154,9 @@ </span><span class="cx"> </span><span class="cx"> if select_table is not None: </span><span class="cx"> self.select_table = select_table </span><del>- self.unjoined_table = self.select_table </del><span class="cx"> else: </span><span class="cx"> self.select_table = self.mapped_table </span><del>- self.unjoined_table = self.local_table </del><ins>+ self.unjoined_table = self.local_table </ins><span class="cx"> </span><span class="cx"> # locate all tables contained within the "table" passed in, which </span><span class="cx"> # may be a join or other construct </span><span class="lines">@@ -169,9 +170,9 @@ </span><span class="cx"> if k.table != self.mapped_table: </span><span class="cx"> # associate pk cols from subtables to the "main" table </span><span class="cx"> self.pks_by_table.setdefault(self.mapped_table, util.HashSet(ordered=True)).append(k) </span><del>- # TODO: need select_table, local_table properly accounted for when custom primary key is sent </del><ins>+ # TODO: need local_table properly accounted for when custom primary key is sent </ins><span class="cx"> else: </span><del>- for t in self.tables + [self.mapped_table, self.select_table]: </del><ins>+ for t in self.tables + [self.mapped_table]: </ins><span class="cx"> try: </span><span class="cx"> l = self.pks_by_table[t] </span><span class="cx"> except KeyError: </span><span class="lines">@@ -191,22 +192,31 @@ </span><span class="cx"> # table columns mapped to lists of MapperProperty objects </span><span class="cx"> # using a list allows a single column to be defined as </span><span class="cx"> # populating multiple object attributes </span><del>- self.columntoproperty = TranslatingDict(self.select_table) </del><ins>+ self.columntoproperty = TranslatingDict(self.mapped_table) </ins><span class="cx"> </span><span class="cx"> # load custom properties </span><span class="cx"> if properties is not None: </span><span class="cx"> for key, prop in properties.iteritems(): </span><span class="cx"> self.add_property(key, prop, False) </span><span class="cx"> </span><ins>+ if inherits is not None: + inherits._inheriting_mappers.add(self) + for key, prop in inherits.props.iteritems(): + if not self.props.has_key(key): + p = prop.copy() + if p.adapt(self): + self.add_property(key, p, init=False) + </ins><span class="cx"> # load properties from the main table object, </span><span class="cx"> # not overriding those set up in the 'properties' argument </span><del>- for column in self.select_table.columns: </del><ins>+ for column in self.mapped_table.columns: + + if self.columntoproperty.has_key(column): + continue + </ins><span class="cx"> if not self.columns.has_key(column.key): </span><span class="cx"> self.columns[column.key] = column </span><span class="cx"> </span><del>- if self.columntoproperty.has_key(column): - continue - </del><span class="cx"> prop = self.props.get(column.key, None) </span><span class="cx"> if prop is None: </span><span class="cx"> prop = ColumnProperty(column) </span><span class="lines">@@ -229,7 +239,7 @@ </span><span class="cx"> # back to the property </span><span class="cx"> proplist = self.columntoproperty.setdefault(column, []) </span><span class="cx"> proplist.append(prop) </span><del>- </del><ins>+ </ins><span class="cx"> if not non_primary and (not mapper_registry.has_key(self.class_key) or self.is_primary or (inherits is not None and inherits._is_primary_mapper())): </span><span class="cx"> sessionlib.global_attributes.reset_class_managed(self.class_) </span><span class="cx"> self._init_class() </span><span class="lines">@@ -240,54 +250,71 @@ </span><span class="cx"> if isinstance(self.polymorphic_map[key], type): </span><span class="cx"> self.polymorphic_map[key] = class_mapper(self.polymorphic_map[key]) </span><span class="cx"> </span><del>- if inherits is not None: - inherits._inheriting_mappers.add(self) - for key, prop in inherits.props.iteritems(): - if not self.props.has_key(key): - self.props[key] = prop.copy() - self.props[key].parent = self - # self.props[key].key = None # force re-init </del><span class="cx"> l = [(key, prop) for key, prop in self.props.iteritems()] </span><span class="cx"> for key, prop in l: </span><span class="cx"> if getattr(prop, 'key', None) is None: </span><span class="cx"> prop.init(key, self) </span><span class="cx"> </span><ins>+ # select_table specified...set up a surrogate mapper that will be used for selects + # select_table has to encompass all the columns of the mapped_table either directly + # or through proxying relationships + if self.select_table is not self.mapped_table: + props = {} + if properties is not None: + for key, prop in properties.iteritems(): + if sql.is_column(prop): + props[key] = self.select_table.corresponding_column(prop) + elif (isinstance(column, list) and sql.is_column(column[0])): + props[key] = [self.select_table.corresponding_column(c) for c in prop] + self.__surrogate_mapper = Mapper(self.class_, self.select_table, non_primary=True, properties=props, polymorphic_map=self.polymorphic_map, polymorphic_on=self.polymorphic_on) + </ins><span class="cx"> def add_polymorphic_mapping(self, key, class_or_mapper, entity_name=None): </span><span class="cx"> if isinstance(class_or_mapper, type): </span><span class="cx"> class_or_mapper = class_mapper(class_or_mapper, entity_name=entity_name) </span><span class="cx"> self.polymorphic_map[key] = class_or_mapper </span><del>- - </del><span class="cx"> </span><del>- </del><span class="cx"> def add_properties(self, dict_of_properties): </span><span class="cx"> """adds the given dictionary of properties to this mapper, using add_property.""" </span><span class="cx"> for key, value in dict_of_properties.iteritems(): </span><span class="cx"> self.add_property(key, value, True) </span><ins>+ + def _create_prop_from_column(self, column, skipmissing=False): + if sql.is_column(column): + try: + column = self.mapped_table.corresponding_column(column) + except KeyError: + if skipmissing: + return + raise exceptions.ArgumentError("Column '%s' is not represented in mapper's table" % prop._label) + return ColumnProperty(column) + elif isinstance(column, list) and sql.is_column(column[0]): + try: + column = [self.mapped_table.corresponding_column(c) for c in column] + except KeyError, e: + # TODO: want to take the columns we have from this + if skipmissing: + return + raise exceptions.ArgumentError("Column '%s' is not represented in mapper's table" % e.args[0]) + return ColumnProperty(*column) + else: + return None </ins><span class="cx"> </span><del>- def add_property(self, key, prop, init=True): </del><ins>+ def add_property(self, key, prop, init=True, skipmissing=False): </ins><span class="cx"> """adds an additional property to this mapper. this is the same as if it were </span><span class="cx"> specified within the 'properties' argument to the constructor. if the named </span><span class="cx"> property already exists, this will replace it. Useful for </span><span class="cx"> circular relationships, or overriding the parameters of auto-generated properties </span><span class="cx"> such as backreferences.""" </span><span class="cx"> </span><del>- if sql.is_column(prop): - try: - prop = self.select_table.corresponding_column(prop) - except KeyError: - raise exceptions.ArgumentError("Column '%s' is not represented in mapper's table" % prop._label) - self.columns[key] = prop - prop = ColumnProperty(prop) - elif isinstance(prop, list) and sql.is_column(prop[0]): - try: - prop = [self.select_table.corresponding_column(p) for p in prop] - except KeyError, e: - raise exceptions.ArgumentError("Column '%s' is not represented in mapper's table" % e.args[0]) - self.columns[key] = prop[0] - prop = ColumnProperty(*prop) </del><ins>+ if not isinstance(prop, MapperProperty): + prop = self._create_prop_from_column(prop, skipmissing=skipmissing) + if prop is None: + raise exceptions.ArgumentError("'%s' is not an instance of MapperProperty or Column" % repr(prop)) + </ins><span class="cx"> self.props[key] = prop </span><ins>+ </ins><span class="cx"> if isinstance(prop, ColumnProperty): </span><ins>+ self.columns[key] = prop.columns[0] </ins><span class="cx"> for col in prop.columns: </span><span class="cx"> proplist = self.columntoproperty.setdefault(col, []) </span><span class="cx"> proplist.append(prop) </span><span class="lines">@@ -297,11 +324,11 @@ </span><span class="cx"> </span><span class="cx"> for mapper in self._inheriting_mappers: </span><span class="cx"> p = prop.copy() </span><del>- p.parent = mapper - mapper.add_property(key, p, init=False) </del><ins>+ if p.adapt(mapper): + mapper.add_property(key, p, init=False) </ins><span class="cx"> </span><span class="cx"> def __str__(self): </span><del>- return "Mapper|" + self.class_.__name__ + "|" + (self.entity_name is not None and "/%s" % self.entity_name or "") + self.select_table.name </del><ins>+ return "Mapper|" + self.class_.__name__ + "|" + (self.entity_name is not None and "/%s" % self.entity_name or "") + self.mapped_table.name </ins><span class="cx"> </span><span class="cx"> def _is_primary_mapper(self): </span><span class="cx"> """returns True if this mapper is the primary mapper for its class key (class + entity_name)""" </span><span class="lines">@@ -416,7 +443,7 @@ </span><span class="cx"> </span><span class="cx"> def identity(self, instance): </span><span class="cx"> """returns the identity (list of primary key values) for the given instance. The list of values can be fed directly into the get() method as mapper.get(*key).""" </span><del>- return [self._getattrbycolumn(instance, column) for column in self.pks_by_table[self.select_table]] </del><ins>+ return [self._getattrbycolumn(instance, column) for column in self.pks_by_table[self.mapped_table]] </ins><span class="cx"> </span><span class="cx"> </span><span class="cx"> def copy(self, **kwargs): </span><span class="lines">@@ -690,21 +717,23 @@ </span><span class="cx"> yield c </span><span class="cx"> </span><span class="cx"> def _row_identity_key(self, row): </span><del>- return sessionlib.get_row_key(row, self.class_, self.pks_by_table[self.select_table], self.entity_name) </del><ins>+ return sessionlib.get_row_key(row, self.class_, self.pks_by_table[self.mapped_table], self.entity_name) </ins><span class="cx"> </span><ins>+ def get_select_mapper(self): + return self.__surrogate_mapper or self + </ins><span class="cx"> def _instance(self, session, row, imap, result = None, populate_existing = False): </span><span class="cx"> """pulls an object instance from the given row and appends it to the given result </span><span class="cx"> list. if the instance already exists in the given identity map, its not added. in </span><span class="cx"> either case, executes all the property loaders on the instance to also process extra </span><span class="cx"> information in the row.""" </span><del>- </del><ins>+ </ins><span class="cx"> if self.polymorphic_on is not None: </span><span class="cx"> discriminator = row[self.polymorphic_on] </span><span class="cx"> mapper = self.polymorphic_map[discriminator] </span><span class="cx"> if mapper is not self: </span><span class="cx"> row = self.translate_row(mapper, row) </span><span class="cx"> return mapper._instance(session, row, imap, result=result, populate_existing=populate_existing) </span><del>- </del><span class="cx"> </span><span class="cx"> # look in main identity map. if its there, we dont do anything to it, </span><span class="cx"> # including modifying any of its related items lists, as its already </span><span class="lines">@@ -730,7 +759,7 @@ </span><span class="cx"> if not exists: </span><span class="cx"> # check if primary key cols in the result are None - this indicates </span><span class="cx"> # an instance of the object is not present in the row </span><del>- for col in self.pks_by_table[self.select_table]: </del><ins>+ for col in self.pks_by_table[self.mapped_table]: </ins><span class="cx"> if row[col] is None: </span><span class="cx"> return None </span><span class="cx"> # plugin point </span><span class="lines">@@ -768,8 +797,8 @@ </span><span class="cx"> """attempts to take a row and translate its values to a row that can </span><span class="cx"> be understood by another mapper.""" </span><span class="cx"> newrow = util.DictDecorator(row) </span><del>- for c in tomapper.select_table.c: - c2 = self.select_table.corresponding_column(c) </del><ins>+ for c in tomapper.mapped_table.c: + c2 = self.mapped_table.corresponding_column(c, keys_ok=True, raiseerr=True) </ins><span class="cx"> newrow[c] = row[c2] </span><span class="cx"> return newrow </span><span class="cx"> </span><span class="lines">@@ -880,6 +909,14 @@ </span><span class="cx"> self.key = key </span><span class="cx"> self.parent = parent </span><span class="cx"> self.do_init(key, parent) </span><ins>+ def adapt(self, newparent): + """adapts this MapperProperty to a new parent, assuming the new parent is an inheriting + descendant of the old parent. Should return True if the adaptation was successful, or + False if this MapperProperty cannot be adapted to the new parent (the case for this is, + the parent mapper has a polymorphic select, and this property represents a column that is not + represented in the new mapper's mapped table)""" + self.parent = newparent + return True </ins><span class="cx"> def do_init(self, key, parent): </span><span class="cx"> """template method for subclasses""" </span><span class="cx"> pass </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemyormpropertiespy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/orm/properties.py (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/orm/properties.py 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/lib/sqlalchemy/orm/properties.py 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -186,11 +186,13 @@ </span><span class="cx"> else: </span><span class="cx"> self.mapper = self.argument </span><span class="cx"> </span><ins>+ self.mapper = self.mapper.get_select_mapper() + </ins><span class="cx"> if self.association is not None: </span><span class="cx"> if isinstance(self.association, type): </span><span class="cx"> self.association = mapper.class_mapper(self.association) </span><span class="cx"> </span><del>- self.target = self.mapper.select_table </del><ins>+ self.target = self.mapper.mapped_table </ins><span class="cx"> self.key = key </span><span class="cx"> self.parent = parent </span><span class="cx"> </span><span class="lines">@@ -316,8 +318,8 @@ </span><span class="cx"> </span><span class="cx"> The list of rules is used within commits by the _synchronize() method when dependent </span><span class="cx"> objects are processed.""" </span><del>- parent_tables = util.HashSet(self.parent.tables + [self.parent.mapped_table, self.parent.select_table]) - target_tables = util.HashSet(self.mapper.tables + [self.mapper.mapped_table, self.mapper.select_table]) </del><ins>+ parent_tables = util.HashSet(self.parent.tables + [self.parent.mapped_table]) + target_tables = util.HashSet(self.mapper.tables + [self.mapper.mapped_table]) </ins><span class="cx"> </span><span class="cx"> self.syncrules = sync.ClauseSynchronizer(self.parent, self.mapper, self.direction) </span><span class="cx"> if self.direction == sync.MANYTOMANY: </span><span class="lines">@@ -360,7 +362,7 @@ </span><span class="cx"> # to possibly save a DB round trip </span><span class="cx"> if self.use_get: </span><span class="cx"> ident = [] </span><del>- for primary_key in self.mapper.pks_by_table[self.mapper.select_table]: </del><ins>+ for primary_key in self.mapper.pks_by_table[self.mapper.mapped_table]: </ins><span class="cx"> bind = self.lazyreverse[primary_key] </span><span class="cx"> ident.append(params[bind.key]) </span><span class="cx"> return self.mapper.using(session).get(ident) </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemyormquerypy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/orm/query.py (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/orm/query.py 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/lib/sqlalchemy/orm/query.py 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -15,6 +15,8 @@ </span><span class="cx"> self.mapper = class_mapper(class_or_mapper, entity_name=entity_name) </span><span class="cx"> else: </span><span class="cx"> self.mapper = class_or_mapper </span><ins>+ self.mapper = self.mapper.get_select_mapper() + </ins><span class="cx"> self.always_refresh = kwargs.pop('always_refresh', self.mapper.always_refresh) </span><span class="cx"> self.order_by = kwargs.pop('order_by', self.mapper.order_by) </span><span class="cx"> self.extension = kwargs.pop('extension', self.mapper.extension) </span><span class="lines">@@ -31,7 +33,6 @@ </span><span class="cx"> else: </span><span class="cx"> return self._session </span><span class="cx"> table = property(lambda s:s.mapper.select_table) </span><del>- props = property(lambda s:s.mapper.props) </del><span class="cx"> session = property(_get_session) </span><span class="cx"> </span><span class="cx"> def get(self, ident, **kwargs): </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemyormsessionpy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/orm/session.py (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/orm/session.py 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/lib/sqlalchemy/orm/session.py 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -164,12 +164,12 @@ </span><span class="cx"> return self.bind_to </span><span class="cx"> elif self.binds.has_key(mapper): </span><span class="cx"> return self.binds[mapper] </span><del>- elif self.binds.has_key(mapper.select_table): - return self.binds[mapper.select_table] </del><ins>+ elif self.binds.has_key(mapper.mapped_table): + return self.binds[mapper.mapped_table] </ins><span class="cx"> elif self.bind_to is not None: </span><span class="cx"> return self.bind_to </span><span class="cx"> else: </span><del>- e = mapper.select_table.engine </del><ins>+ e = mapper.mapped_table.engine </ins><span class="cx"> if e is None: </span><span class="cx"> raise exceptions.InvalidRequestError("Could not locate any Engine bound to mapper '%s'" % str(mapper)) </span><span class="cx"> return e </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemyschemapy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/schema.py (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/schema.py 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/lib/sqlalchemy/schema.py 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -214,15 +214,15 @@ </span><span class="cx"> issue a SQL DROP statement.""" </span><span class="cx"> key = _get_table_key(self.name, self.schema) </span><span class="cx"> del self.metadata.tables[key] </span><del>- def create(self, engine=None): - if engine is not None: - engine.create(self) </del><ins>+ def create(self, connectable=None): + if connectable is not None: + connectable.create(self) </ins><span class="cx"> else: </span><span class="cx"> self.engine.create(self) </span><span class="cx"> return self </span><del>- def drop(self, engine=None): - if engine is not None: - engine.drop(self) </del><ins>+ def drop(self, connectable=None): + if connectable is not None: + connectable.drop(self) </ins><span class="cx"> else: </span><span class="cx"> self.engine.drop(self) </span><span class="cx"> def tometadata(self, metadata, schema=None): </span><span class="lines">@@ -309,7 +309,11 @@ </span><span class="cx"> </span><span class="cx"> def __str__(self): </span><span class="cx"> if self.table is not None: </span><del>- return str(self.table) + "." + self.name </del><ins>+ tname = self.table.displayname + if tname is not None: + return tname + "." + self.name + else: + return self.name </ins><span class="cx"> else: </span><span class="cx"> return self.name </span><span class="cx"> </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemysqlpy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/sql.py (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/sql.py 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/lib/sqlalchemy/sql.py 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -656,6 +656,12 @@ </span><span class="cx"> """represents an element that can be used within the FROM clause of a SELECT statement.""" </span><span class="cx"> def __init__(self, from_name = None): </span><span class="cx"> self.from_name = self.name = from_name </span><ins>+ def _display_name(self): + if self.named_with_column(): + return self.name + else: + return None + displayname = property(_display_name) </ins><span class="cx"> def _get_from_objects(self): </span><span class="cx"> # this could also be [self], at the moment it doesnt matter to the Select object </span><span class="cx"> return [] </span><span class="lines">@@ -681,7 +687,7 @@ </span><span class="cx"> if not hasattr(self, '_oid_column'): </span><span class="cx"> self._oid_column = self._locate_oid_column() </span><span class="cx"> return self._oid_column </span><del>- def corresponding_column(self, column, raiseerr=True): </del><ins>+ def corresponding_column(self, column, raiseerr=True, keys_ok=False): </ins><span class="cx"> """given a ColumnElement, return the ColumnElement object from this </span><span class="cx"> Selectable which corresponds to that original Column via a proxy relationship.""" </span><span class="cx"> for c in column.orig_set: </span><span class="lines">@@ -690,10 +696,15 @@ </span><span class="cx"> except KeyError: </span><span class="cx"> pass </span><span class="cx"> else: </span><ins>+ if keys_ok: + try: + return self.c[column.key] + except KeyError: + pass </ins><span class="cx"> if not raiseerr: </span><span class="cx"> return None </span><span class="cx"> else: </span><del>- raise exceptions.InvalidRequestError("cant get orig for " + str(column) + " with table " + str(column.table.name) + " from table " + str(self.name)) </del><ins>+ raise exceptions.InvalidRequestError("Given column '%s', attached to table '%s', failed to locate a corresponding column from table '%s'" % (str(column), str(column.table), self.name)) </ins><span class="cx"> </span><span class="cx"> def _get_exported_attribute(self, name): </span><span class="cx"> try: </span></span></pre></div> <a id="sqlalchemybranchesschematestalltestspy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/test/alltests.py (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/test/alltests.py 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/test/alltests.py 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -52,6 +52,7 @@ </span><span class="cx"> 'manytomany', </span><span class="cx"> 'onetoone', </span><span class="cx"> 'inheritance', </span><ins>+ 'polymorph', </ins><span class="cx"> </span><span class="cx"> # extensions </span><span class="cx"> 'proxy_engine', </span></span></pre></div> <a id="sqlalchemybranchesschematestpolymorphpy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/test/polymorph.py (1472 => 1473)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/test/polymorph.py 2006-05-18 14:16:56 UTC (rev 1472) +++ sqlalchemy/branches/schema/test/polymorph.py 2006-05-19 00:13:01 UTC (rev 1473) </span><span class="lines">@@ -1,5 +1,169 @@ </span><del>-# test basic polymorphic relationship with a single tasble </del><ins>+import testbase +from sqlalchemy import * +import sets </ins><span class="cx"> </span><del>-# test polymorphic relationship with multiple tables </del><ins>+# test classes +class Person(object): + def __init__(self, **kwargs): + for key, value in kwargs.iteritems(): + setattr(self, key, value) + def get_name(self): + try: + return getattr(self, 'person_name') + except AttributeError: + return getattr(self, 'name') + def __repr__(self): + return "Ordinary person %s" % self.get_name() +class Engineer(Person): + def __repr__(self): + return "Engineer %s, status %s, engineer_name %s, primary_language %s" % (self.get_name(), self.status, self.engineer_name, self.primary_language) +class Manager(Person): + def __repr__(self): + return "Manager %s, status %s, manager_name %s" % (self.get_name(), self.status, self.manager_name) +class Company(object): + def __init__(self, **kwargs): + for key, value in kwargs.iteritems(): + setattr(self, key, value) + def __repr__(self): + return "Company %s" % self.name </ins><span class="cx"> </span><del>-# test that loading an object A from its non-polymorphic mapper returns the same identity as from its polymorphic mapper </del><span class="cx">\ No newline at end of file </span><ins>+class MultipleTableTest(testbase.PersistTest): + def setUpAll(self, use_person_column=False): + global companies, people, engineers, managers, metadata + metadata = BoundMetaData(testbase.db) + + # a table to store companies + companies = Table('companies', metadata, + Column('company_id', Integer, primary_key=True), + Column('name', String(50))) + + # we will define an inheritance relationship between the table "people" and "engineers", + # and a second inheritance relationship between the table "people" and "managers" + people = Table('people', metadata, + Column('person_id', Integer, primary_key=True), + Column('company_id', Integer, ForeignKey('companies.company_id')), + Column('name', String(50)), + Column('type', String(30))) + + engineers = Table('engineers', metadata, + Column('person_id', Integer, ForeignKey('people.person_id'), primary_key=True), + Column('status', String(30)), + Column('engineer_name', String(50)), + Column('primary_language', String(50)), + ) + + managers = Table('managers', metadata, + Column('person_id', Integer, ForeignKey('people.person_id'), primary_key=True), + Column('status', String(30)), + Column('manager_name', String(50)) + ) + + metadata.create_all() + + def tearDownAll(self): + metadata.drop_all() + + def tearDown(self): + clear_mappers() + for t in metadata.table_iterator(reverse=True): + t.delete().execute() + + def test_f_f_f(self): + self.do_test(False, False, False) + def test_f_f_t(self): + self.do_test(False, False, True) + def test_f_t_f(self): + self.do_test(False, True, False) + def test_f_t_t(self): + self.do_test(False, True, True) + def test_t_f_f(self): + self.do_test(True, False, False) + def test_t_f_t(self): + self.do_test(True, False, True) + def test_t_t_f(self): + self.do_test(True, True, False) + def test_t_t_t(self): + self.do_test(True, True, True) + + + def do_test(self, include_base=False, lazy_relation=True, redefine_colprop=False): + """tests the polymorph.py example, with several options: + + include_base - whether or not to include the base 'person' type in the union. + lazy_relation - whether or not the Company relation to People is lazy or eager. + redefine_colprop - if we redefine the 'name' column to be 'people_name' on the base Person class + """ + # create a union that represents both types of joins. + if include_base: + person_join = polymorphic_union( + { + 'engineer':people.join(engineers), + 'manager':people.join(managers), + 'person':people.select(people.c.type=='person'), + }, None, 'pjoin') + else: + person_join = polymorphic_union( + { + 'engineer':people.join(engineers), + 'manager':people.join(managers), + }, None, 'pjoin') + + if redefine_colprop: + person_mapper = mapper(Person, people, select_table=person_join, polymorphic_on=person_join.c.type, polymorphic_identity='person', properties= {'person_name':people.c.name}) + else: + person_mapper = mapper(Person, people, select_table=person_join, polymorphic_on=person_join.c.type, polymorphic_identity='person') + + mapper(Engineer, engineers, inherits=person_mapper, polymorphic_identity='engineer') + mapper(Manager, managers, inherits=person_mapper, polymorphic_identity='manager') + + mapper(Company, companies, properties={ + 'employees': relation(Person, lazy=lazy_relation, private=True, backref='company') + }) + + if redefine_colprop: + person_attribute_name = 'person_name' + else: + person_attribute_name = 'name' + + session = create_session() + c = Company(name='company1') + c.employees.append(Manager(status='AAB', manager_name='manager1', **{person_attribute_name:'pointy haired boss'})) + c.employees.append(Engineer(status='BBA', engineer_name='engineer1', primary_language='java', **{person_attribute_name:'dilbert'})) + if include_base: + c.employees.append(Person(status='HHH', **{person_attribute_name:'joesmith'})) + c.employees.append(Engineer(status='CGG', engineer_name='engineer2', primary_language='python', **{person_attribute_name:'wally'})) + c.employees.append(Manager(status='ABA', manager_name='manager2', **{person_attribute_name:'jsmith'})) + session.save(c) + print session.new + session.flush() + session.clear() + + c = session.query(Company).get(1) + for e in c.employees: + print e, e._instance_key, e.company + if include_base: + assert sets.Set([e.get_name() for e in c.employees]) == sets.Set(['pointy haired boss', 'dilbert', 'joesmith', 'wally', 'jsmith']) + else: + assert sets.Set([e.get_name() for e in c.employees]) == sets.Set(['pointy haired boss', 'dilbert', 'wally', 'jsmith']) + print "\n" + + + dilbert = session.query(Person).selectfirst(person_join.c.name=='dilbert') + dilbert2 = session.query(Engineer).selectfirst(people.c.name=='dilbert') + assert dilbert is dilbert2 + + dilbert.engineer_name = 'hes dibert!' + + session.flush() + session.clear() + + c = session.query(Company).get(1) + for e in c.employees: + print e, e._instance_key + + session.delete(c) + session.flush() + +if __name__ == "__main__": + testbase.main() + </ins></span></pre> </div> </div> </body> </html> |