| 1 | # properties.py |
|---|
| 2 | # Copyright (C) 2005, 2006, 2007, 2008, 2009 Michael Bayer mike_mp@zzzcomputing.com |
|---|
| 3 | # |
|---|
| 4 | # This module is part of SQLAlchemy and is released under |
|---|
| 5 | # the MIT License: http://www.opensource.org/licenses/mit-license.php |
|---|
| 6 | |
|---|
| 7 | """MapperProperty implementations. |
|---|
| 8 | |
|---|
| 9 | This is a private module which defines the behavior of invidual ORM-mapped |
|---|
| 10 | attributes. |
|---|
| 11 | |
|---|
| 12 | """ |
|---|
| 13 | |
|---|
| 14 | from sqlalchemy import sql, util, log |
|---|
| 15 | import sqlalchemy.exceptions as sa_exc |
|---|
| 16 | from sqlalchemy.sql.util import ClauseAdapter, criterion_as_pairs, join_condition |
|---|
| 17 | from sqlalchemy.sql import operators, expression |
|---|
| 18 | from sqlalchemy.orm import ( |
|---|
| 19 | attributes, dependency, mapper, object_mapper, strategies, |
|---|
| 20 | ) |
|---|
| 21 | from sqlalchemy.orm.util import CascadeOptions, _class_to_mapper, _orm_annotate, _orm_deannotate |
|---|
| 22 | from sqlalchemy.orm.interfaces import ( |
|---|
| 23 | MANYTOMANY, MANYTOONE, MapperProperty, ONETOMANY, PropComparator, |
|---|
| 24 | StrategizedProperty, |
|---|
| 25 | ) |
|---|
| 26 | |
|---|
| 27 | __all__ = ('ColumnProperty', 'CompositeProperty', 'SynonymProperty', |
|---|
| 28 | 'ComparableProperty', 'RelationProperty', 'BackRef') |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | class ColumnProperty(StrategizedProperty): |
|---|
| 32 | """Describes an object attribute that corresponds to a table column.""" |
|---|
| 33 | |
|---|
| 34 | def __init__(self, *columns, **kwargs): |
|---|
| 35 | """Construct a ColumnProperty. |
|---|
| 36 | |
|---|
| 37 | :param \*columns: The list of `columns` describes a single |
|---|
| 38 | object property. If there are multiple tables joined |
|---|
| 39 | together for the mapper, this list represents the equivalent |
|---|
| 40 | column as it appears across each table. |
|---|
| 41 | |
|---|
| 42 | :param group: |
|---|
| 43 | |
|---|
| 44 | :param deferred: |
|---|
| 45 | |
|---|
| 46 | :param comparator_factory: |
|---|
| 47 | |
|---|
| 48 | :param descriptor: |
|---|
| 49 | |
|---|
| 50 | :param extension: |
|---|
| 51 | |
|---|
| 52 | """ |
|---|
| 53 | self.columns = [expression._labeled(c) for c in columns] |
|---|
| 54 | self.group = kwargs.pop('group', None) |
|---|
| 55 | self.deferred = kwargs.pop('deferred', False) |
|---|
| 56 | self.no_instrument = kwargs.pop('_no_instrument', False) |
|---|
| 57 | self.comparator_factory = kwargs.pop('comparator_factory', self.__class__.Comparator) |
|---|
| 58 | self.descriptor = kwargs.pop('descriptor', None) |
|---|
| 59 | self.extension = kwargs.pop('extension', None) |
|---|
| 60 | if kwargs: |
|---|
| 61 | raise TypeError( |
|---|
| 62 | "%s received unexpected keyword argument(s): %s" % ( |
|---|
| 63 | self.__class__.__name__, ', '.join(sorted(kwargs.keys())))) |
|---|
| 64 | |
|---|
| 65 | util.set_creation_order(self) |
|---|
| 66 | if self.no_instrument: |
|---|
| 67 | self.strategy_class = strategies.UninstrumentedColumnLoader |
|---|
| 68 | elif self.deferred: |
|---|
| 69 | self.strategy_class = strategies.DeferredColumnLoader |
|---|
| 70 | else: |
|---|
| 71 | self.strategy_class = strategies.ColumnLoader |
|---|
| 72 | |
|---|
| 73 | def instrument_class(self, mapper): |
|---|
| 74 | if self.no_instrument: |
|---|
| 75 | return |
|---|
| 76 | |
|---|
| 77 | attributes.register_descriptor( |
|---|
| 78 | mapper.class_, |
|---|
| 79 | self.key, |
|---|
| 80 | comparator=self.comparator_factory(self, mapper), |
|---|
| 81 | parententity=mapper, |
|---|
| 82 | property_=self |
|---|
| 83 | ) |
|---|
| 84 | |
|---|
| 85 | def do_init(self): |
|---|
| 86 | super(ColumnProperty, self).do_init() |
|---|
| 87 | if len(self.columns) > 1 and self.parent.primary_key.issuperset(self.columns): |
|---|
| 88 | util.warn( |
|---|
| 89 | ("On mapper %s, primary key column '%s' is being combined " |
|---|
| 90 | "with distinct primary key column '%s' in attribute '%s'. " |
|---|
| 91 | "Use explicit properties to give each column its own mapped " |
|---|
| 92 | "attribute name.") % (self.parent, self.columns[1], |
|---|
| 93 | self.columns[0], self.key)) |
|---|
| 94 | |
|---|
| 95 | def copy(self): |
|---|
| 96 | return ColumnProperty(deferred=self.deferred, group=self.group, *self.columns) |
|---|
| 97 | |
|---|
| 98 | def getattr(self, state, column): |
|---|
| 99 | return state.get_impl(self.key).get(state, state.dict) |
|---|
| 100 | |
|---|
| 101 | def getcommitted(self, state, column, passive=False): |
|---|
| 102 | return state.get_impl(self.key).get_committed_value(state, state.dict, passive=passive) |
|---|
| 103 | |
|---|
| 104 | def setattr(self, state, value, column): |
|---|
| 105 | state.get_impl(self.key).set(state, state.dict, value, None) |
|---|
| 106 | |
|---|
| 107 | def merge(self, session, source, dest, dont_load, _recursive): |
|---|
| 108 | value = attributes.instance_state(source).value_as_iterable( |
|---|
| 109 | self.key, passive=True) |
|---|
| 110 | if value: |
|---|
| 111 | setattr(dest, self.key, value[0]) |
|---|
| 112 | else: |
|---|
| 113 | attributes.instance_state(dest).expire_attributes([self.key]) |
|---|
| 114 | |
|---|
| 115 | def get_col_value(self, column, value): |
|---|
| 116 | return value |
|---|
| 117 | |
|---|
| 118 | class Comparator(PropComparator): |
|---|
| 119 | @util.memoized_instancemethod |
|---|
| 120 | def __clause_element__(self): |
|---|
| 121 | if self.adapter: |
|---|
| 122 | return self.adapter(self.prop.columns[0]) |
|---|
| 123 | else: |
|---|
| 124 | return self.prop.columns[0]._annotate({"parententity": self.mapper, "parentmapper":self.mapper}) |
|---|
| 125 | |
|---|
| 126 | def operate(self, op, *other, **kwargs): |
|---|
| 127 | return op(self.__clause_element__(), *other, **kwargs) |
|---|
| 128 | |
|---|
| 129 | def reverse_operate(self, op, other, **kwargs): |
|---|
| 130 | col = self.__clause_element__() |
|---|
| 131 | return op(col._bind_param(other), col, **kwargs) |
|---|
| 132 | |
|---|
| 133 | # TODO: legacy..do we need this ? (0.5) |
|---|
| 134 | ColumnComparator = Comparator |
|---|
| 135 | |
|---|
| 136 | def __str__(self): |
|---|
| 137 | return str(self.parent.class_.__name__) + "." + self.key |
|---|
| 138 | |
|---|
| 139 | log.class_logger(ColumnProperty) |
|---|
| 140 | |
|---|
| 141 | class CompositeProperty(ColumnProperty): |
|---|
| 142 | """subclasses ColumnProperty to provide composite type support.""" |
|---|
| 143 | |
|---|
| 144 | def __init__(self, class_, *columns, **kwargs): |
|---|
| 145 | if 'comparator' in kwargs: |
|---|
| 146 | util.warn_deprecated("The 'comparator' argument to CompositeProperty is deprecated. Use comparator_factory.") |
|---|
| 147 | kwargs['comparator_factory'] = kwargs['comparator'] |
|---|
| 148 | super(CompositeProperty, self).__init__(*columns, **kwargs) |
|---|
| 149 | self._col_position_map = util.column_dict((c, i) for i, c in enumerate(columns)) |
|---|
| 150 | self.composite_class = class_ |
|---|
| 151 | self.strategy_class = strategies.CompositeColumnLoader |
|---|
| 152 | |
|---|
| 153 | def copy(self): |
|---|
| 154 | return CompositeProperty(deferred=self.deferred, group=self.group, composite_class=self.composite_class, *self.columns) |
|---|
| 155 | |
|---|
| 156 | def do_init(self): |
|---|
| 157 | # skip over ColumnProperty's do_init(), |
|---|
| 158 | # which issues assertions that do not apply to CompositeColumnProperty |
|---|
| 159 | super(ColumnProperty, self).do_init() |
|---|
| 160 | |
|---|
| 161 | def getattr(self, state, column): |
|---|
| 162 | obj = state.get_impl(self.key).get(state, state.dict) |
|---|
| 163 | return self.get_col_value(column, obj) |
|---|
| 164 | |
|---|
| 165 | def getcommitted(self, state, column, passive=False): |
|---|
| 166 | # TODO: no coverage here |
|---|
| 167 | obj = state.get_impl(self.key).get_committed_value(state, state.dict, passive=passive) |
|---|
| 168 | return self.get_col_value(column, obj) |
|---|
| 169 | |
|---|
| 170 | def setattr(self, state, value, column): |
|---|
| 171 | |
|---|
| 172 | obj = state.get_impl(self.key).get(state, state.dict) |
|---|
| 173 | if obj is None: |
|---|
| 174 | obj = self.composite_class(*[None for c in self.columns]) |
|---|
| 175 | state.get_impl(self.key).set(state, state.dict, obj, None) |
|---|
| 176 | |
|---|
| 177 | if hasattr(obj, '__set_composite_values__'): |
|---|
| 178 | values = list(obj.__composite_values__()) |
|---|
| 179 | values[self._col_position_map[column]] = value |
|---|
| 180 | obj.__set_composite_values__(*values) |
|---|
| 181 | else: |
|---|
| 182 | setattr(obj, column.key, value) |
|---|
| 183 | |
|---|
| 184 | def get_col_value(self, column, value): |
|---|
| 185 | if value is None: |
|---|
| 186 | return None |
|---|
| 187 | for a, b in zip(self.columns, value.__composite_values__()): |
|---|
| 188 | if a is column: |
|---|
| 189 | return b |
|---|
| 190 | |
|---|
| 191 | class Comparator(PropComparator): |
|---|
| 192 | def __clause_element__(self): |
|---|
| 193 | if self.adapter: |
|---|
| 194 | # TODO: test coverage for adapted composite comparison |
|---|
| 195 | return expression.ClauseList(*[self.adapter(x) for x in self.prop.columns]) |
|---|
| 196 | else: |
|---|
| 197 | return expression.ClauseList(*self.prop.columns) |
|---|
| 198 | |
|---|
| 199 | __hash__ = None |
|---|
| 200 | |
|---|
| 201 | def __eq__(self, other): |
|---|
| 202 | if other is None: |
|---|
| 203 | values = [None] * len(self.prop.columns) |
|---|
| 204 | else: |
|---|
| 205 | values = other.__composite_values__() |
|---|
| 206 | return sql.and_(*[a==b for a, b in zip(self.prop.columns, values)]) |
|---|
| 207 | |
|---|
| 208 | def __ne__(self, other): |
|---|
| 209 | return sql.not_(self.__eq__(other)) |
|---|
| 210 | |
|---|
| 211 | def __str__(self): |
|---|
| 212 | return str(self.parent.class_.__name__) + "." + self.key |
|---|
| 213 | |
|---|
| 214 | class ConcreteInheritedProperty(MapperProperty): |
|---|
| 215 | extension = None |
|---|
| 216 | |
|---|
| 217 | def setup(self, context, entity, path, adapter, **kwargs): |
|---|
| 218 | pass |
|---|
| 219 | |
|---|
| 220 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
|---|
| 221 | return (None, None) |
|---|
| 222 | |
|---|
| 223 | def instrument_class(self, mapper): |
|---|
| 224 | def warn(): |
|---|
| 225 | raise AttributeError("Concrete %s does not implement attribute %r at " |
|---|
| 226 | "the instance level. Add this property explicitly to %s." % |
|---|
| 227 | (self.parent, self.key, self.parent)) |
|---|
| 228 | |
|---|
| 229 | class NoninheritedConcreteProp(object): |
|---|
| 230 | def __set__(s, obj, value): |
|---|
| 231 | warn() |
|---|
| 232 | def __delete__(s, obj): |
|---|
| 233 | warn() |
|---|
| 234 | def __get__(s, obj, owner): |
|---|
| 235 | warn() |
|---|
| 236 | |
|---|
| 237 | comparator_callable = None |
|---|
| 238 | # TODO: put this process into a deferred callable? |
|---|
| 239 | for m in self.parent.iterate_to_root(): |
|---|
| 240 | p = m._get_property(self.key) |
|---|
| 241 | if not isinstance(p, ConcreteInheritedProperty): |
|---|
| 242 | comparator_callable = p.comparator_factory |
|---|
| 243 | break |
|---|
| 244 | |
|---|
| 245 | attributes.register_descriptor( |
|---|
| 246 | mapper.class_, |
|---|
| 247 | self.key, |
|---|
| 248 | comparator=comparator_callable(self, mapper), |
|---|
| 249 | parententity=mapper, |
|---|
| 250 | property_=self, |
|---|
| 251 | proxy_property=NoninheritedConcreteProp() |
|---|
| 252 | ) |
|---|
| 253 | |
|---|
| 254 | |
|---|
| 255 | class SynonymProperty(MapperProperty): |
|---|
| 256 | |
|---|
| 257 | extension = None |
|---|
| 258 | |
|---|
| 259 | def __init__(self, name, map_column=None, descriptor=None, comparator_factory=None): |
|---|
| 260 | self.name = name |
|---|
| 261 | self.map_column = map_column |
|---|
| 262 | self.descriptor = descriptor |
|---|
| 263 | self.comparator_factory = comparator_factory |
|---|
| 264 | util.set_creation_order(self) |
|---|
| 265 | |
|---|
| 266 | def setup(self, context, entity, path, adapter, **kwargs): |
|---|
| 267 | pass |
|---|
| 268 | |
|---|
| 269 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
|---|
| 270 | return (None, None) |
|---|
| 271 | |
|---|
| 272 | def instrument_class(self, mapper): |
|---|
| 273 | class_ = self.parent.class_ |
|---|
| 274 | |
|---|
| 275 | if self.descriptor is None: |
|---|
| 276 | class SynonymProp(object): |
|---|
| 277 | def __set__(s, obj, value): |
|---|
| 278 | setattr(obj, self.name, value) |
|---|
| 279 | def __delete__(s, obj): |
|---|
| 280 | delattr(obj, self.name) |
|---|
| 281 | def __get__(s, obj, owner): |
|---|
| 282 | if obj is None: |
|---|
| 283 | return s |
|---|
| 284 | return getattr(obj, self.name) |
|---|
| 285 | |
|---|
| 286 | self.descriptor = SynonymProp() |
|---|
| 287 | |
|---|
| 288 | def comparator_callable(prop, mapper): |
|---|
| 289 | def comparator(): |
|---|
| 290 | prop = self.parent._get_property(self.key, resolve_synonyms=True) |
|---|
| 291 | if self.comparator_factory: |
|---|
| 292 | return self.comparator_factory(prop, mapper) |
|---|
| 293 | else: |
|---|
| 294 | return prop.comparator_factory(prop, mapper) |
|---|
| 295 | return comparator |
|---|
| 296 | |
|---|
| 297 | attributes.register_descriptor( |
|---|
| 298 | mapper.class_, |
|---|
| 299 | self.key, |
|---|
| 300 | comparator=comparator_callable(self, mapper), |
|---|
| 301 | parententity=mapper, |
|---|
| 302 | property_=self, |
|---|
| 303 | proxy_property=self.descriptor |
|---|
| 304 | ) |
|---|
| 305 | |
|---|
| 306 | def merge(self, session, source, dest, dont_load, _recursive): |
|---|
| 307 | pass |
|---|
| 308 | |
|---|
| 309 | log.class_logger(SynonymProperty) |
|---|
| 310 | |
|---|
| 311 | class ComparableProperty(MapperProperty): |
|---|
| 312 | """Instruments a Python property for use in query expressions.""" |
|---|
| 313 | |
|---|
| 314 | extension = None |
|---|
| 315 | |
|---|
| 316 | def __init__(self, comparator_factory, descriptor=None): |
|---|
| 317 | self.descriptor = descriptor |
|---|
| 318 | self.comparator_factory = comparator_factory |
|---|
| 319 | util.set_creation_order(self) |
|---|
| 320 | |
|---|
| 321 | def instrument_class(self, mapper): |
|---|
| 322 | """Set up a proxy to the unmanaged descriptor.""" |
|---|
| 323 | |
|---|
| 324 | attributes.register_descriptor( |
|---|
| 325 | mapper.class_, |
|---|
| 326 | self.key, |
|---|
| 327 | comparator=self.comparator_factory(self, mapper), |
|---|
| 328 | parententity=mapper, |
|---|
| 329 | property_=self, |
|---|
| 330 | proxy_property=self.descriptor |
|---|
| 331 | ) |
|---|
| 332 | |
|---|
| 333 | def setup(self, context, entity, path, adapter, **kwargs): |
|---|
| 334 | pass |
|---|
| 335 | |
|---|
| 336 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
|---|
| 337 | return (None, None) |
|---|
| 338 | |
|---|
| 339 | def merge(self, session, source, dest, dont_load, _recursive): |
|---|
| 340 | pass |
|---|
| 341 | |
|---|
| 342 | |
|---|
| 343 | class RelationProperty(StrategizedProperty): |
|---|
| 344 | """Describes an object property that holds a single item or list |
|---|
| 345 | of items that correspond to a related database table. |
|---|
| 346 | """ |
|---|
| 347 | |
|---|
| 348 | def __init__(self, argument, |
|---|
| 349 | secondary=None, primaryjoin=None, |
|---|
| 350 | secondaryjoin=None, |
|---|
| 351 | foreign_keys=None, |
|---|
| 352 | uselist=None, |
|---|
| 353 | order_by=False, |
|---|
| 354 | backref=None, |
|---|
| 355 | back_populates=None, |
|---|
| 356 | post_update=False, |
|---|
| 357 | cascade=False, extension=None, |
|---|
| 358 | viewonly=False, lazy=True, |
|---|
| 359 | collection_class=None, passive_deletes=False, |
|---|
| 360 | passive_updates=True, remote_side=None, |
|---|
| 361 | enable_typechecks=True, join_depth=None, |
|---|
| 362 | comparator_factory=None, |
|---|
| 363 | single_parent=False, |
|---|
| 364 | strategy_class=None, _local_remote_pairs=None, query_class=None): |
|---|
| 365 | |
|---|
| 366 | self.uselist = uselist |
|---|
| 367 | self.argument = argument |
|---|
| 368 | self.secondary = secondary |
|---|
| 369 | self.primaryjoin = primaryjoin |
|---|
| 370 | self.secondaryjoin = secondaryjoin |
|---|
| 371 | self.post_update = post_update |
|---|
| 372 | self.direction = None |
|---|
| 373 | self.viewonly = viewonly |
|---|
| 374 | self.lazy = lazy |
|---|
| 375 | self.single_parent = single_parent |
|---|
| 376 | self._foreign_keys = foreign_keys |
|---|
| 377 | self.collection_class = collection_class |
|---|
| 378 | self.passive_deletes = passive_deletes |
|---|
| 379 | self.passive_updates = passive_updates |
|---|
| 380 | self.remote_side = remote_side |
|---|
| 381 | self.enable_typechecks = enable_typechecks |
|---|
| 382 | self.query_class = query_class |
|---|
| 383 | |
|---|
| 384 | self.join_depth = join_depth |
|---|
| 385 | self.local_remote_pairs = _local_remote_pairs |
|---|
| 386 | self.extension = extension |
|---|
| 387 | self.comparator_factory = comparator_factory or RelationProperty.Comparator |
|---|
| 388 | self.comparator = self.comparator_factory(self, None) |
|---|
| 389 | util.set_creation_order(self) |
|---|
| 390 | |
|---|
| 391 | if strategy_class: |
|---|
| 392 | self.strategy_class = strategy_class |
|---|
| 393 | elif self.lazy == 'dynamic': |
|---|
| 394 | from sqlalchemy.orm import dynamic |
|---|
| 395 | self.strategy_class = dynamic.DynaLoader |
|---|
| 396 | elif self.lazy is False: |
|---|
| 397 | self.strategy_class = strategies.EagerLoader |
|---|
| 398 | elif self.lazy is None: |
|---|
| 399 | self.strategy_class = strategies.NoLoader |
|---|
| 400 | else: |
|---|
| 401 | self.strategy_class = strategies.LazyLoader |
|---|
| 402 | |
|---|
| 403 | self._reverse_property = set() |
|---|
| 404 | |
|---|
| 405 | if cascade is not False: |
|---|
| 406 | self.cascade = CascadeOptions(cascade) |
|---|
| 407 | else: |
|---|
| 408 | self.cascade = CascadeOptions("save-update, merge") |
|---|
| 409 | |
|---|
| 410 | if self.passive_deletes == 'all' and ("delete" in self.cascade or "delete-orphan" in self.cascade): |
|---|
| 411 | raise sa_exc.ArgumentError("Can't set passive_deletes='all' in conjunction with 'delete' or 'delete-orphan' cascade") |
|---|
| 412 | |
|---|
| 413 | self.order_by = order_by |
|---|
| 414 | |
|---|
| 415 | self.back_populates = back_populates |
|---|
| 416 | |
|---|
| 417 | if self.back_populates: |
|---|
| 418 | if backref: |
|---|
| 419 | raise sa_exc.ArgumentError("backref and back_populates keyword arguments are mutually exclusive") |
|---|
| 420 | self.backref = None |
|---|
| 421 | elif isinstance(backref, basestring): |
|---|
| 422 | # propagate explicitly sent primary/secondary join conditions to the BackRef object if |
|---|
| 423 | # just a string was sent |
|---|
| 424 | if secondary is not None: |
|---|
| 425 | # reverse primary/secondary in case of a many-to-many |
|---|
| 426 | self.backref = BackRef(backref, primaryjoin=secondaryjoin, |
|---|
| 427 | secondaryjoin=primaryjoin, passive_updates=self.passive_updates) |
|---|
| 428 | else: |
|---|
| 429 | self.backref = BackRef(backref, primaryjoin=primaryjoin, |
|---|
| 430 | secondaryjoin=secondaryjoin, passive_updates=self.passive_updates) |
|---|
| 431 | else: |
|---|
| 432 | self.backref = backref |
|---|
| 433 | |
|---|
| 434 | def instrument_class(self, mapper): |
|---|
| 435 | attributes.register_descriptor( |
|---|
| 436 | mapper.class_, |
|---|
| 437 | self.key, |
|---|
| 438 | comparator=self.comparator_factory(self, mapper), |
|---|
| 439 | parententity=mapper, |
|---|
| 440 | property_=self |
|---|
| 441 | ) |
|---|
| 442 | |
|---|
| 443 | class Comparator(PropComparator): |
|---|
| 444 | def __init__(self, prop, mapper, of_type=None, adapter=None): |
|---|
| 445 | self.prop = prop |
|---|
| 446 | self.mapper = mapper |
|---|
| 447 | self.adapter = adapter |
|---|
| 448 | if of_type: |
|---|
| 449 | self._of_type = _class_to_mapper(of_type) |
|---|
| 450 | |
|---|
| 451 | def adapted(self, adapter): |
|---|
| 452 | """Return a copy of this PropComparator which will use the given adaption function |
|---|
| 453 | on the local side of generated expressions. |
|---|
| 454 | |
|---|
| 455 | """ |
|---|
| 456 | return self.__class__(self.property, self.mapper, getattr(self, '_of_type', None), adapter) |
|---|
| 457 | |
|---|
| 458 | @property |
|---|
| 459 | def parententity(self): |
|---|
| 460 | return self.property.parent |
|---|
| 461 | |
|---|
| 462 | def __clause_element__(self): |
|---|
| 463 | elem = self.property.parent._with_polymorphic_selectable |
|---|
| 464 | if self.adapter: |
|---|
| 465 | return self.adapter(elem) |
|---|
| 466 | else: |
|---|
| 467 | return elem |
|---|
| 468 | |
|---|
| 469 | def operate(self, op, *other, **kwargs): |
|---|
| 470 | return op(self, *other, **kwargs) |
|---|
| 471 | |
|---|
| 472 | def reverse_operate(self, op, other, **kwargs): |
|---|
| 473 | return op(self, *other, **kwargs) |
|---|
| 474 | |
|---|
| 475 | def of_type(self, cls): |
|---|
| 476 | return RelationProperty.Comparator(self.property, self.mapper, cls, adapter=self.adapter) |
|---|
| 477 | |
|---|
| 478 | def in_(self, other): |
|---|
| 479 | raise NotImplementedError("in_() not yet supported for relations. For a " |
|---|
| 480 | "simple many-to-one, use in_() against the set of foreign key values.") |
|---|
| 481 | |
|---|
| 482 | __hash__ = None |
|---|
| 483 | |
|---|
| 484 | def __eq__(self, other): |
|---|
| 485 | if other is None: |
|---|
| 486 | if self.property.direction in [ONETOMANY, MANYTOMANY]: |
|---|
| 487 | return ~self._criterion_exists() |
|---|
| 488 | else: |
|---|
| 489 | return _orm_annotate(self.property._optimized_compare(None, adapt_source=self.adapter)) |
|---|
| 490 | elif self.property.uselist: |
|---|
| 491 | raise sa_exc.InvalidRequestError("Can't compare a collection to an object or collection; use contains() to test for membership.") |
|---|
| 492 | else: |
|---|
| 493 | return _orm_annotate(self.property._optimized_compare(other, adapt_source=self.adapter)) |
|---|
| 494 | |
|---|
| 495 | def _criterion_exists(self, criterion=None, **kwargs): |
|---|
| 496 | if getattr(self, '_of_type', None): |
|---|
| 497 | target_mapper = self._of_type |
|---|
| 498 | to_selectable = target_mapper._with_polymorphic_selectable |
|---|
| 499 | if self.property._is_self_referential(): |
|---|
| 500 | to_selectable = to_selectable.alias() |
|---|
| 501 | |
|---|
| 502 | single_crit = target_mapper._single_table_criterion |
|---|
| 503 | if single_crit: |
|---|
| 504 | if criterion is not None: |
|---|
| 505 | criterion = single_crit & criterion |
|---|
| 506 | else: |
|---|
| 507 | criterion = single_crit |
|---|
| 508 | else: |
|---|
| 509 | to_selectable = None |
|---|
| 510 | |
|---|
| 511 | if self.adapter: |
|---|
| 512 | source_selectable = self.__clause_element__() |
|---|
| 513 | else: |
|---|
| 514 | source_selectable = None |
|---|
| 515 | |
|---|
| 516 | pj, sj, source, dest, secondary, target_adapter = \ |
|---|
| 517 | self.property._create_joins(dest_polymorphic=True, dest_selectable=to_selectable, source_selectable=source_selectable) |
|---|
| 518 | |
|---|
| 519 | for k in kwargs: |
|---|
| 520 | crit = self.property.mapper.class_manager[k] == kwargs[k] |
|---|
| 521 | if criterion is None: |
|---|
| 522 | criterion = crit |
|---|
| 523 | else: |
|---|
| 524 | criterion = criterion & crit |
|---|
| 525 | |
|---|
| 526 | # annotate the *local* side of the join condition, in the case of pj + sj this |
|---|
| 527 | # is the full primaryjoin, in the case of just pj its the local side of |
|---|
| 528 | # the primaryjoin. |
|---|
| 529 | if sj: |
|---|
| 530 | j = _orm_annotate(pj) & sj |
|---|
| 531 | else: |
|---|
| 532 | j = _orm_annotate(pj, exclude=self.property.remote_side) |
|---|
| 533 | |
|---|
| 534 | if criterion and target_adapter: |
|---|
| 535 | # limit this adapter to annotated only? |
|---|
| 536 | criterion = target_adapter.traverse(criterion) |
|---|
| 537 | |
|---|
| 538 | # only have the "joined left side" of what we return be subject to Query adaption. The right |
|---|
| 539 | # side of it is used for an exists() subquery and should not correlate or otherwise reach out |
|---|
| 540 | # to anything in the enclosing query. |
|---|
| 541 | if criterion: |
|---|
| 542 | criterion = criterion._annotate({'_halt_adapt': True}) |
|---|
| 543 | |
|---|
| 544 | crit = j & criterion |
|---|
| 545 | |
|---|
| 546 | return sql.exists([1], crit, from_obj=dest).correlate(source) |
|---|
| 547 | |
|---|
| 548 | def any(self, criterion=None, **kwargs): |
|---|
| 549 | if not self.property.uselist: |
|---|
| 550 | raise sa_exc.InvalidRequestError("'any()' not implemented for scalar attributes. Use has().") |
|---|
| 551 | |
|---|
| 552 | return self._criterion_exists(criterion, **kwargs) |
|---|
| 553 | |
|---|
| 554 | def has(self, criterion=None, **kwargs): |
|---|
| 555 | if self.property.uselist: |
|---|
| 556 | raise sa_exc.InvalidRequestError("'has()' not implemented for collections. Use any().") |
|---|
| 557 | return self._criterion_exists(criterion, **kwargs) |
|---|
| 558 | |
|---|
| 559 | def contains(self, other, **kwargs): |
|---|
| 560 | if not self.property.uselist: |
|---|
| 561 | raise sa_exc.InvalidRequestError("'contains' not implemented for scalar attributes. Use ==") |
|---|
| 562 | clause = self.property._optimized_compare(other, adapt_source=self.adapter) |
|---|
| 563 | |
|---|
| 564 | if self.property.secondaryjoin: |
|---|
| 565 | clause.negation_clause = self.__negated_contains_or_equals(other) |
|---|
| 566 | |
|---|
| 567 | return clause |
|---|
| 568 | |
|---|
| 569 | def __negated_contains_or_equals(self, other): |
|---|
| 570 | if self.property.direction == MANYTOONE: |
|---|
| 571 | state = attributes.instance_state(other) |
|---|
| 572 | strategy = self.property._get_strategy(strategies.LazyLoader) |
|---|
| 573 | |
|---|
| 574 | def state_bindparam(state, col): |
|---|
| 575 | o = state.obj() # strong ref |
|---|
| 576 | return lambda: self.property.mapper._get_committed_attr_by_column(o, col) |
|---|
| 577 | |
|---|
| 578 | def adapt(col): |
|---|
| 579 | if self.adapter: |
|---|
| 580 | return self.adapter(col) |
|---|
| 581 | else: |
|---|
| 582 | return col |
|---|
| 583 | |
|---|
| 584 | if strategy.use_get: |
|---|
| 585 | return sql.and_(*[ |
|---|
| 586 | sql.or_( |
|---|
| 587 | adapt(x) != state_bindparam(state, y), |
|---|
| 588 | adapt(x) == None) |
|---|
| 589 | for (x, y) in self.property.local_remote_pairs]) |
|---|
| 590 | |
|---|
| 591 | criterion = sql.and_(*[x==y for (x, y) in zip(self.property.mapper.primary_key, self.property.mapper.primary_key_from_instance(other))]) |
|---|
| 592 | return ~self._criterion_exists(criterion) |
|---|
| 593 | |
|---|
| 594 | def __ne__(self, other): |
|---|
| 595 | if other is None: |
|---|
| 596 | if self.property.direction == MANYTOONE: |
|---|
| 597 | return sql.or_(*[x!=None for x in self.property._foreign_keys]) |
|---|
| 598 | else: |
|---|
| 599 | return self._criterion_exists() |
|---|
| 600 | elif self.property.uselist: |
|---|
| 601 | raise sa_exc.InvalidRequestError("Can't compare a collection to an object or collection; use contains() to test for membership.") |
|---|
| 602 | else: |
|---|
| 603 | return self.__negated_contains_or_equals(other) |
|---|
| 604 | |
|---|
| 605 | @util.memoized_property |
|---|
| 606 | def property(self): |
|---|
| 607 | self.prop.parent.compile() |
|---|
| 608 | return self.prop |
|---|
| 609 | |
|---|
| 610 | def compare(self, op, value, value_is_parent=False): |
|---|
| 611 | if op == operators.eq: |
|---|
| 612 | if value is None: |
|---|
| 613 | if self.uselist: |
|---|
| 614 | return ~sql.exists([1], self.primaryjoin) |
|---|
| 615 | else: |
|---|
| 616 | return self._optimized_compare(None, value_is_parent=value_is_parent) |
|---|
| 617 | else: |
|---|
| 618 | return self._optimized_compare(value, value_is_parent=value_is_parent) |
|---|
| 619 | else: |
|---|
| 620 | return op(self.comparator, value) |
|---|
| 621 | |
|---|
| 622 | def _optimized_compare(self, value, value_is_parent=False, adapt_source=None): |
|---|
| 623 | if value is not None: |
|---|
| 624 | value = attributes.instance_state(value) |
|---|
| 625 | return self._get_strategy(strategies.LazyLoader).\ |
|---|
| 626 | lazy_clause(value, reverse_direction=not value_is_parent, alias_secondary=True, adapt_source=adapt_source) |
|---|
| 627 | |
|---|
| 628 | def __str__(self): |
|---|
| 629 | return str(self.parent.class_.__name__) + "." + self.key |
|---|
| 630 | |
|---|
| 631 | def merge(self, session, source, dest, dont_load, _recursive): |
|---|
| 632 | if not dont_load: |
|---|
| 633 | # TODO: no test coverage for recursive check |
|---|
| 634 | for r in self._reverse_property: |
|---|
| 635 | if (source, r) in _recursive: |
|---|
| 636 | return |
|---|
| 637 | |
|---|
| 638 | source_state = attributes.instance_state(source) |
|---|
| 639 | dest_state, dest_dict = attributes.instance_state(dest), attributes.instance_dict(dest) |
|---|
| 640 | |
|---|
| 641 | if not "merge" in self.cascade: |
|---|
| 642 | dest_state.expire_attributes([self.key]) |
|---|
| 643 | return |
|---|
| 644 | |
|---|
| 645 | instances = source_state.value_as_iterable(self.key, passive=True) |
|---|
| 646 | |
|---|
| 647 | if not instances: |
|---|
| 648 | return |
|---|
| 649 | |
|---|
| 650 | if self.uselist: |
|---|
| 651 | dest_list = [] |
|---|
| 652 | for current in instances: |
|---|
| 653 | _recursive[(current, self)] = True |
|---|
| 654 | obj = session._merge(current, dont_load=dont_load, _recursive=_recursive) |
|---|
| 655 | if obj is not None: |
|---|
| 656 | dest_list.append(obj) |
|---|
| 657 | if dont_load: |
|---|
| 658 | coll = attributes.init_collection(dest_state, self.key) |
|---|
| 659 | for c in dest_list: |
|---|
| 660 | coll.append_without_event(c) |
|---|
| 661 | else: |
|---|
| 662 | getattr(dest.__class__, self.key).impl._set_iterable(dest_state, dest_dict, dest_list) |
|---|
| 663 | else: |
|---|
| 664 | current = instances[0] |
|---|
| 665 | if current is not None: |
|---|
| 666 | _recursive[(current, self)] = True |
|---|
| 667 | obj = session._merge(current, dont_load=dont_load, _recursive=_recursive) |
|---|
| 668 | if obj is not None: |
|---|
| 669 | if dont_load: |
|---|
| 670 | dest_state.dict[self.key] = obj |
|---|
| 671 | else: |
|---|
| 672 | setattr(dest, self.key, obj) |
|---|
| 673 | |
|---|
| 674 | def cascade_iterator(self, type_, state, visited_instances, halt_on=None): |
|---|
| 675 | if not type_ in self.cascade: |
|---|
| 676 | return |
|---|
| 677 | |
|---|
| 678 | # only actively lazy load on the 'delete' cascade |
|---|
| 679 | if type_ != 'delete' or self.passive_deletes: |
|---|
| 680 | passive = attributes.PASSIVE_NO_INITIALIZE |
|---|
| 681 | else: |
|---|
| 682 | passive = attributes.PASSIVE_OFF |
|---|
| 683 | |
|---|
| 684 | mapper = self.mapper.primary_mapper() |
|---|
| 685 | instances = state.value_as_iterable(self.key, passive=passive) |
|---|
| 686 | if instances: |
|---|
| 687 | for c in instances: |
|---|
| 688 | if c is not None and c not in visited_instances and (halt_on is None or not halt_on(c)): |
|---|
| 689 | if not isinstance(c, self.mapper.class_): |
|---|
| 690 | raise AssertionError("Attribute '%s' on class '%s' doesn't handle objects " |
|---|
| 691 | "of type '%s'" % (self.key, str(self.parent.class_), str(c.__class__))) |
|---|
| 692 | visited_instances.add(c) |
|---|
| 693 | |
|---|
| 694 | # cascade using the mapper local to this object, so that its individual properties are located |
|---|
| 695 | instance_mapper = object_mapper(c) |
|---|
| 696 | yield (c, instance_mapper, attributes.instance_state(c)) |
|---|
| 697 | |
|---|
| 698 | def _add_reverse_property(self, key): |
|---|
| 699 | other = self.mapper._get_property(key) |
|---|
| 700 | self._reverse_property.add(other) |
|---|
| 701 | other._reverse_property.add(self) |
|---|
| 702 | |
|---|
| 703 | if not other._get_target().common_parent(self.parent): |
|---|
| 704 | raise sa_exc.ArgumentError("reverse_property %r on relation %s references " |
|---|
| 705 | "relation %s, which does not reference mapper %s" % (key, self, other, self.parent)) |
|---|
| 706 | |
|---|
| 707 | if self.direction in (ONETOMANY, MANYTOONE) and self.direction == other.direction: |
|---|
| 708 | raise sa_exc.ArgumentError("%s and back-reference %s are both of the same direction %r." |
|---|
| 709 | " Did you mean to set remote_side on the many-to-one side ?" % (self, other, self.direction)) |
|---|
| 710 | |
|---|
| 711 | def do_init(self): |
|---|
| 712 | self._get_target() |
|---|
| 713 | self._process_dependent_arguments() |
|---|
| 714 | self._determine_joins() |
|---|
| 715 | self._determine_synchronize_pairs() |
|---|
| 716 | self._determine_direction() |
|---|
| 717 | self._determine_local_remote_pairs() |
|---|
| 718 | self._post_init() |
|---|
| 719 | super(RelationProperty, self).do_init() |
|---|
| 720 | |
|---|
| 721 | def _get_target(self): |
|---|
| 722 | if not hasattr(self, 'mapper'): |
|---|
| 723 | if isinstance(self.argument, type): |
|---|
| 724 | self.mapper = mapper.class_mapper(self.argument, compile=False) |
|---|
| 725 | elif isinstance(self.argument, mapper.Mapper): |
|---|
| 726 | self.mapper = self.argument |
|---|
| 727 | elif util.callable(self.argument): |
|---|
| 728 | # accept a callable to suit various deferred-configurational schemes |
|---|
| 729 | self.mapper = mapper.class_mapper(self.argument(), compile=False) |
|---|
| 730 | else: |
|---|
| 731 | raise sa_exc.ArgumentError("relation '%s' expects a class or a mapper argument (received: %s)" % (self.key, type(self.argument))) |
|---|
| 732 | assert isinstance(self.mapper, mapper.Mapper), self.mapper |
|---|
| 733 | return self.mapper |
|---|
| 734 | |
|---|
| 735 | def _process_dependent_arguments(self): |
|---|
| 736 | |
|---|
| 737 | # accept callables for other attributes which may require deferred initialization |
|---|
| 738 | for attr in ('order_by', 'primaryjoin', 'secondaryjoin', 'secondary', '_foreign_keys', 'remote_side'): |
|---|
| 739 | if util.callable(getattr(self, attr)): |
|---|
| 740 | setattr(self, attr, getattr(self, attr)()) |
|---|
| 741 | |
|---|
| 742 | # in the case that InstrumentedAttributes were used to construct |
|---|
| 743 | # primaryjoin or secondaryjoin, remove the "_orm_adapt" annotation so these |
|---|
| 744 | # interact with Query in the same way as the original Table-bound Column objects |
|---|
| 745 | for attr in ('primaryjoin', 'secondaryjoin'): |
|---|
| 746 | val = getattr(self, attr) |
|---|
| 747 | if val is not None: |
|---|
| 748 | util.assert_arg_type(val, sql.ClauseElement, attr) |
|---|
| 749 | setattr(self, attr, _orm_deannotate(val)) |
|---|
| 750 | |
|---|
| 751 | if self.order_by: |
|---|
| 752 | self.order_by = [expression._literal_as_column(x) for x in util.to_list(self.order_by)] |
|---|
| 753 | |
|---|
| 754 | self._foreign_keys = util.column_set(expression._literal_as_column(x) for x in util.to_column_set(self._foreign_keys)) |
|---|
| 755 | self.remote_side = util.column_set(expression._literal_as_column(x) for x in util.to_column_set(self.remote_side)) |
|---|
| 756 | |
|---|
| 757 | if not self.parent.concrete: |
|---|
| 758 | for inheriting in self.parent.iterate_to_root(): |
|---|
| 759 | if inheriting is not self.parent and inheriting._get_property(self.key, raiseerr=False): |
|---|
| 760 | util.warn( |
|---|
| 761 | ("Warning: relation '%s' on mapper '%s' supercedes " |
|---|
| 762 | "the same relation on inherited mapper '%s'; this " |
|---|
| 763 | "can cause dependency issues during flush") % |
|---|
| 764 | (self.key, self.parent, inheriting)) |
|---|
| 765 | |
|---|
| 766 | # TODO: remove 'self.table' |
|---|
| 767 | self.target = self.table = self.mapper.mapped_table |
|---|
| 768 | |
|---|
| 769 | if self.cascade.delete_orphan: |
|---|
| 770 | if self.parent.class_ is self.mapper.class_: |
|---|
| 771 | raise sa_exc.ArgumentError("In relationship '%s', can't establish 'delete-orphan' cascade " |
|---|
| 772 | "rule on a self-referential relationship. " |
|---|
| 773 | "You probably want cascade='all', which includes delete cascading but not orphan detection." %(str(self))) |
|---|
| 774 | self.mapper.primary_mapper().delete_orphans.append((self.key, self.parent.class_)) |
|---|
| 775 | |
|---|
| 776 | def _determine_joins(self): |
|---|
| 777 | if self.secondaryjoin is not None and self.secondary is None: |
|---|
| 778 | raise sa_exc.ArgumentError("Property '" + self.key + "' specified with secondary join condition but no secondary argument") |
|---|
| 779 | # if join conditions were not specified, figure them out based on foreign keys |
|---|
| 780 | |
|---|
| 781 | def _search_for_join(mapper, table): |
|---|
| 782 | # find a join between the given mapper's mapped table and the given table. |
|---|
| 783 | # will try the mapper's local table first for more specificity, then if not |
|---|
| 784 | # found will try the more general mapped table, which in the case of inheritance |
|---|
| 785 | # is a join. |
|---|
| 786 | try: |
|---|
| 787 | return join_condition(mapper.local_table, table) |
|---|
| 788 | except sa_exc.ArgumentError, e: |
|---|
| 789 | return join_condition(mapper.mapped_table, table) |
|---|
| 790 | |
|---|
| 791 | try: |
|---|
| 792 | if self.secondary is not None: |
|---|
| 793 | if self.secondaryjoin is None: |
|---|
| 794 | self.secondaryjoin = _search_for_join(self.mapper, self.secondary) |
|---|
| 795 | if self.primaryjoin is None: |
|---|
| 796 | self.primaryjoin = _search_for_join(self.parent, self.secondary) |
|---|
| 797 | else: |
|---|
| 798 | if self.primaryjoin is None: |
|---|
| 799 | self.primaryjoin = _search_for_join(self.parent, self.target) |
|---|
| 800 | except sa_exc.ArgumentError, e: |
|---|
| 801 | raise sa_exc.ArgumentError("Could not determine join condition between " |
|---|
| 802 | "parent/child tables on relation %s. " |
|---|
| 803 | "Specify a 'primaryjoin' expression. If this is a " |
|---|
| 804 | "many-to-many relation, 'secondaryjoin' is needed as well." % (self)) |
|---|
| 805 | |
|---|
| 806 | def _col_is_part_of_mappings(self, column): |
|---|
| 807 | if self.secondary is None: |
|---|
| 808 | return self.parent.mapped_table.c.contains_column(column) or \ |
|---|
| 809 | self.target.c.contains_column(column) |
|---|
| 810 | else: |
|---|
| 811 | return self.parent.mapped_table.c.contains_column(column) or \ |
|---|
| 812 | self.target.c.contains_column(column) or \ |
|---|
| 813 | self.secondary.c.contains_column(column) is not None |
|---|
| 814 | |
|---|
| 815 | def _determine_synchronize_pairs(self): |
|---|
| 816 | |
|---|
| 817 | if self.local_remote_pairs: |
|---|
| 818 | if not self._foreign_keys: |
|---|
| 819 | raise sa_exc.ArgumentError("foreign_keys argument is required with _local_remote_pairs argument") |
|---|
| 820 | |
|---|
| 821 | self.synchronize_pairs = [] |
|---|
| 822 | |
|---|
| 823 | for l, r in self.local_remote_pairs: |
|---|
| 824 | if r in self._foreign_keys: |
|---|
| 825 | self.synchronize_pairs.append((l, r)) |
|---|
| 826 | elif l in self._foreign_keys: |
|---|
| 827 | self.synchronize_pairs.append((r, l)) |
|---|
| 828 | else: |
|---|
| 829 | eq_pairs = criterion_as_pairs( |
|---|
| 830 | self.primaryjoin, |
|---|
| 831 | consider_as_foreign_keys=self._foreign_keys, |
|---|
| 832 | any_operator=self.viewonly |
|---|
| 833 | ) |
|---|
| 834 | eq_pairs = [ |
|---|
| 835 | (l, r) for l, r in eq_pairs if |
|---|
| 836 | (self._col_is_part_of_mappings(l) and |
|---|
| 837 | self._col_is_part_of_mappings(r)) |
|---|
| 838 | or self.viewonly and r in self._foreign_keys |
|---|
| 839 | ] |
|---|
| 840 | |
|---|
| 841 | if not eq_pairs: |
|---|
| 842 | if not self.viewonly and criterion_as_pairs(self.primaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=True): |
|---|
| 843 | raise sa_exc.ArgumentError("Could not locate any equated, locally " |
|---|
| 844 | "mapped column pairs for primaryjoin condition '%s' on relation %s. " |
|---|
| 845 | "For more relaxed rules on join conditions, the relation may be " |
|---|
| 846 | "marked as viewonly=True." % (self.primaryjoin, self) |
|---|
| 847 | ) |
|---|
| 848 | else: |
|---|
| 849 | if self._foreign_keys: |
|---|
| 850 | raise sa_exc.ArgumentError("Could not determine relation direction for " |
|---|
| 851 | "primaryjoin condition '%s', on relation %s. " |
|---|
| 852 | "Do the columns in 'foreign_keys' represent only the 'foreign' columns " |
|---|
| 853 | "in this join condition ?" % (self.primaryjoin, self)) |
|---|
| 854 | else: |
|---|
| 855 | raise sa_exc.ArgumentError("Could not determine relation direction for " |
|---|
| 856 | "primaryjoin condition '%s', on relation %s. " |
|---|
| 857 | "Specify the 'foreign_keys' argument to indicate which columns " |
|---|
| 858 | "on the relation are foreign." % (self.primaryjoin, self)) |
|---|
| 859 | |
|---|
| 860 | self.synchronize_pairs = eq_pairs |
|---|
| 861 | |
|---|
| 862 | if self.secondaryjoin: |
|---|
| 863 | sq_pairs = criterion_as_pairs(self.secondaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=self.viewonly) |
|---|
| 864 | 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] |
|---|
| 865 | |
|---|
| 866 | if not sq_pairs: |
|---|
| 867 | if not self.viewonly and criterion_as_pairs(self.secondaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=True): |
|---|
| 868 | raise sa_exc.ArgumentError("Could not locate any equated, locally mapped " |
|---|
| 869 | "column pairs for secondaryjoin condition '%s' on relation %s. " |
|---|
| 870 | "For more relaxed rules on join conditions, the " |
|---|
| 871 | "relation may be marked as viewonly=True." % (self.secondaryjoin, self) |
|---|
| 872 | ) |
|---|
| 873 | else: |
|---|
| 874 | raise sa_exc.ArgumentError("Could not determine relation direction " |
|---|
| 875 | "for secondaryjoin condition '%s', on relation %s. " |
|---|
| 876 | "Specify the foreign_keys argument to indicate which " |
|---|
| 877 | "columns on the relation are foreign." % (self.secondaryjoin, self)) |
|---|
| 878 | |
|---|
| 879 | self.secondary_synchronize_pairs = sq_pairs |
|---|
| 880 | else: |
|---|
| 881 | self.secondary_synchronize_pairs = None |
|---|
| 882 | |
|---|
| 883 | self._foreign_keys = util.column_set(r for l, r in self.synchronize_pairs) |
|---|
| 884 | if self.secondary_synchronize_pairs: |
|---|
| 885 | self._foreign_keys.update(r for l, r in self.secondary_synchronize_pairs) |
|---|
| 886 | |
|---|
| 887 | def _determine_direction(self): |
|---|
| 888 | if self.secondaryjoin is not None: |
|---|
| 889 | self.direction = MANYTOMANY |
|---|
| 890 | elif self._refers_to_parent_table(): |
|---|
| 891 | # self referential defaults to ONETOMANY unless the "remote" side is present |
|---|
| 892 | # and does not reference any foreign key columns |
|---|
| 893 | |
|---|
| 894 | if self.local_remote_pairs: |
|---|
| 895 | remote = [r for l, r in self.local_remote_pairs] |
|---|
| 896 | elif self.remote_side: |
|---|
| 897 | remote = self.remote_side |
|---|
| 898 | else: |
|---|
| 899 | remote = None |
|---|
| 900 | |
|---|
| 901 | if not remote or self._foreign_keys.\ |
|---|
| 902 | difference(l for l, r in self.synchronize_pairs).\ |
|---|
| 903 | intersection(remote): |
|---|
| 904 | self.direction = ONETOMANY |
|---|
| 905 | else: |
|---|
| 906 | self.direction = MANYTOONE |
|---|
| 907 | |
|---|
| 908 | else: |
|---|
| 909 | foreign_keys = [f for c, f in self.synchronize_pairs] |
|---|
| 910 | |
|---|
| 911 | parentcols = util.column_set(self.parent.mapped_table.c) |
|---|
| 912 | targetcols = util.column_set(self.mapper.mapped_table.c) |
|---|
| 913 | |
|---|
| 914 | # fk collection which suggests ONETOMANY. |
|---|
| 915 | onetomany_fk = targetcols.intersection(foreign_keys) |
|---|
| 916 | |
|---|
| 917 | # fk collection which suggests MANYTOONE. |
|---|
| 918 | manytoone_fk = parentcols.intersection(foreign_keys) |
|---|
| 919 | |
|---|
| 920 | if not onetomany_fk and not manytoone_fk: |
|---|
| 921 | raise sa_exc.ArgumentError( |
|---|
| 922 | "Can't determine relation direction for relationship '%s' " |
|---|
| 923 | "- foreign key columns are present in neither the " |
|---|
| 924 | "parent nor the child's mapped tables" % self ) |
|---|
| 925 | |
|---|
| 926 | elif onetomany_fk and manytoone_fk: |
|---|
| 927 | # fks on both sides. do the same |
|---|
| 928 | # test only based on the local side. |
|---|
| 929 | referents = [c for c, f in self.synchronize_pairs] |
|---|
| 930 | onetomany_local = parentcols.intersection(referents) |
|---|
| 931 | manytoone_local = targetcols.intersection(referents) |
|---|
| 932 | |
|---|
| 933 | if onetomany_local and not manytoone_local: |
|---|
| 934 | self.direction = ONETOMANY |
|---|
| 935 | elif manytoone_local and not onetomany_local: |
|---|
| 936 | self.direction = MANYTOONE |
|---|
| 937 | elif onetomany_fk: |
|---|
| 938 | self.direction = ONETOMANY |
|---|
| 939 | elif manytoone_fk: |
|---|
| 940 | self.direction = MANYTOONE |
|---|
| 941 | |
|---|
| 942 | if not self.direction: |
|---|
| 943 | raise sa_exc.ArgumentError( |
|---|
| 944 | "Can't determine relation direction for relationship '%s' " |
|---|
| 945 | "- foreign key columns are present in both the parent and " |
|---|
| 946 | "the child's mapped tables. Specify 'foreign_keys' " |
|---|
| 947 | "argument." % self) |
|---|
| 948 | |
|---|
| 949 | if self.cascade.delete_orphan and not self.single_parent and \ |
|---|
| 950 | (self.direction is MANYTOMANY or self.direction is MANYTOONE): |
|---|
| 951 | util.warn("On %s, delete-orphan cascade is not supported on a " |
|---|
| 952 | "many-to-many or many-to-one relationship when single_parent is not set. " |
|---|
| 953 | " Set single_parent=True on the relation()." % self) |
|---|
| 954 | |
|---|
| 955 | def _determine_local_remote_pairs(self): |
|---|
| 956 | if not self.local_remote_pairs: |
|---|
| 957 | if self.remote_side: |
|---|
| 958 | if self.direction is MANYTOONE: |
|---|
| 959 | self.local_remote_pairs = [ |
|---|
| 960 | (r, l) for l, r in |
|---|
| 961 | criterion_as_pairs(self.primaryjoin, consider_as_referenced_keys=self.remote_side, any_operator=True) |
|---|
| 962 | ] |
|---|
| 963 | else: |
|---|
| 964 | self.local_remote_pairs = criterion_as_pairs(self.primaryjoin, consider_as_foreign_keys=self.remote_side, any_operator=True) |
|---|
| 965 | |
|---|
| 966 | if not self.local_remote_pairs: |
|---|
| 967 | raise sa_exc.ArgumentError("Relation %s could not determine any local/remote column pairs from remote side argument %r" % (self, self.remote_side)) |
|---|
| 968 | |
|---|
| 969 | else: |
|---|
| 970 | if self.viewonly: |
|---|
| 971 | eq_pairs = self.synchronize_pairs |
|---|
| 972 | if self.secondaryjoin: |
|---|
| 973 | eq_pairs += self.secondary_synchronize_pairs |
|---|
| 974 | else: |
|---|
| 975 | eq_pairs = criterion_as_pairs(self.primaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=True) |
|---|
| 976 | if self.secondaryjoin: |
|---|
| 977 | eq_pairs += criterion_as_pairs(self.secondaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=True) |
|---|
| 978 | 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)] |
|---|
| 979 | |
|---|
| 980 | if self.direction is MANYTOONE: |
|---|
| 981 | self.local_remote_pairs = [(r, l) for l, r in eq_pairs] |
|---|
| 982 | else: |
|---|
| 983 | self.local_remote_pairs = eq_pairs |
|---|
| 984 | elif self.remote_side: |
|---|
| 985 | raise sa_exc.ArgumentError("remote_side argument is redundant against more detailed _local_remote_side argument.") |
|---|
| 986 | |
|---|
| 987 | for l, r in self.local_remote_pairs: |
|---|
| 988 | |
|---|
| 989 | if self.direction is ONETOMANY and not self._col_is_part_of_mappings(l): |
|---|
| 990 | raise sa_exc.ArgumentError("Local column '%s' is not part of mapping %s. " |
|---|
| 991 | "Specify remote_side argument to indicate which column " |
|---|
| 992 | "lazy join condition should compare against." % (l, self.parent)) |
|---|
| 993 | |
|---|
| 994 | elif self.direction is MANYTOONE and not self._col_is_part_of_mappings(r): |
|---|
| 995 | raise sa_exc.ArgumentError("Remote column '%s' is not part of mapping %s. " |
|---|
| 996 | "Specify remote_side argument to indicate which column lazy " |
|---|
| 997 | "join condition should bind." % (r, self.mapper)) |
|---|
| 998 | |
|---|
| 999 | self.local_side, self.remote_side = [util.ordered_column_set(x) for x in zip(*list(self.local_remote_pairs))] |
|---|
| 1000 | |
|---|
| 1001 | |
|---|
| 1002 | def _post_init(self): |
|---|
| 1003 | if self._should_log_info: |
|---|
| 1004 | self.logger.info(str(self) + " setup primary join %s" % self.primaryjoin) |
|---|
| 1005 | self.logger.info(str(self) + " setup secondary join %s" % self.secondaryjoin) |
|---|
| 1006 | self.logger.info(str(self) + " synchronize pairs [%s]" % ",".join("(%s => %s)" % (l, r) for l, r in self.synchronize_pairs)) |
|---|
| 1007 | self.logger.info(str(self) + " secondary synchronize pairs [%s]" % ",".join(("(%s => %s)" % (l, r) for l, r in self.secondary_synchronize_pairs or []))) |
|---|
| 1008 | self.logger.info(str(self) + " local/remote pairs [%s]" % ",".join("(%s / %s)" % (l, r) for l, r in self.local_remote_pairs)) |
|---|
| 1009 | self.logger.info(str(self) + " relation direction %s" % self.direction) |
|---|
| 1010 | |
|---|
| 1011 | if self.uselist is None and self.direction is MANYTOONE: |
|---|
| 1012 | self.uselist = False |
|---|
| 1013 | |
|---|
| 1014 | if self.uselist is None: |
|---|
| 1015 | self.uselist = True |
|---|
| 1016 | |
|---|
| 1017 | if not self.viewonly: |
|---|
| 1018 | self._dependency_processor = dependency.create_dependency_processor(self) |
|---|
| 1019 | |
|---|
| 1020 | # primary property handler, set up class attributes |
|---|
| 1021 | if self.is_primary(): |
|---|
| 1022 | if self.back_populates: |
|---|
| 1023 | self.extension = list(util.to_list(self.extension, default=[])) |
|---|
| 1024 | self.extension.append(attributes.GenericBackrefExtension(self.back_populates)) |
|---|
| 1025 | self._add_reverse_property(self.back_populates) |
|---|
| 1026 | |
|---|
| 1027 | if self.backref is not None: |
|---|
| 1028 | self.backref.compile(self) |
|---|
| 1029 | elif not mapper.class_mapper(self.parent.class_, compile=False)._get_property(self.key, raiseerr=False): |
|---|
| 1030 | raise sa_exc.ArgumentError("Attempting to assign a new relation '%s' to " |
|---|
| 1031 | "a non-primary mapper on class '%s'. New relations can only be " |
|---|
| 1032 | "added to the primary mapper, i.e. the very first " |
|---|
| 1033 | "mapper created for class '%s' " % (self.key, self.parent.class_.__name__, self.parent.class_.__name__)) |
|---|
| 1034 | |
|---|
| 1035 | |
|---|
| 1036 | def _refers_to_parent_table(self): |
|---|
| 1037 | for c, f in self.synchronize_pairs: |
|---|
| 1038 | if c.table is f.table: |
|---|
| 1039 | return True |
|---|
| 1040 | else: |
|---|
| 1041 | return False |
|---|
| 1042 | |
|---|
| 1043 | def _is_self_referential(self): |
|---|
| 1044 | return self.mapper.common_parent(self.parent) |
|---|
| 1045 | |
|---|
| 1046 | def _create_joins(self, source_polymorphic=False, source_selectable=None, dest_polymorphic=False, dest_selectable=None, of_type=None): |
|---|
| 1047 | if source_selectable is None: |
|---|
| 1048 | if source_polymorphic and self.parent.with_polymorphic: |
|---|
| 1049 | source_selectable = self.parent._with_polymorphic_selectable |
|---|
| 1050 | |
|---|
| 1051 | aliased = False |
|---|
| 1052 | if dest_selectable is None: |
|---|
| 1053 | if dest_polymorphic and self.mapper.with_polymorphic: |
|---|
| 1054 | dest_selectable = self.mapper._with_polymorphic_selectable |
|---|
| 1055 | aliased = True |
|---|
| 1056 | else: |
|---|
| 1057 | dest_selectable = self.mapper.mapped_table |
|---|
| 1058 | |
|---|
| 1059 | if self._is_self_referential() and source_selectable is None: |
|---|
| 1060 | dest_selectable = dest_selectable.alias() |
|---|
| 1061 | aliased = True |
|---|
| 1062 | else: |
|---|
| 1063 | aliased = True |
|---|
| 1064 | |
|---|
| 1065 | aliased = aliased or bool(source_selectable) |
|---|
| 1066 | |
|---|
| 1067 | primaryjoin, secondaryjoin, secondary = self.primaryjoin, self.secondaryjoin, self.secondary |
|---|
| 1068 | |
|---|
| 1069 | # adjust the join condition for single table inheritance, |
|---|
| 1070 | # in the case that the join is to a subclass |
|---|
| 1071 | # this is analgous to the "_adjust_for_single_table_inheritance()" |
|---|
| 1072 | # method in Query. |
|---|
| 1073 | |
|---|
| 1074 | dest_mapper = of_type or self.mapper |
|---|
| 1075 | |
|---|
| 1076 | single_crit = dest_mapper._single_table_criterion |
|---|
| 1077 | if single_crit: |
|---|
| 1078 | if secondaryjoin: |
|---|
| 1079 | secondaryjoin = secondaryjoin & single_crit |
|---|
| 1080 | else: |
|---|
| 1081 | primaryjoin = primaryjoin & single_crit |
|---|
| 1082 | |
|---|
| 1083 | |
|---|
| 1084 | if aliased: |
|---|
| 1085 | if secondary: |
|---|
| 1086 | secondary = secondary.alias() |
|---|
| 1087 | primary_aliasizer = ClauseAdapter(secondary) |
|---|
| 1088 | if dest_selectable: |
|---|
| 1089 | secondary_aliasizer = ClauseAdapter(dest_selectable, equivalents=self.mapper._equivalent_columns).chain(primary_aliasizer) |
|---|
| 1090 | else: |
|---|
| 1091 | secondary_aliasizer = primary_aliasizer |
|---|
| 1092 | |
|---|
| 1093 | if source_selectable: |
|---|
| 1094 | primary_aliasizer = ClauseAdapter(secondary).chain(ClauseAdapter(source_selectable, equivalents=self.parent._equivalent_columns)) |
|---|
| 1095 | |
|---|
| 1096 | secondaryjoin = secondary_aliasizer.traverse(secondaryjoin) |
|---|
| 1097 | else: |
|---|
| 1098 | if dest_selectable: |
|---|
| 1099 | primary_aliasizer = ClauseAdapter(dest_selectable, exclude=self.local_side, equivalents=self.mapper._equivalent_columns) |
|---|
| 1100 | if source_selectable: |
|---|
| 1101 | primary_aliasizer.chain(ClauseAdapter(source_selectable, exclude=self.remote_side, equivalents=self.parent._equivalent_columns)) |
|---|
| 1102 | elif source_selectable: |
|---|
| 1103 | primary_aliasizer = ClauseAdapter(source_selectable, exclude=self.remote_side, equivalents=self.parent._equivalent_columns) |
|---|
| 1104 | |
|---|
| 1105 | secondary_aliasizer = None |
|---|
| 1106 | |
|---|
| 1107 | primaryjoin = primary_aliasizer.traverse(primaryjoin) |
|---|
| 1108 | target_adapter = secondary_aliasizer or primary_aliasizer |
|---|
| 1109 | target_adapter.include = target_adapter.exclude = None |
|---|
| 1110 | else: |
|---|
| 1111 | target_adapter = None |
|---|
| 1112 | |
|---|
| 1113 | return (primaryjoin, secondaryjoin, |
|---|
| 1114 | (source_selectable or self.parent.local_table), |
|---|
| 1115 | (dest_selectable or self.mapper.local_table), secondary, target_adapter) |
|---|
| 1116 | |
|---|
| 1117 | def _get_join(self, parent, primary=True, secondary=True, polymorphic_parent=True): |
|---|
| 1118 | """deprecated. use primary_join_against(), secondary_join_against(), full_join_against()""" |
|---|
| 1119 | |
|---|
| 1120 | pj, sj, source, dest, secondarytable, adapter = self._create_joins(source_polymorphic=polymorphic_parent) |
|---|
| 1121 | |
|---|
| 1122 | if primary and secondary: |
|---|
| 1123 | return pj & sj |
|---|
| 1124 | elif primary: |
|---|
| 1125 | return pj |
|---|
| 1126 | elif secondary: |
|---|
| 1127 | return sj |
|---|
| 1128 | else: |
|---|
| 1129 | raise AssertionError("illegal condition") |
|---|
| 1130 | |
|---|
| 1131 | def register_dependencies(self, uowcommit): |
|---|
| 1132 | if not self.viewonly: |
|---|
| 1133 | self._dependency_processor.register_dependencies(uowcommit) |
|---|
| 1134 | |
|---|
| 1135 | def register_processors(self, uowcommit): |
|---|
| 1136 | if not self.viewonly: |
|---|
| 1137 | self._dependency_processor.register_processors(uowcommit) |
|---|
| 1138 | |
|---|
| 1139 | PropertyLoader = RelationProperty |
|---|
| 1140 | log.class_logger(RelationProperty) |
|---|
| 1141 | |
|---|
| 1142 | class BackRef(object): |
|---|
| 1143 | """Attached to a RelationProperty to indicate a complementary reverse relationship. |
|---|
| 1144 | |
|---|
| 1145 | Handles the job of creating the opposite RelationProperty according to configuration. |
|---|
| 1146 | |
|---|
| 1147 | Alternatively, two explicit RelationProperty objects can be associated bidirectionally |
|---|
| 1148 | using the back_populates keyword argument on each. |
|---|
| 1149 | |
|---|
| 1150 | """ |
|---|
| 1151 | |
|---|
| 1152 | def __init__(self, key, _prop=None, **kwargs): |
|---|
| 1153 | self.key = key |
|---|
| 1154 | self.kwargs = kwargs |
|---|
| 1155 | self.prop = _prop |
|---|
| 1156 | self.extension = attributes.GenericBackrefExtension(self.key) |
|---|
| 1157 | |
|---|
| 1158 | def compile(self, prop): |
|---|
| 1159 | if self.prop: |
|---|
| 1160 | return |
|---|
| 1161 | |
|---|
| 1162 | self.prop = prop |
|---|
| 1163 | |
|---|
| 1164 | mapper = prop.mapper.primary_mapper() |
|---|
| 1165 | if mapper._get_property(self.key, raiseerr=False) is None: |
|---|
| 1166 | if prop.secondary: |
|---|
| 1167 | pj = self.kwargs.pop('primaryjoin', prop.secondaryjoin) |
|---|
| 1168 | sj = self.kwargs.pop('secondaryjoin', prop.primaryjoin) |
|---|
| 1169 | else: |
|---|
| 1170 | pj = self.kwargs.pop('primaryjoin', prop.primaryjoin) |
|---|
| 1171 | sj = self.kwargs.pop('secondaryjoin', None) |
|---|
| 1172 | if sj: |
|---|
| 1173 | raise sa_exc.InvalidRequestError( |
|---|
| 1174 | "Can't assign 'secondaryjoin' on a backref against " |
|---|
| 1175 | "a non-secondary relation.") |
|---|
| 1176 | |
|---|
| 1177 | foreign_keys = self.kwargs.pop('foreign_keys', prop._foreign_keys) |
|---|
| 1178 | |
|---|
| 1179 | parent = prop.parent.primary_mapper() |
|---|
| 1180 | self.kwargs.setdefault('viewonly', prop.viewonly) |
|---|
| 1181 | self.kwargs.setdefault('post_update', prop.post_update) |
|---|
| 1182 | |
|---|
| 1183 | relation = RelationProperty(parent, prop.secondary, pj, sj, foreign_keys=foreign_keys, |
|---|
| 1184 | backref=BackRef(prop.key, _prop=prop), |
|---|
| 1185 | **self.kwargs) |
|---|
| 1186 | |
|---|
| 1187 | mapper._configure_property(self.key, relation); |
|---|
| 1188 | |
|---|
| 1189 | prop._add_reverse_property(self.key) |
|---|
| 1190 | |
|---|
| 1191 | else: |
|---|
| 1192 | raise sa_exc.ArgumentError("Error creating backref '%s' on relation '%s': " |
|---|
| 1193 | "property of that name exists on mapper '%s'" % (self.key, prop, mapper)) |
|---|
| 1194 | |
|---|
| 1195 | mapper.ColumnProperty = ColumnProperty |
|---|
| 1196 | mapper.SynonymProperty = SynonymProperty |
|---|
| 1197 | mapper.ComparableProperty = ComparableProperty |
|---|
| 1198 | mapper.RelationProperty = RelationProperty |
|---|
| 1199 | mapper.ConcreteInheritedProperty = ConcreteInheritedProperty |
|---|