summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/properties.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm/properties.py')
-rw-r--r--lib/sqlalchemy/orm/properties.py190
1 files changed, 110 insertions, 80 deletions
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index cbfba91f3..5788c30f9 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -446,7 +446,7 @@ class RelationshipProperty(StrategizedProperty):
self.viewonly = viewonly
self.lazy = lazy
self.single_parent = single_parent
- self._foreign_keys = foreign_keys
+ self._user_defined_foreign_keys = foreign_keys
self.collection_class = collection_class
self.passive_deletes = passive_deletes
self.passive_updates = passive_updates
@@ -695,7 +695,7 @@ class RelationshipProperty(StrategizedProperty):
if isinstance(other, (NoneType, expression._Null)):
if self.property.direction == MANYTOONE:
return sql.or_(*[x != None for x in
- self.property._foreign_keys])
+ self.property._calculated_foreign_keys])
else:
return self._criterion_exists()
elif self.property.uselist:
@@ -912,7 +912,7 @@ class RelationshipProperty(StrategizedProperty):
'primaryjoin',
'secondaryjoin',
'secondary',
- '_foreign_keys',
+ '_user_defined_foreign_keys',
'remote_side',
):
if util.callable(getattr(self, attr)):
@@ -931,9 +931,9 @@ class RelationshipProperty(StrategizedProperty):
if self.order_by is not False and self.order_by is not None:
self.order_by = [expression._literal_as_column(x) for x in
util.to_list(self.order_by)]
- self._foreign_keys = \
+ self._user_defined_foreign_keys = \
util.column_set(expression._literal_as_column(x) for x in
- util.to_column_set(self._foreign_keys))
+ util.to_column_set(self._user_defined_foreign_keys))
self.remote_side = \
util.column_set(expression._literal_as_column(x) for x in
util.to_column_set(self.remote_side))
@@ -999,8 +999,8 @@ class RelationshipProperty(StrategizedProperty):
raise sa_exc.ArgumentError("Could not determine join "
"condition between parent/child tables on "
"relationship %s. Specify a 'primaryjoin' "
- "expression. If this is a many-to-many "
- "relationship, 'secondaryjoin' is needed as well."
+ "expression. If 'secondary' is present, "
+ "'secondaryjoin' is needed as well."
% self)
def _col_is_part_of_mappings(self, column):
@@ -1012,91 +1012,121 @@ class RelationshipProperty(StrategizedProperty):
self.target.c.contains_column(column) or \
self.secondary.c.contains_column(column) is not None
+ def _sync_pairs_from_join(self, join_condition, primary):
+ """Given a join condition, figure out what columns are foreign
+ and are part of a binary "equated" condition to their referecned
+ columns, and convert into a list of tuples of (primary col->foreign col).
+
+ Make several attempts to determine if cols are compared using
+ "=" or other comparators (in which case suggest viewonly),
+ columns are present but not part of the expected mappings, columns
+ don't have any :class:`ForeignKey` information on them, or
+ the ``foreign_keys`` attribute is being used incorrectly.
+
+ """
+ eq_pairs = criterion_as_pairs(join_condition,
+ consider_as_foreign_keys=self._user_defined_foreign_keys,
+ any_operator=self.viewonly)
+
+ eq_pairs = [(l, r) for (l, r) in eq_pairs
+ if self._col_is_part_of_mappings(l)
+ and self._col_is_part_of_mappings(r)
+ or self.viewonly and r in self._user_defined_foreign_keys]
+
+ if not eq_pairs and \
+ self.secondary is not None and \
+ not self._user_defined_foreign_keys:
+ fks = set(self.secondary.c)
+ eq_pairs = criterion_as_pairs(join_condition,
+ consider_as_foreign_keys=fks,
+ any_operator=self.viewonly)
+
+ eq_pairs = [(l, r) for (l, r) in eq_pairs
+ if self._col_is_part_of_mappings(l)
+ and self._col_is_part_of_mappings(r)
+ or self.viewonly and r in fks]
+ if eq_pairs:
+ util.warn("No ForeignKey objects were present "
+ "in secondary table '%s'. Assumed referenced "
+ "foreign key columns %s for join condition '%s' "
+ "on relationship %s" % (
+ self.secondary.description,
+ ", ".join(sorted(["'%s'" % col for col in fks])),
+ join_condition,
+ self
+ ))
+
+ if not eq_pairs:
+ if not self.viewonly and criterion_as_pairs(join_condition,
+ consider_as_foreign_keys=self._user_defined_foreign_keys,
+ any_operator=True):
+ raise sa_exc.ArgumentError("Could not locate any "
+ "equated, locally mapped column pairs for %s "
+ "condition '%s' on relationship %s. For more "
+ "relaxed rules on join conditions, the "
+ "relationship may be marked as viewonly=True."
+ % (
+ primary and 'primaryjoin' or 'secondaryjoin',
+ join_condition,
+ self
+ ))
+ else:
+ if self._user_defined_foreign_keys:
+ raise sa_exc.ArgumentError("Could not determine "
+ "relationship direction for %s condition "
+ "'%s', on relationship %s, using manual "
+ "'foreign_keys' setting. Do the columns "
+ "in 'foreign_keys' represent all, and "
+ "only, the 'foreign' columns in this join "
+ "condition? Does the %s Table already "
+ "have adequate ForeignKey and/or "
+ "ForeignKeyConstraint objects established "
+ "(in which case 'foreign_keys' is usually "
+ "unnecessary)?"
+ % (
+ primary and 'primaryjoin' or 'secondaryjoin',
+ join_condition,
+ self,
+ primary and 'mapped' or 'secondary'
+ ))
+ else:
+ raise sa_exc.ArgumentError("Could not determine "
+ "relationship direction for %s condition "
+ "'%s', on relationship %s. Ensure that the "
+ "referencing Column objects have a "
+ "ForeignKey present, or are otherwise part "
+ "of a ForeignKeyConstraint on their parent "
+ "Table."
+ % (
+ primary and 'primaryjoin' or 'secondaryjoin',
+ join_condition,
+ self
+ ))
+ return eq_pairs
+
def _determine_synchronize_pairs(self):
if self.local_remote_pairs:
- if not self._foreign_keys:
+ if not self._user_defined_foreign_keys:
raise sa_exc.ArgumentError('foreign_keys argument is '
'required with _local_remote_pairs argument')
self.synchronize_pairs = []
for l, r in self.local_remote_pairs:
- if r in self._foreign_keys:
+ if r in self._user_defined_foreign_keys:
self.synchronize_pairs.append((l, r))
- elif l in self._foreign_keys:
+ elif l in self._user_defined_foreign_keys:
self.synchronize_pairs.append((r, l))
else:
- eq_pairs = criterion_as_pairs(self.primaryjoin,
- consider_as_foreign_keys=self._foreign_keys,
- any_operator=self.viewonly)
- eq_pairs = [(l, r) for (l, r) in eq_pairs
- if self._col_is_part_of_mappings(l)
- and self._col_is_part_of_mappings(r)
- or self.viewonly and r in self._foreign_keys]
- if not eq_pairs:
- if not self.viewonly \
- and criterion_as_pairs(self.primaryjoin,
- consider_as_foreign_keys=self._foreign_keys,
- any_operator=True):
- raise sa_exc.ArgumentError("Could not locate any "
- "equated, locally mapped column pairs for "
- "primaryjoin condition '%s' on "
- "relationship %s. For more relaxed rules "
- "on join conditions, the relationship may "
- "be marked as viewonly=True."
- % (self.primaryjoin, self))
- else:
- if self._foreign_keys:
- raise sa_exc.ArgumentError("Could not determine"
- " relationship direction for "
- "primaryjoin condition '%s', on "
- "relationship %s. Do the columns in "
- "'foreign_keys' represent only the "
- "'foreign' columns in this join "
- "condition ?" % (self.primaryjoin,
- self))
- else:
- raise sa_exc.ArgumentError("Could not determine"
- " relationship direction for "
- "primaryjoin condition '%s', on "
- "relationship %s. Specify the "
- "'foreign_keys' argument to indicate "
- "which columns on the relationship are "
- "foreign." % (self.primaryjoin, self))
+ eq_pairs = self._sync_pairs_from_join(self.primaryjoin, True)
self.synchronize_pairs = eq_pairs
if self.secondaryjoin is not None:
- sq_pairs = criterion_as_pairs(self.secondaryjoin,
- consider_as_foreign_keys=self._foreign_keys,
- any_operator=self.viewonly)
- sq_pairs = [(l, r) for (l, r) in sq_pairs
- if self._col_is_part_of_mappings(l)
- and self._col_is_part_of_mappings(r) or r
- in self._foreign_keys]
- if not sq_pairs:
- if not self.viewonly \
- and criterion_as_pairs(self.secondaryjoin,
- consider_as_foreign_keys=self._foreign_keys,
- any_operator=True):
- raise sa_exc.ArgumentError("Could not locate any "
- "equated, locally mapped column pairs for "
- "secondaryjoin condition '%s' on "
- "relationship %s. For more relaxed rules "
- "on join conditions, the relationship may "
- "be marked as viewonly=True."
- % (self.secondaryjoin, self))
- else:
- raise sa_exc.ArgumentError("Could not determine "
- "relationship direction for secondaryjoin "
- "condition '%s', on relationship %s. "
- "Specify the foreign_keys argument to "
- "indicate which columns on the "
- "relationship are foreign."
- % (self.secondaryjoin, self))
+ sq_pairs = self._sync_pairs_from_join(self.secondaryjoin, False)
self.secondary_synchronize_pairs = sq_pairs
else:
self.secondary_synchronize_pairs = None
- self._foreign_keys = util.column_set(r for (l, r) in
+ self._calculated_foreign_keys = util.column_set(r for (l, r) in
self.synchronize_pairs)
if self.secondary_synchronize_pairs:
- self._foreign_keys.update(r for (l, r) in
+ self._calculated_foreign_keys.update(r for (l, r) in
self.secondary_synchronize_pairs)
def _determine_direction(self):
@@ -1114,7 +1144,7 @@ class RelationshipProperty(StrategizedProperty):
remote = self.remote_side
else:
remote = None
- if not remote or self._foreign_keys.difference(l for (l,
+ if not remote or self._calculated_foreign_keys.difference(l for (l,
r) in self.synchronize_pairs).intersection(remote):
self.direction = ONETOMANY
else:
@@ -1192,12 +1222,12 @@ class RelationshipProperty(StrategizedProperty):
eq_pairs += self.secondary_synchronize_pairs
else:
eq_pairs = criterion_as_pairs(self.primaryjoin,
- consider_as_foreign_keys=self._foreign_keys,
+ consider_as_foreign_keys=self._calculated_foreign_keys,
any_operator=True)
if self.secondaryjoin is not None:
eq_pairs += \
criterion_as_pairs(self.secondaryjoin,
- consider_as_foreign_keys=self._foreign_keys,
+ consider_as_foreign_keys=self._calculated_foreign_keys,
any_operator=True)
eq_pairs = [(l, r) for (l, r) in eq_pairs
if self._col_is_part_of_mappings(l)
@@ -1266,7 +1296,7 @@ class RelationshipProperty(StrategizedProperty):
"a non-secondary relationship."
)
foreign_keys = kwargs.pop('foreign_keys',
- self._foreign_keys)
+ self._user_defined_foreign_keys)
parent = self.parent.primary_mapper()
kwargs.setdefault('viewonly', self.viewonly)
kwargs.setdefault('post_update', self.post_update)