summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2007-01-28 23:33:53 +0000
committerMike Bayer <mike_mp@zzzcomputing.com>2007-01-28 23:33:53 +0000
commitbbc5e7c285a160f148eafa0ab442675fe88551ce (patch)
tree71f407e9de4c29ac427d01acc7730ac3dd714a80 /lib/sqlalchemy
parent7a1a746d7abaebb1e1712224a8ebbf037cc94435 (diff)
downloadsqlalchemy-bbc5e7c285a160f148eafa0ab442675fe88551ce.tar.gz
merged the polymorphic relationship refactoring branch in. i want to go further on that branch and introduce the foreign_keys argument, and further centralize the "intelligence" about the joins and selectables into PropertyLoader so that lazyloader/sync can be simplified, but the current branch goes pretty far.
- relations keep track of "polymorphic_primaryjoin", "polymorphic_secondaryjoin" which it derives from the plain primaryjoin/secondaryjoin. - lazy/eagerloaders work from those polymorphic join objects. - the join exported by PropertyLoader to Query/SelectResults is the polymorphic join, so that join_to/etc work properly. - Query builds itself against the base Mapper again, not the "polymorphic" mapper. uses the "polymorphic" version only as appropriate. this helps join_by/join_to/etc to work with polymorphic mappers. - Query will also adapt incoming WHERE criterion to the polymorphic mapper, i.e. the "people" table becomes the "person_join" automatically. - quoting has been modified since labels made out of non-case-sensitive columns could themselves require quoting..so case_sensitive defaults to True if not otherwise specified (used to be based on the identifier itself). - the test harness gets an ORMTest base class and a bunch of the ORM unit tests are using it now, decreases a lot of redundancy.
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/ext/selectresults.py12
-rw-r--r--lib/sqlalchemy/orm/mapper.py33
-rw-r--r--lib/sqlalchemy/orm/properties.py58
-rw-r--r--lib/sqlalchemy/orm/query.py42
-rw-r--r--lib/sqlalchemy/orm/strategies.py98
-rw-r--r--lib/sqlalchemy/schema.py14
-rw-r--r--lib/sqlalchemy/sql.py8
-rw-r--r--lib/sqlalchemy/sql_util.py32
8 files changed, 216 insertions, 81 deletions
diff --git a/lib/sqlalchemy/ext/selectresults.py b/lib/sqlalchemy/ext/selectresults.py
index c2ad40917..153b0c2b9 100644
--- a/lib/sqlalchemy/ext/selectresults.py
+++ b/lib/sqlalchemy/ext/selectresults.py
@@ -71,7 +71,10 @@ class SelectResults(object):
def select(self, clause):
return self.filter(clause)
-
+
+ def select_by(self, *args, **kwargs):
+ return self.filter(self._query._join_by(args, kwargs, start=self._joinpoint[1]))
+
def order_by(self, order_by):
"""apply an ORDER BY to the query."""
new = self.clone()
@@ -131,9 +134,12 @@ class SelectResults(object):
for key in keys:
prop = mapper.props[key]
if outerjoin:
- clause = clause.outerjoin(prop.mapper.mapped_table, prop.get_join())
+ clause = clause.outerjoin(prop.select_table, prop.get_join())
else:
- clause = clause.join(prop.mapper.mapped_table, prop.get_join())
+ clause = clause.join(prop.select_table, prop.get_join())
+ print "SELECT_TABLE", prop.select_table
+ print "JOIN", prop.get_join()
+ print "CLAUSE", str(clause), "DONE CLAUSE"
mapper = prop.mapper
return (clause, mapper)
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index e0ee36fac..d6967934c 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -500,8 +500,9 @@ class Mapper(object):
def _initialize_properties(self):
- """calls the init() method on all MapperProperties attached to this mapper. this will incur the
- compilation of related mappers."""
+ """calls the init() method on all MapperProperties attached to this mapper. this happens
+ after all mappers have completed compiling everything else up until this point, so that all
+ dependencies are fully available."""
self.__log("_initialize_properties() started")
l = [(key, prop) for key, prop in self.__props.iteritems()]
for key, prop in l:
@@ -514,7 +515,8 @@ class Mapper(object):
"""if the 'select_table' keyword argument was specified,
set up a second "surrogate mapper" that will be used for select operations.
the columns of select_table should encompass all the columns of the mapped_table either directly
- or through proxying relationships."""
+ or through proxying relationships. Currently, non-column properties are *not* copied. this implies
+ that a polymorphic mapper cant do any eager loading right now."""
if self.select_table is not self.mapped_table:
if self.polymorphic_identity is None:
raise exceptions.ArgumentError("Could not locate a polymorphic_identity field for mapper '%s'. This field is required for polymorphic mappers" % str(self))
@@ -531,7 +533,7 @@ class Mapper(object):
"""if this mapper is to be a primary mapper (i.e. the non_primary flag is not set),
associate this Mapper with the given class_ and entity name. subsequent
calls to class_mapper() for the class_/entity name combination will return this
- mapper. also decorates the __init__ method on the mapped class to include auto-session attachment logic."""
+ mapper. also decorates the __init__ method on the mapped class to include optional auto-session attachment logic."""
if self.non_primary:
return
@@ -626,7 +628,24 @@ class Mapper(object):
for x in iterate(mapper):
yield x
return iterate(self)
-
+
+ def _get_inherited_column_equivalents(self):
+ """return a map of all 'equivalent' columns, based on traversing the full set of inherit_conditions across
+ all inheriting mappers and determining column pairs that are equated to one another.
+
+ this is used when relating columns to those of a polymorphic selectable, as the selectable usually only contains
+ one of two columns that are equated to one another."""
+ result = {}
+ def visit_binary(binary):
+ if binary.operator == '=':
+ result[binary.left] = binary.right
+ result[binary.right] = binary.left
+ vis = mapperutil.BinaryVisitor(visit_binary)
+ for mapper in self.polymorphic_iterator():
+ if mapper.inherit_condition is not None:
+ mapper.inherit_condition.accept_visitor(vis)
+ return result
+
def add_properties(self, dict_of_properties):
"""adds the given dictionary of properties to this mapper, using add_property."""
for key, value in dict_of_properties.iteritems():
@@ -755,8 +774,8 @@ class Mapper(object):
def identity_key_from_row(self, row):
"""return an identity-map key for use in storing/retrieving an item from the identity map.
- row - a sqlalchemy.dbengine.RowProxy instance or other map corresponding result-set
- column names to their values within a row.
+ row - a sqlalchemy.engine.base.RowProxy instance or a dictionary corresponding result-set
+ ColumnElement instances to their values within a row.
"""
return (self.class_, tuple([row[column] for column in self.pks_by_table[self.mapped_table]]), self.entity_name)
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index bc74fcafd..42c017bc5 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -201,6 +201,9 @@ class PropertyLoader(StrategizedProperty):
self.association = mapper.class_mapper(self.association, compile=False)._check_compile()
self.target = self.mapper.mapped_table
+ self.select_mapper = self.mapper.get_select_mapper()
+ self.select_table = self.mapper.select_table
+ self.loads_polymorphic = self.target is not self.select_table
if self.cascade.delete_orphan:
if self.parent.class_ is self.mapper.class_:
@@ -226,7 +229,7 @@ class PropertyLoader(StrategizedProperty):
# as the loader strategies expect to be working with those now (they will adapt the join conditions
# to the "polymorphic" selectable as needed). since this is an API change, put an explicit check/
# error message in case its the "old" way.
- if self.mapper.select_table is not self.mapper.mapped_table:
+ if self.loads_polymorphic:
vis = sql_util.ColumnsInClause(self.mapper.select_table)
self.primaryjoin.accept_visitor(vis)
if self.secondaryjoin:
@@ -234,6 +237,7 @@ class PropertyLoader(StrategizedProperty):
if vis.result:
raise exceptions.ArgumentError("In relationship '%s' between mappers '%s' and '%s', primary and secondary join conditions must not include columns from the polymorphic 'select_table' argument as of SA release 0.3.4. Construct join conditions using the base tables of the related mappers." % (self.key, self.parent, self.mapper))
+
# if the foreign key wasnt specified and theres no assocaition table, try to figure
# out who is dependent on who. we dont need all the foreign keys represented in the join,
# just one of them.
@@ -247,6 +251,52 @@ class PropertyLoader(StrategizedProperty):
if self.direction is None:
self.direction = self._get_direction()
+ #print "DIRECTION IS ", self.direction, sync.ONETOMANY, sync.MANYTOONE
+ #print "FKEY IS", self.foreignkey
+
+ # get ready to create "polymorphic" primary/secondary join clauses.
+ # these clauses represent the same join between parent/child tables that the primary
+ # and secondary join clauses represent, except they reference ColumnElements that are specifically
+ # in the "polymorphic" selectables. these are used to construct joins for both Query as well as
+ # eager loading, and also are used to calculate "lazy loading" clauses.
+
+ # as we will be using the polymorphic selectables (i.e. select_table argument to Mapper) to figure this out,
+ # first create maps of all the "equivalent" columns, since polymorphic selectables will often munge
+ # several "equivalent" columns (such as parent/child fk cols) into just one column.
+ parent_equivalents = self.parent._get_inherited_column_equivalents()
+ target_equivalents = self.mapper._get_inherited_column_equivalents()
+
+ # if the target mapper loads polymorphically, adapt the clauses to the target's selectable
+ if self.loads_polymorphic:
+ if self.secondaryjoin:
+ self.polymorphic_secondaryjoin = self.secondaryjoin.copy_container()
+ self.polymorphic_secondaryjoin.accept_visitor(sql_util.ClauseAdapter(self.mapper.select_table))
+ self.polymorphic_primaryjoin = self.primaryjoin.copy_container()
+ else:
+ self.polymorphic_primaryjoin = self.primaryjoin.copy_container()
+ if self.direction is sync.ONETOMANY:
+ self.polymorphic_primaryjoin.accept_visitor(sql_util.ClauseAdapter(self.mapper.select_table, include=self.foreignkey, equivalents=target_equivalents))
+ elif self.direction is sync.MANYTOONE:
+ self.polymorphic_primaryjoin.accept_visitor(sql_util.ClauseAdapter(self.mapper.select_table, exclude=self.foreignkey, equivalents=target_equivalents))
+
+ self.polymorphic_secondaryjoin = None
+ else:
+ self.polymorphic_primaryjoin = self.primaryjoin.copy_container()
+ self.polymorphic_secondaryjoin = self.secondaryjoin and self.secondaryjoin.copy_container() or None
+
+ # if the parent mapper loads polymorphically, adapt the clauses to the parent's selectable
+ if self.parent.select_table is not self.parent.mapped_table:
+ if self.direction is sync.ONETOMANY:
+ self.polymorphic_primaryjoin.accept_visitor(sql_util.ClauseAdapter(self.parent.select_table, exclude=self.foreignkey, equivalents=parent_equivalents))
+ elif self.direction is sync.MANYTOONE:
+ self.polymorphic_primaryjoin.accept_visitor(sql_util.ClauseAdapter(self.parent.select_table, include=self.foreignkey, equivalents=parent_equivalents))
+ elif self.secondaryjoin:
+ self.polymorphic_primaryjoin.accept_visitor(sql_util.ClauseAdapter(self.parent.select_table, exclude=self.foreignkey, equivalents=parent_equivalents))
+
+ #print "KEY", self.key, "PARENT", str(self.parent)
+ #print "KEY", self.key, "REG PRIMARY JOIN", str(self.primaryjoin)
+ #print "KEY", self.key, "POLY PRIMARY JOIN", str(self.polymorphic_primaryjoin)
+
if self.uselist is None and self.direction == sync.MANYTOONE:
self.uselist = False
@@ -326,10 +376,10 @@ class PropertyLoader(StrategizedProperty):
self.foreignkey = foreignkeys
def get_join(self):
- if self.secondaryjoin is not None:
- return self.primaryjoin & self.secondaryjoin
+ if self.polymorphic_secondaryjoin is not None:
+ return self.polymorphic_primaryjoin & self.polymorphic_secondaryjoin
else:
- return self.primaryjoin
+ return self.polymorphic_primaryjoin
def register_dependencies(self, uowcommit):
if not self.viewonly:
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index ce2f78bb7..e0294ef72 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -5,7 +5,7 @@
# the MIT License: http://www.opensource.org/licenses/mit-license.php
from sqlalchemy import sql, util, exceptions, sql_util, logging
-from sqlalchemy.orm import mapper
+from sqlalchemy.orm import mapper, class_mapper
from sqlalchemy.orm.interfaces import OperationContext
__all__ = ['Query', 'QueryContext', 'SelectionContext']
@@ -18,7 +18,7 @@ class Query(object):
else:
self.mapper = class_or_mapper.compile()
self.with_options = with_options or []
- self.mapper = self.mapper.get_select_mapper().compile()
+ self.select_mapper = self.mapper.get_select_mapper().compile()
self.always_refresh = kwargs.pop('always_refresh', self.mapper.always_refresh)
self.order_by = kwargs.pop('order_by', self.mapper.order_by)
self.lockmode = lockmode
@@ -26,10 +26,11 @@ class Query(object):
if extension is not None:
self.extension.append(extension)
self.extension.append(self.mapper.extension)
+ self.is_polymorphic = self.mapper is not self.select_mapper
self._session = session
if not hasattr(self.mapper, '_get_clause'):
_get_clause = sql.and_()
- for primary_key in self.mapper.pks_by_table[self.table]:
+ for primary_key in self.primary_key_columns:
_get_clause.clauses.append(primary_key == sql.bindparam(primary_key._label, type=primary_key.type))
self.mapper._get_clause = _get_clause
self._get_clause = self.mapper._get_clause
@@ -44,7 +45,8 @@ class Query(object):
return self.mapper.get_session()
else:
return self._session
- table = property(lambda s:s.mapper.select_table)
+ table = property(lambda s:s.select_mapper.mapped_table)
+ primary_key_columns = property(lambda s:s.select_mapper.pks_by_table[s.select_mapper.mapped_table])
session = property(_get_session)
def get(self, ident, **kwargs):
@@ -117,6 +119,10 @@ class Query(object):
def join_by(self, *args, **params):
"""return a ClauseElement representing the WHERE clause that would normally be sent to select_whereclause() by select_by()."""
+ return self._join_by(args, params)
+
+ def _join_by(self, args, params, start=None):
+ """return a ClauseElement representing the WHERE clause that would normally be sent to select_whereclause() by select_by()."""
clause = None
for arg in args:
if clause is None:
@@ -125,7 +131,7 @@ class Query(object):
clause &= arg
for key, value in params.iteritems():
- (keys, prop) = self._locate_prop(key)
+ (keys, prop) = self._locate_prop(key, start=start)
c = prop.compare(value) & self.join_via(keys)
if clause is None:
clause = c
@@ -265,7 +271,7 @@ class Query(object):
if self._nestable(**kwargs):
s = sql.select([self.table], whereclause, **kwargs).alias('getcount').count()
else:
- primary_key = self.mapper.pks_by_table[self.table]
+ primary_key = self.primary_key_columns
s = sql.select([sql.func.count(list(primary_key)[0])], whereclause, from_obj=from_obj, **kwargs)
return self.session.scalar(self.mapper, s, params=params)
@@ -317,7 +323,7 @@ class Query(object):
session = self.session
- context = SelectionContext(self.mapper, session, with_options=self.with_options, **kwargs)
+ context = SelectionContext(self.select_mapper, session, with_options=self.with_options, **kwargs)
result = util.UniqueAppender([])
if mappers:
@@ -326,7 +332,7 @@ class Query(object):
otherresults.append(util.UniqueAppender([]))
for row in cursor.fetchall():
- self.mapper._instance(context, row, result)
+ self.select_mapper._instance(context, row, result)
i = 0
for m in mappers:
m._instance(context, row, otherresults[i])
@@ -356,7 +362,7 @@ class Query(object):
ident = util.to_list(ident)
i = 0
params = {}
- for primary_key in self.mapper.pks_by_table[self.table]:
+ for primary_key in self.primary_key_columns:
params[primary_key._label] = ident[i]
# if there are not enough elements in the given identifier, then
# use the previous identifier repeatedly. this is a workaround for the issue
@@ -392,6 +398,16 @@ class Query(object):
def compile(self, whereclause = None, **kwargs):
"""given a WHERE criterion, produce a ClauseElement-based statement suitable for usage in the execute() method."""
+
+ if whereclause is not None and self.is_polymorphic:
+ # adapt the given WHERECLAUSE to adjust instances of this query's mapped table to be that of our select_table,
+ # which may be the "polymorphic" selectable used by our mapper.
+ print "PolYMORPHIC YES"
+ print "WHERECLAUSE", str(whereclause)
+ print "OUR TABLE", str(self.table)
+ whereclause.accept_visitor(sql_util.ClauseAdapter(self.table))
+ print "AND NOW ITS", str(whereclause)
+
context = kwargs.pop('query_context', None)
if context is None:
context = QueryContext(self, kwargs)
@@ -412,8 +428,8 @@ class Query(object):
except KeyError:
raise exceptions.ArgumentError("Unknown lockmode '%s'" % lockmode)
- if self.mapper.single and self.mapper.polymorphic_on is not None and self.mapper.polymorphic_identity is not None:
- whereclause = sql.and_(whereclause, self.mapper.polymorphic_on.in_(*[m.polymorphic_identity for m in self.mapper.polymorphic_iterator()]))
+ if self.select_mapper.single and self.select_mapper.polymorphic_on is not None and self.select_mapper.polymorphic_identity is not None:
+ whereclause = sql.and_(whereclause, self.select_mapper.polymorphic_on.in_(*[m.polymorphic_identity for m in self.select_mapper.polymorphic_iterator()]))
alltables = []
for l in [sql_util.TableFinder(x) for x in from_obj]:
@@ -460,7 +476,9 @@ class Query(object):
context.statement = statement
# give all the attached properties a chance to modify the query
- for value in self.mapper.props.values():
+ # TODO: doing this off the select_mapper. if its the polymorphic mapper, then
+ # it has no relations() on it. should we compile those too into the query ? (i.e. eagerloads)
+ for value in self.select_mapper.props.values():
value.setup(context)
return statement
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 863f2e65d..d8d9a9c47 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -125,20 +125,8 @@ class DeferredOption(StrategizedOption):
class AbstractRelationLoader(LoaderStrategy):
def init(self):
super(AbstractRelationLoader, self).init()
- self.primaryjoin = self.parent_property.primaryjoin
- self.secondaryjoin = self.parent_property.secondaryjoin
- self.secondary = self.parent_property.secondary
- self.foreignkey = self.parent_property.foreignkey
- self.mapper = self.parent_property.mapper
- self.select_mapper = self.mapper.get_select_mapper()
- self.target = self.parent_property.target
- self.select_table = self.parent_property.mapper.select_table
- self.loads_polymorphic = self.target is not self.select_table
- self.uselist = self.parent_property.uselist
- self.cascade = self.parent_property.cascade
- self.attributeext = self.parent_property.attributeext
- self.order_by = self.parent_property.order_by
- self.remote_side = self.parent_property.remote_side
+ for attr in ['primaryjoin', 'secondaryjoin', 'secondary', 'foreignkey', 'mapper', 'select_mapper', 'target', 'select_table', 'loads_polymorphic', 'uselist', 'cascade', 'attributeext', 'order_by', 'remote_side', 'polymorphic_primaryjoin', 'polymorphic_secondaryjoin', 'direction']:
+ setattr(self, attr, getattr(self.parent_property, attr))
self._should_log_debug = logging.is_debug_enabled(self.logger)
def _init_instance_attribute(self, instance, callable_=None):
@@ -163,7 +151,14 @@ NoLoader.logger = logging.class_logger(NoLoader)
class LazyLoader(AbstractRelationLoader):
def init(self):
super(LazyLoader, self).init()
- (self.lazywhere, self.lazybinds, self.lazyreverse) = self._create_lazy_clause(self.parent.unjoined_table, self.primaryjoin, self.secondaryjoin, self.foreignkey, self.remote_side, self.mapper.select_table)
+ (self.lazywhere, self.lazybinds, self.lazyreverse) = self._create_lazy_clause(
+ self.parent.select_table,
+ self.mapper.select_table,
+ self.polymorphic_primaryjoin,
+ self.polymorphic_secondaryjoin,
+ self.foreignkey,
+ self.remote_side)
+
# determine if our "lazywhere" clause is the same as the mapper's
# get() clause. then we can just use mapper.get()
self.use_get = not self.uselist and query.Query(self.mapper)._get_clause.compare(self.lazywhere)
@@ -210,7 +205,7 @@ class LazyLoader(AbstractRelationLoader):
# to possibly save a DB round trip
if self.use_get:
ident = []
- for primary_key in self.mapper.pks_by_table[self.mapper.mapped_table]:
+ for primary_key in self.select_mapper.pks_by_table[self.select_mapper.mapped_table]:
bind = self.lazyreverse[primary_key]
ident.append(params[bind.key])
return session.query(self.mapper).get(ident)
@@ -247,11 +242,49 @@ class LazyLoader(AbstractRelationLoader):
# to load data into it.
sessionlib.attribute_manager.reset_instance_attribute(instance, self.key)
- def _create_lazy_clause(self, table, primaryjoin, secondaryjoin, foreignkey, remote_side, select_table):
+ def _create_lazy_clause(self, parenttable, targettable, primaryjoin, secondaryjoin, foreignkey, remote_side):
binds = {}
reverse = {}
- def column_in_table(table, column):
- return table.corresponding_column(column, raiseerr=False, keys_ok=False) is not None
+
+ #print "PARENTTABLE", parenttable, "TARGETTABLE", targettable
+
+ def should_bind(targetcol, othercol):
+ # determine if the given target column is part of the parent table
+ # portion of the join condition, in which case it gets converted
+ # to a bind param.
+
+ # contains_column will return if this column is exactly in the table, with no
+ # proxying relationships. the table can be either the column's actual parent table,
+ # or a Join object containing the table. for a Select, Alias, or Union, the column
+ # needs to be the actual ColumnElement exported by that selectable, not the "originating" column.
+ inparent = parenttable.c.contains_column(targetcol)
+
+ # check if its also in the target table. if this is a many-to-many relationship,
+ # then we dont care about target table presence
+ intarget = secondaryjoin is None and targettable.c.contains_column(targetcol)
+
+ if inparent and not intarget:
+ # its in the parent and not the target, return true.
+ return True
+ elif inparent and intarget:
+ # its in both. hmm.
+ if parenttable is not targettable:
+ # the column is in both tables, but the two tables are different.
+ # this corresponds to a table relating to a Join which also contains that table.
+ # such as tableA.c.col1 == tableB.c.col2, tables are tableA and tableA.join(tableB)
+ # in which case we only accept that the parenttable is the "base" table, not the "joined" table
+ return targetcol.table is parenttable
+ else:
+ # parent/target are the same table, i.e. circular reference.
+ # we have to rely on the "remote_side" argument
+ # and/or foreignkey collection.
+ # technically we can use this for the non-circular refs as well except that "remote_side" is usually
+ # only calculated for self-referential relationships at the moment.
+ # TODO: have PropertyLoader calculate remote_side completely ? this would involve moving most of the
+ # "should_bind()" logic to PropertyLoader. remote_side could also then be accurately used by sync.py.
+ if col_in_collection(othercol, remote_side):
+ return True
+ return False
if remote_side is None or len(remote_side) == 0:
remote_side = foreignkey
@@ -280,14 +313,15 @@ class LazyLoader(AbstractRelationLoader):
rightcol = find_column_in_expr(binary.right)
if leftcol is None or rightcol is None:
return
- circular = leftcol.table is rightcol.table
- if ((not circular and column_in_table(table, leftcol)) or (circular and col_in_collection(rightcol, remote_side))):
+ if should_bind(leftcol, rightcol):
col = leftcol
binary.left = binds.setdefault(leftcol,
sql.bindparam(bind_label(), None, shortname=leftcol.name, type=binary.right.type))
reverse[rightcol] = binds[col]
- if (leftcol is not rightcol) and ((not circular and column_in_table(table, rightcol)) or (circular and col_in_collection(leftcol, remote_side))):
+ # the "left is not right" compare is to handle part of a join clause that is "table.c.col1==table.c.col1",
+ # which can happen in rare cases
+ if leftcol is not rightcol and should_bind(rightcol, leftcol):
col = rightcol
binary.right = binds.setdefault(rightcol,
sql.bindparam(bind_label(), None, shortname=rightcol.name, type=binary.left.type))
@@ -299,13 +333,9 @@ class LazyLoader(AbstractRelationLoader):
if secondaryjoin is not None:
secondaryjoin = secondaryjoin.copy_container()
- if self.loads_polymorphic:
- secondaryjoin.accept_visitor(sql_util.ClauseAdapter(select_table))
lazywhere = sql.and_(lazywhere, secondaryjoin)
- else:
- if self.loads_polymorphic:
- lazywhere.accept_visitor(sql_util.ClauseAdapter(select_table))
-
+
+ #print "LAZYCLAUSE", str(lazywhere)
LazyLoader.logger.info("create_lazy_clause " + str(lazywhere))
return (lazywhere, binds, reverse)
@@ -317,7 +347,7 @@ class EagerLoader(AbstractRelationLoader):
"""loads related objects inline with a parent query."""
def init(self):
super(EagerLoader, self).init()
- if self.parent.isa(self.select_mapper):
+ if self.parent.isa(self.mapper):
raise exceptions.ArgumentError("Error creating eager relationship '%s' on parent class '%s' to child class '%s': Cant use eager loading on a self referential relationship." % (self.key, repr(self.parent.class_), repr(self.mapper.class_)))
self.parent._eager_loaders.add(self.parent_property)
@@ -364,16 +394,12 @@ class EagerLoader(AbstractRelationLoader):
eagerloader.target:self.eagertarget,
eagerloader.secondary:self.eagersecondary
})
- self.eagersecondaryjoin = eagerloader.secondaryjoin.copy_container()
- if eagerloader.loads_polymorphic:
- self.eagersecondaryjoin.accept_visitor(sql_util.ClauseAdapter(eagerloader.select_table))
+ self.eagersecondaryjoin = eagerloader.polymorphic_secondaryjoin.copy_container()
self.eagersecondaryjoin.accept_visitor(self.aliasizer)
- self.eagerprimary = eagerloader.primaryjoin.copy_container()
+ self.eagerprimary = eagerloader.polymorphic_primaryjoin.copy_container()
self.eagerprimary.accept_visitor(self.aliasizer)
else:
- self.eagerprimary = eagerloader.primaryjoin.copy_container()
- if eagerloader.loads_polymorphic:
- self.eagerprimary.accept_visitor(sql_util.ClauseAdapter(eagerloader.select_table))
+ self.eagerprimary = eagerloader.polymorphic_primaryjoin.copy_container()
self.aliasizer = sql_util.Aliasizer(self.target, aliases={self.target:self.eagertarget})
self.eagerprimary.accept_visitor(self.aliasizer)
diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py
index 8b36ba026..2be808ba6 100644
--- a/lib/sqlalchemy/schema.py
+++ b/lib/sqlalchemy/schema.py
@@ -65,8 +65,7 @@ class SchemaItem(object):
a local non-None value overrides all others. after that, the parent item
(i.e. Column for a Sequence, Table for a Column, MetaData for a Table) is
searched for a non-None setting, traversing each parent until none are found.
- finally, case_sensitive is set to True if and only if the name of this item
- is not all lowercase.
+ finally, case_sensitive is set to True as a default.
"""
local = getattr(self, '_%s_setting' % keyname, None)
if local is not None:
@@ -78,7 +77,7 @@ class SchemaItem(object):
parentval = getattr(parent, '_case_sensitive_setting', None)
if parentval is not None:
return parentval
- return name is not None and name.lower() != name
+ return True
def _get_case_sensitive(self):
try:
return self.__case_sensitive
@@ -194,11 +193,9 @@ class Table(SchemaItem, sql.TableClause):
quote_schema=False : indicates that the Namespace identifier must be properly escaped and quoted before being sent
to the database. This flag overrides all other quoting behavior.
- case_sensitive=True : indicates that the identifier should be interpreted by the database in the natural case for identifiers.
- Mixed case is not sufficient to cause this identifier to be quoted; it must contain an illegal character.
+ case_sensitive=True : indicates quoting should be used if the identifier needs it.
- case_sensitive_schema=True : indicates that the identifier should be interpreted by the database in the natural case for identifiers.
- Mixed case is not sufficient to cause this identifier to be quoted; it must contain an illegal character.
+ case_sensitive_schema=True : indicates quoting should be used if the identifier needs it.
"""
super(Table, self).__init__(name)
self._metadata = metadata
@@ -365,8 +362,7 @@ class Column(SchemaItem, sql._ColumnClause):
to the database. This flag should normally not be required as dialects can auto-detect conditions where quoting
is required.
- case_sensitive=True : indicates that the identifier should be interpreted by the database in the natural case for identifiers.
- Mixed case is not sufficient to cause this identifier to be quoted; it must contain an illegal character.
+ case_sensitive=True : indicates quoting should be used if the identifier needs it.
"""
name = str(name) # in case of incoming unicode
super(Column, self).__init__(name, None, type)
diff --git a/lib/sqlalchemy/sql.py b/lib/sqlalchemy/sql.py
index 559847261..5f392a61c 100644
--- a/lib/sqlalchemy/sql.py
+++ b/lib/sqlalchemy/sql.py
@@ -694,7 +694,12 @@ class ColumnCollection(util.OrderedProperties):
if c.shares_lineage(local):
l.append(c==local)
return and_(*l)
-
+ def contains_column(self, col):
+ # have to use a Set here, because it will compare the identity
+ # of the column, not just using "==" for comparison which will always return a
+ # "True" value (i.e. a BinaryClause...)
+ return col in util.Set(self)
+
class FromClause(Selectable):
"""represents an element that can be used within the FROM clause of a SELECT statement."""
def __init__(self, name=None):
@@ -1400,6 +1405,7 @@ class CompoundSelect(_SelectBaseMixin, FromClause):
for c in s.c:
yield c
def _proxy_column(self, column):
+ print "PROXYING COLUMN", type(column), column
if self.use_labels:
col = column._make_proxy(self, name=column._label)
else:
diff --git a/lib/sqlalchemy/sql_util.py b/lib/sqlalchemy/sql_util.py
index 6b87a2dec..10d4495d9 100644
--- a/lib/sqlalchemy/sql_util.py
+++ b/lib/sqlalchemy/sql_util.py
@@ -135,17 +135,31 @@ class ClauseAdapter(sql.ClauseVisitor):
s.c.col1 == table2.c.col1
"""
- def __init__(self, selectable):
+ def __init__(self, selectable, include=None, exclude=None, equivalents=None):
self.selectable = selectable
+ self.include = include
+ self.exclude = exclude
+ self.equivalents = equivalents
+ def include_col(self, col):
+ if not isinstance(col, sql.ColumnElement):
+ return None
+ if self.include is not None:
+ if col not in self.include:
+ return None
+ if self.exclude is not None:
+ if col in self.exclude:
+ return None
+ newcol = self.selectable.corresponding_column(col, raiseerr=False, keys_ok=False)
+ if newcol is None and self.equivalents is not None and col in self.equivalents:
+ newcol = self.selectable.corresponding_column(self.equivalents[col], raiseerr=False, keys_ok=False)
+ return newcol
def visit_binary(self, binary):
- if isinstance(binary.left, sql.ColumnElement):
- col = self.selectable.corresponding_column(binary.left, raiseerr=False, keys_ok=True)
- if col is not None:
- binary.left = col
- if isinstance(binary.right, sql.ColumnElement):
- col = self.selectable.corresponding_column(binary.right, raiseerr=False, keys_ok=True)
- if col is not None:
- binary.right = col
+ col = self.include_col(binary.left)
+ if col is not None:
+ binary.left = col
+ col = self.include_col(binary.right)
+ if col is not None:
+ binary.right = col
class ColumnsInClause(sql.ClauseVisitor):
"""given a selectable, visits clauses and determines if any columns from the clause are in the selectable"""