codekingpro/portable-devtools
115k
1# sql/traversals.py
2# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7# mypy: allow-untyped-defs, allow-untyped-calls
8
9from __future__ import annotations
10
11from collections import deque
12import collections.abc as collections_abc
13import itertools
14from itertools import zip_longest
15import operator
16import typing
17from typing import Any
18from typing import Callable
19from typing import Deque
20from typing import Dict
21from typing import Iterable
22from typing import Optional
23from typing import Set
24from typing import Tuple
25from typing import Type
26
27from . import operators
28from .cache_key import HasCacheKey
29from .visitors import _TraverseInternalsType
30from .visitors import anon_map
31from .visitors import ExternallyTraversible
32from .visitors import HasTraversalDispatch
33from .visitors import HasTraverseInternals
34from .. import util
35from ..util import langhelpers
36from ..util.typing import Self
37
38
39SKIP_TRAVERSE = util.symbol("skip_traverse")
40COMPARE_FAILED = False
41COMPARE_SUCCEEDED = True
42
43
44def compare(obj1: Any, obj2: Any, **kw: Any) -> bool:
45 strategy: TraversalComparatorStrategy
46 if kw.get("use_proxies", False):
47 strategy = ColIdentityComparatorStrategy()
48 else:
49 strategy = TraversalComparatorStrategy()
50
51 return strategy.compare(obj1, obj2, **kw)
52
53
54def _preconfigure_traversals(target_hierarchy: Type[Any]) -> None:
55 for cls in util.walk_subclasses(target_hierarchy):
56 if hasattr(cls, "_generate_cache_attrs") and hasattr(
57 cls, "_traverse_internals"
58 ):
59 cls._generate_cache_attrs()
60 _copy_internals.generate_dispatch(
61 cls,
62 cls._traverse_internals,
63 "_generated_copy_internals_traversal",
64 )
65 _get_children.generate_dispatch(
66 cls,
67 cls._traverse_internals,
68 "_generated_get_children_traversal",
69 )
70
71
72class HasShallowCopy(HasTraverseInternals):
73 """attribute-wide operations that are useful for classes that use
74 __slots__ and therefore can't operate on their attributes in a dictionary.
75
76
77 """
78
79 __slots__ = ()
80
81 if typing.TYPE_CHECKING:
82
83 def _generated_shallow_copy_traversal(self, other: Self) -> None: ...
84
85 def _generated_shallow_from_dict_traversal(
86 self, d: Dict[str, Any]
87 ) -> None: ...
88
89 def _generated_shallow_to_dict_traversal(self) -> Dict[str, Any]: ...
90
91 @classmethod
92 def _generate_shallow_copy(
93 cls,
94 internal_dispatch: _TraverseInternalsType,
95 method_name: str,
96 ) -> Callable[[Self, Self], None]:
97 code = "\n".join(
98 f" other.{attrname} = self.{attrname}"
99 for attrname, _ in internal_dispatch
100 )
101 meth_text = f"def {method_name}(self, other):\n{code}\n"
102 return langhelpers._exec_code_in_env(meth_text, {}, method_name)
103
104 @classmethod
105 def _generate_shallow_to_dict(
106 cls,
107 internal_dispatch: _TraverseInternalsType,
108 method_name: str,
109 ) -> Callable[[Self], Dict[str, Any]]:
110 code = ",\n".join(
111 f" '{attrname}': self.{attrname}"
112 for attrname, _ in internal_dispatch
113 )
114 meth_text = f"def {method_name}(self):\n return {{{code}}}\n"
115 return langhelpers._exec_code_in_env(meth_text, {}, method_name)
116
117 @classmethod
118 def _generate_shallow_from_dict(
119 cls,
120 internal_dispatch: _TraverseInternalsType,
121 method_name: str,
122 ) -> Callable[[Self, Dict[str, Any]], None]:
123 code = "\n".join(
124 f" self.{attrname} = d['{attrname}']"
125 for attrname, _ in internal_dispatch
126 )
127 meth_text = f"def {method_name}(self, d):\n{code}\n"
128 return langhelpers._exec_code_in_env(meth_text, {}, method_name)
129
130 def _shallow_from_dict(self, d: Dict[str, Any]) -> None:
131 cls = self.__class__
132
133 shallow_from_dict: Callable[[HasShallowCopy, Dict[str, Any]], None]
134 try:
135 shallow_from_dict = cls.__dict__[
136 "_generated_shallow_from_dict_traversal"
137 ]
138 except KeyError:
139 shallow_from_dict = self._generate_shallow_from_dict(
140 cls._traverse_internals,
141 "_generated_shallow_from_dict_traversal",
142 )
143
144 cls._generated_shallow_from_dict_traversal = shallow_from_dict # type: ignore # noqa: E501
145
146 shallow_from_dict(self, d)
147
148 def _shallow_to_dict(self) -> Dict[str, Any]:
149 cls = self.__class__
150
151 shallow_to_dict: Callable[[HasShallowCopy], Dict[str, Any]]
152
153 try:
154 shallow_to_dict = cls.__dict__[
155 "_generated_shallow_to_dict_traversal"
156 ]
157 except KeyError:
158 shallow_to_dict = self._generate_shallow_to_dict(
159 cls._traverse_internals, "_generated_shallow_to_dict_traversal"
160 )
161
162 cls._generated_shallow_to_dict_traversal = shallow_to_dict # type: ignore # noqa: E501
163 return shallow_to_dict(self)
164
165 def _shallow_copy_to(self, other: Self) -> None:
166 cls = self.__class__
167
168 shallow_copy: Callable[[Self, Self], None]
169 try:
170 shallow_copy = cls.__dict__["_generated_shallow_copy_traversal"]
171 except KeyError:
172 shallow_copy = self._generate_shallow_copy(
173 cls._traverse_internals, "_generated_shallow_copy_traversal"
174 )
175
176 cls._generated_shallow_copy_traversal = shallow_copy # type: ignore # noqa: E501
177 shallow_copy(self, other)
178
179 def _clone(self, **kw: Any) -> Self:
180 """Create a shallow copy"""
181 c = self.__class__.__new__(self.__class__)
182 self._shallow_copy_to(c)
183 return c
184
185
186class GenerativeOnTraversal(HasShallowCopy):
187 """Supplies Generative behavior but making use of traversals to shallow
188 copy.
189
190 .. seealso::
191
192 :class:`sqlalchemy.sql.base.Generative`
193
194
195 """
196
197 __slots__ = ()
198
199 def _generate(self) -> Self:
200 cls = self.__class__
201 s = cls.__new__(cls)
202 self._shallow_copy_to(s)
203 return s
204
205
206def _clone(element, **kw):
207 return element._clone()
208
209
210class HasCopyInternals(HasTraverseInternals):
211 __slots__ = ()
212
213 def _clone(self, **kw):
214 raise NotImplementedError()
215
216 def _copy_internals(
217 self, *, omit_attrs: Iterable[str] = (), **kw: Any
218 ) -> None:
219 """Reassign internal elements to be clones of themselves.
220
221 Called during a copy-and-traverse operation on newly
222 shallow-copied elements to create a deep copy.
223
224 The given clone function should be used, which may be applying
225 additional transformations to the element (i.e. replacement
226 traversal, cloned traversal, annotations).
227
228 """
229
230 try:
231 traverse_internals = self._traverse_internals
232 except AttributeError:
233 # user-defined classes may not have a _traverse_internals
234 return
235
236 for attrname, obj, meth in _copy_internals.run_generated_dispatch(
237 self, traverse_internals, "_generated_copy_internals_traversal"
238 ):
239 if attrname in omit_attrs:
240 continue
241
242 if obj is not None:
243 result = meth(attrname, self, obj, **kw)
244 if result is not None:
245 setattr(self, attrname, result)
246
247
248class _CopyInternalsTraversal(HasTraversalDispatch):
249 """Generate a _copy_internals internal traversal dispatch for classes
250 with a _traverse_internals collection."""
251
252 def visit_clauseelement(
253 self, attrname, parent, element, clone=_clone, **kw
254 ):
255 return clone(element, **kw)
256
257 def visit_clauseelement_list(
258 self, attrname, parent, element, clone=_clone, **kw
259 ):
260 return [clone(clause, **kw) for clause in element]
261
262 def visit_clauseelement_tuple(
263 self, attrname, parent, element, clone=_clone, **kw
264 ):
265 return tuple([clone(clause, **kw) for clause in element])
266
267 def visit_executable_options(
268 self, attrname, parent, element, clone=_clone, **kw
269 ):
270 return tuple([clone(clause, **kw) for clause in element])
271
272 def visit_clauseelement_unordered_set(
273 self, attrname, parent, element, clone=_clone, **kw
274 ):
275 return {clone(clause, **kw) for clause in element}
276
277 def visit_clauseelement_tuples(
278 self, attrname, parent, element, clone=_clone, **kw
279 ):
280 return [
281 tuple(clone(tup_elem, **kw) for tup_elem in elem)
282 for elem in element
283 ]
284
285 def visit_string_clauseelement_dict(
286 self, attrname, parent, element, clone=_clone, **kw
287 ):
288 return {key: clone(value, **kw) for key, value in element.items()}
289
290 def visit_setup_join_tuple(
291 self, attrname, parent, element, clone=_clone, **kw
292 ):
293 return tuple(
294 (
295 clone(target, **kw) if target is not None else None,
296 clone(onclause, **kw) if onclause is not None else None,
297 clone(from_, **kw) if from_ is not None else None,
298 flags,
299 )
300 for (target, onclause, from_, flags) in element
301 )
302
303 def visit_memoized_select_entities(self, attrname, parent, element, **kw):
304 return self.visit_clauseelement_tuple(attrname, parent, element, **kw)
305
306 def visit_dml_ordered_values(
307 self, attrname, parent, element, clone=_clone, **kw
308 ):
309 # sequence of 2-tuples
310 return [
311 (
312 (
313 clone(key, **kw)
314 if hasattr(key, "__clause_element__")
315 else key
316 ),
317 clone(value, **kw),
318 )
319 for key, value in element
320 ]
321
322 def visit_dml_values(self, attrname, parent, element, clone=_clone, **kw):
323 return {
324 (
325 clone(key, **kw) if hasattr(key, "__clause_element__") else key
326 ): clone(value, **kw)
327 for key, value in element.items()
328 }
329
330 def visit_dml_multi_values(
331 self, attrname, parent, element, clone=_clone, **kw
332 ):
333 # sequence of sequences, each sequence contains a list/dict/tuple
334
335 def copy(elem):
336 if isinstance(elem, (list, tuple)):
337 return [
338 (
339 clone(value, **kw)
340 if hasattr(value, "__clause_element__")
341 else value
342 )
343 for value in elem
344 ]
345 elif isinstance(elem, dict):
346 return {
347 (
348 clone(key, **kw)
349 if hasattr(key, "__clause_element__")
350 else key
351 ): (
352 clone(value, **kw)
353 if hasattr(value, "__clause_element__")
354 else value
355 )
356 for key, value in elem.items()
357 }
358 else:
359 # TODO: use abc classes
360 assert False
361
362 return [
363 [copy(sub_element) for sub_element in sequence]
364 for sequence in element
365 ]
366
367 def visit_propagate_attrs(
368 self, attrname, parent, element, clone=_clone, **kw
369 ):
370 return element
371
372
373_copy_internals = _CopyInternalsTraversal()
374
375
376def _flatten_clauseelement(element):
377 while hasattr(element, "__clause_element__") and not getattr(
378 element, "is_clause_element", False
379 ):
380 element = element.__clause_element__()
381
382 return element
383
384
385class _GetChildrenTraversal(HasTraversalDispatch):
386 """Generate a _children_traversal internal traversal dispatch for classes
387 with a _traverse_internals collection."""
388
389 def visit_has_cache_key(self, element, **kw):
390 # the GetChildren traversal refers explicitly to ClauseElement
391 # structures. Within these, a plain HasCacheKey is not a
392 # ClauseElement, so don't include these.
393 return ()
394
395 def visit_clauseelement(self, element, **kw):
396 return (element,)
397
398 def visit_clauseelement_list(self, element, **kw):
399 return element
400
401 def visit_clauseelement_tuple(self, element, **kw):
402 return element
403
404 def visit_clauseelement_tuples(self, element, **kw):
405 return itertools.chain.from_iterable(element)
406
407 def visit_fromclause_canonical_column_collection(self, element, **kw):
408 return ()
409
410 def visit_string_clauseelement_dict(self, element, **kw):
411 return element.values()
412
413 def visit_fromclause_ordered_set(self, element, **kw):
414 return element
415
416 def visit_clauseelement_unordered_set(self, element, **kw):
417 return element
418
419 def visit_setup_join_tuple(self, element, **kw):
420 for target, onclause, from_, flags in element:
421 if from_ is not None:
422 yield from_
423
424 if not isinstance(target, str):
425 yield _flatten_clauseelement(target)
426
427 if onclause is not None and not isinstance(onclause, str):
428 yield _flatten_clauseelement(onclause)
429
430 def visit_memoized_select_entities(self, element, **kw):
431 return self.visit_clauseelement_tuple(element, **kw)
432
433 def visit_dml_ordered_values(self, element, **kw):
434 for k, v in element:
435 if hasattr(k, "__clause_element__"):
436 yield k
437 yield v
438
439 def visit_dml_values(self, element, **kw):
440 expr_values = {k for k in element if hasattr(k, "__clause_element__")}
441 str_values = expr_values.symmetric_difference(element)
442
443 for k in sorted(str_values):
444 yield element[k]
445 for k in expr_values:
446 yield k
447 yield element[k]
448
449 def visit_dml_multi_values(self, element, **kw):
450 return ()
451
452 def visit_propagate_attrs(self, element, **kw):
453 return ()
454
455
456_get_children = _GetChildrenTraversal()
457
458
459@util.preload_module("sqlalchemy.sql.elements")
460def _resolve_name_for_compare(element, name, anon_map, **kw):
461 if isinstance(name, util.preloaded.sql_elements._anonymous_label):
462 name = name.apply_map(anon_map)
463
464 return name
465
466
467class TraversalComparatorStrategy(HasTraversalDispatch, util.MemoizedSlots):
468 __slots__ = "stack", "cache", "anon_map"
469
470 def __init__(self):
471 self.stack: Deque[
472 Tuple[
473 Optional[ExternallyTraversible],
474 Optional[ExternallyTraversible],
475 ]
476 ] = deque()
477 self.cache = set()
478
479 def _memoized_attr_anon_map(self):
480 return (anon_map(), anon_map())
481
482 def compare(
483 self,
484 obj1: ExternallyTraversible,
485 obj2: ExternallyTraversible,
486 **kw: Any,
487 ) -> bool:
488 stack = self.stack
489 cache = self.cache
490
491 compare_annotations = kw.get("compare_annotations", False)
492
493 stack.append((obj1, obj2))
494
495 while stack:
496 left, right = stack.popleft()
497
498 if left is right:
499 continue
500 elif left is None or right is None:
501 # we know they are different so no match
502 return False
503 elif (left, right) in cache:
504 continue
505 cache.add((left, right))
506
507 visit_name = left.__visit_name__
508 if visit_name != right.__visit_name__:
509 return False
510
511 meth = getattr(self, "compare_%s" % visit_name, None)
512
513 if meth:
514 attributes_compared = meth(left, right, **kw)
515 if attributes_compared is COMPARE_FAILED:
516 return False
517 elif attributes_compared is SKIP_TRAVERSE:
518 continue
519
520 # attributes_compared is returned as a list of attribute
521 # names that were "handled" by the comparison method above.
522 # remaining attribute names in the _traverse_internals
523 # will be compared.
524 else:
525 attributes_compared = ()
526
527 for (
528 (left_attrname, left_visit_sym),
529 (right_attrname, right_visit_sym),
530 ) in zip_longest(
531 left._traverse_internals,
532 right._traverse_internals,
533 fillvalue=(None, None),
534 ):
535 if not compare_annotations and (
536 (left_attrname == "_annotations")
537 or (right_attrname == "_annotations")
538 ):
539 continue
540
541 if (
542 left_attrname != right_attrname
543 or left_visit_sym is not right_visit_sym
544 ):
545 return False
546 elif left_attrname in attributes_compared:
547 continue
548
549 assert left_visit_sym is not None
550 assert left_attrname is not None
551 assert right_attrname is not None
552
553 dispatch = self.dispatch(left_visit_sym)
554 assert dispatch is not None, (
555 f"{self.__class__} has no dispatch for "
556 f"'{self._dispatch_lookup[left_visit_sym]}'"
557 )
558 left_child = operator.attrgetter(left_attrname)(left)
559 right_child = operator.attrgetter(right_attrname)(right)
560 if left_child is None:
561 if right_child is not None:
562 return False
563 else:
564 continue
565
566 comparison = dispatch(
567 left_attrname, left, left_child, right, right_child, **kw
568 )
569 if comparison is COMPARE_FAILED:
570 return False
571
572 return True
573
574 def compare_inner(self, obj1, obj2, **kw):
575 comparator = self.__class__()
576 return comparator.compare(obj1, obj2, **kw)
577
578 def visit_has_cache_key(
579 self, attrname, left_parent, left, right_parent, right, **kw
580 ):
581 if left._gen_cache_key(self.anon_map[0], []) != right._gen_cache_key(
582 self.anon_map[1], []
583 ):
584 return COMPARE_FAILED
585
586 def visit_propagate_attrs(
587 self, attrname, left_parent, left, right_parent, right, **kw
588 ):
589 return self.compare_inner(
590 left.get("plugin_subject", None), right.get("plugin_subject", None)
591 )
592
593 def visit_has_cache_key_list(
594 self, attrname, left_parent, left, right_parent, right, **kw
595 ):
596 for l, r in zip_longest(left, right, fillvalue=None):
597 if l is None:
598 if r is not None:
599 return COMPARE_FAILED
600 else:
601 continue
602 elif r is None:
603 return COMPARE_FAILED
604
605 if l._gen_cache_key(self.anon_map[0], []) != r._gen_cache_key(
606 self.anon_map[1], []
607 ):
608 return COMPARE_FAILED
609
610 def visit_executable_options(
611 self, attrname, left_parent, left, right_parent, right, **kw
612 ):
613 for l, r in zip_longest(left, right, fillvalue=None):
614 if l is None:
615 if r is not None:
616 return COMPARE_FAILED
617 else:
618 continue
619 elif r is None:
620 return COMPARE_FAILED
621
622 if (
623 l._gen_cache_key(self.anon_map[0], [])
624 if l._is_has_cache_key
625 else l
626 ) != (
627 r._gen_cache_key(self.anon_map[1], [])
628 if r._is_has_cache_key
629 else r
630 ):
631 return COMPARE_FAILED
632
633 def visit_clauseelement(
634 self, attrname, left_parent, left, right_parent, right, **kw
635 ):
636 self.stack.append((left, right))
637
638 def visit_fromclause_canonical_column_collection(
639 self, attrname, left_parent, left, right_parent, right, **kw
640 ):
641 for lcol, rcol in zip_longest(left, right, fillvalue=None):
642 self.stack.append((lcol, rcol))
643
644 def visit_fromclause_derived_column_collection(
645 self, attrname, left_parent, left, right_parent, right, **kw
646 ):
647 pass
648
649 def visit_string_clauseelement_dict(
650 self, attrname, left_parent, left, right_parent, right, **kw
651 ):
652 for lstr, rstr in zip_longest(
653 sorted(left), sorted(right), fillvalue=None
654 ):
655 if lstr != rstr:
656 return COMPARE_FAILED
657 self.stack.append((left[lstr], right[rstr]))
658
659 def visit_clauseelement_tuples(
660 self, attrname, left_parent, left, right_parent, right, **kw
661 ):
662 for ltup, rtup in zip_longest(left, right, fillvalue=None):
663 if ltup is None or rtup is None:
664 return COMPARE_FAILED
665
666 for l, r in zip_longest(ltup, rtup, fillvalue=None):
667 self.stack.append((l, r))
668
669 def visit_clauseelement_list(
670 self, attrname, left_parent, left, right_parent, right, **kw
671 ):
672 for l, r in zip_longest(left, right, fillvalue=None):
673 self.stack.append((l, r))
674
675 def visit_clauseelement_tuple(
676 self, attrname, left_parent, left, right_parent, right, **kw
677 ):
678 for l, r in zip_longest(left, right, fillvalue=None):
679 self.stack.append((l, r))
680
681 def _compare_unordered_sequences(self, seq1, seq2, **kw):
682 if seq1 is None:
683 return seq2 is None
684
685 completed: Set[object] = set()
686 for clause in seq1:
687 for other_clause in set(seq2).difference(completed):
688 if self.compare_inner(clause, other_clause, **kw):
689 completed.add(other_clause)
690 break
691 return len(completed) == len(seq1) == len(seq2)
692
693 def visit_clauseelement_unordered_set(
694 self, attrname, left_parent, left, right_parent, right, **kw
695 ):
696 return self._compare_unordered_sequences(left, right, **kw)
697
698 def visit_fromclause_ordered_set(
699 self, attrname, left_parent, left, right_parent, right, **kw
700 ):
701 for l, r in zip_longest(left, right, fillvalue=None):
702 self.stack.append((l, r))
703
704 def visit_string(
705 self, attrname, left_parent, left, right_parent, right, **kw
706 ):
707 return left == right
708
709 def visit_string_list(
710 self, attrname, left_parent, left, right_parent, right, **kw
711 ):
712 return left == right
713
714 def visit_string_multi_dict(
715 self, attrname, left_parent, left, right_parent, right, **kw
716 ):
717 for lk, rk in zip_longest(
718 sorted(left.keys()), sorted(right.keys()), fillvalue=(None, None)
719 ):
720 if lk != rk:
721 return COMPARE_FAILED
722
723 lv, rv = left[lk], right[rk]
724
725 lhc = isinstance(left, HasCacheKey)
726 rhc = isinstance(right, HasCacheKey)
727 if lhc and rhc:
728 if lv._gen_cache_key(
729 self.anon_map[0], []
730 ) != rv._gen_cache_key(self.anon_map[1], []):
731 return COMPARE_FAILED
732 elif lhc != rhc:
733 return COMPARE_FAILED
734 elif lv != rv:
735 return COMPARE_FAILED
736
737 def visit_multi(
738 self, attrname, left_parent, left, right_parent, right, **kw
739 ):
740 lhc = isinstance(left, HasCacheKey)
741 rhc = isinstance(right, HasCacheKey)
742 if lhc and rhc:
743 if left._gen_cache_key(
744 self.anon_map[0], []
745 ) != right._gen_cache_key(self.anon_map[1], []):
746 return COMPARE_FAILED
747 elif lhc != rhc:
748 return COMPARE_FAILED
749 else:
750 return left == right
751
752 def visit_anon_name(
753 self, attrname, left_parent, left, right_parent, right, **kw
754 ):
755 return _resolve_name_for_compare(
756 left_parent, left, self.anon_map[0], **kw
757 ) == _resolve_name_for_compare(
758 right_parent, right, self.anon_map[1], **kw
759 )
760
761 def visit_boolean(
762 self, attrname, left_parent, left, right_parent, right, **kw
763 ):
764 return left == right
765
766 def visit_operator(
767 self, attrname, left_parent, left, right_parent, right, **kw
768 ):
769 return left == right
770
771 def visit_type(
772 self, attrname, left_parent, left, right_parent, right, **kw
773 ):
774 return left._compare_type_affinity(right)
775
776 def visit_plain_dict(
777 self, attrname, left_parent, left, right_parent, right, **kw
778 ):
779 return left == right
780
781 def visit_dialect_options(
782 self, attrname, left_parent, left, right_parent, right, **kw
783 ):
784 return left == right
785
786 def visit_annotations_key(
787 self, attrname, left_parent, left, right_parent, right, **kw
788 ):
789 if left and right:
790 return (
791 left_parent._annotations_cache_key
792 == right_parent._annotations_cache_key
793 )
794 else:
795 return left == right
796
797 def visit_with_context_options(
798 self, attrname, left_parent, left, right_parent, right, **kw
799 ):
800 return tuple((fn.__code__, c_key) for fn, c_key in left) == tuple(
801 (fn.__code__, c_key) for fn, c_key in right
802 )
803
804 def visit_plain_obj(
805 self, attrname, left_parent, left, right_parent, right, **kw
806 ):
807 return left == right
808
809 def visit_named_ddl_element(
810 self, attrname, left_parent, left, right_parent, right, **kw
811 ):
812 if left is None:
813 if right is not None:
814 return COMPARE_FAILED
815
816 return left.name == right.name
817
818 def visit_prefix_sequence(
819 self, attrname, left_parent, left, right_parent, right, **kw
820 ):
821 for (l_clause, l_str), (r_clause, r_str) in zip_longest(
822 left, right, fillvalue=(None, None)
823 ):
824 if l_str != r_str:
825 return COMPARE_FAILED
826 else:
827 self.stack.append((l_clause, r_clause))
828
829 def visit_setup_join_tuple(
830 self, attrname, left_parent, left, right_parent, right, **kw
831 ):
832 # TODO: look at attrname for "legacy_join" and use different structure
833 for (
834 (l_target, l_onclause, l_from, l_flags),
835 (r_target, r_onclause, r_from, r_flags),
836 ) in zip_longest(left, right, fillvalue=(None, None, None, None)):
837 if l_flags != r_flags:
838 return COMPARE_FAILED
839 self.stack.append((l_target, r_target))
840 self.stack.append((l_onclause, r_onclause))
841 self.stack.append((l_from, r_from))
842
843 def visit_memoized_select_entities(
844 self, attrname, left_parent, left, right_parent, right, **kw
845 ):
846 return self.visit_clauseelement_tuple(
847 attrname, left_parent, left, right_parent, right, **kw
848 )
849
850 def visit_table_hint_list(
851 self, attrname, left_parent, left, right_parent, right, **kw
852 ):
853 left_keys = sorted(left, key=lambda elem: (elem[0].fullname, elem[1]))
854 right_keys = sorted(
855 right, key=lambda elem: (elem[0].fullname, elem[1])
856 )
857 for (ltable, ldialect), (rtable, rdialect) in zip_longest(
858 left_keys, right_keys, fillvalue=(None, None)
859 ):
860 if ldialect != rdialect:
861 return COMPARE_FAILED
862 elif left[(ltable, ldialect)] != right[(rtable, rdialect)]:
863 return COMPARE_FAILED
864 else:
865 self.stack.append((ltable, rtable))
866
867 def visit_statement_hint_list(
868 self, attrname, left_parent, left, right_parent, right, **kw
869 ):
870 return left == right
871
872 def visit_unknown_structure(
873 self, attrname, left_parent, left, right_parent, right, **kw
874 ):
875 raise NotImplementedError()
876
877 def visit_dml_ordered_values(
878 self, attrname, left_parent, left, right_parent, right, **kw
879 ):
880 # sequence of tuple pairs
881
882 for (lk, lv), (rk, rv) in zip_longest(
883 left, right, fillvalue=(None, None)
884 ):
885 if not self._compare_dml_values_or_ce(lk, rk, **kw):
886 return COMPARE_FAILED
887
888 def _compare_dml_values_or_ce(self, lv, rv, **kw):
889 lvce = hasattr(lv, "__clause_element__")
890 rvce = hasattr(rv, "__clause_element__")
891 if lvce != rvce:
892 return False
893 elif lvce and not self.compare_inner(lv, rv, **kw):
894 return False
895 elif not lvce and lv != rv:
896 return False
897 elif not self.compare_inner(lv, rv, **kw):
898 return False
899
900 return True
901
902 def visit_dml_values(
903 self, attrname, left_parent, left, right_parent, right, **kw
904 ):
905 if left is None or right is None or len(left) != len(right):
906 return COMPARE_FAILED
907
908 if isinstance(left, collections_abc.Sequence):
909 for lv, rv in zip(left, right):
910 if not self._compare_dml_values_or_ce(lv, rv, **kw):
911 return COMPARE_FAILED
912 elif isinstance(right, collections_abc.Sequence):
913 return COMPARE_FAILED
914 else:
915 # dictionaries guaranteed to support insert ordering in
916 # py37 so that we can compare the keys in order. without
917 # this, we can't compare SQL expression keys because we don't
918 # know which key is which
919 for (lk, lv), (rk, rv) in zip(left.items(), right.items()):
920 if not self._compare_dml_values_or_ce(lk, rk, **kw):
921 return COMPARE_FAILED
922 if not self._compare_dml_values_or_ce(lv, rv, **kw):
923 return COMPARE_FAILED
924
925 def visit_dml_multi_values(
926 self, attrname, left_parent, left, right_parent, right, **kw
927 ):
928 for lseq, rseq in zip_longest(left, right, fillvalue=None):
929 if lseq is None or rseq is None:
930 return COMPARE_FAILED
931
932 for ld, rd in zip_longest(lseq, rseq, fillvalue=None):
933 if (
934 self.visit_dml_values(
935 attrname, left_parent, ld, right_parent, rd, **kw
936 )
937 is COMPARE_FAILED
938 ):
939 return COMPARE_FAILED
940
941 def compare_expression_clauselist(self, left, right, **kw):
942 if left.operator is right.operator:
943 if operators.is_associative(left.operator):
944 if self._compare_unordered_sequences(
945 left.clauses, right.clauses, **kw
946 ):
947 return ["operator", "clauses"]
948 else:
949 return COMPARE_FAILED
950 else:
951 return ["operator"]
952 else:
953 return COMPARE_FAILED
954
955 def compare_clauselist(self, left, right, **kw):
956 return self.compare_expression_clauselist(left, right, **kw)
957
958 def compare_binary(self, left, right, **kw):
959 if left.operator == right.operator:
960 if operators.is_commutative(left.operator):
961 if (
962 self.compare_inner(left.left, right.left, **kw)
963 and self.compare_inner(left.right, right.right, **kw)
964 ) or (
965 self.compare_inner(left.left, right.right, **kw)
966 and self.compare_inner(left.right, right.left, **kw)
967 ):
968 return ["operator", "negate", "left", "right"]
969 else:
970 return COMPARE_FAILED
971 else:
972 return ["operator", "negate"]
973 else:
974 return COMPARE_FAILED
975
976 def compare_bindparam(self, left, right, **kw):
977 compare_keys = kw.pop("compare_keys", True)
978 compare_values = kw.pop("compare_values", True)
979
980 if compare_values:
981 omit = []
982 else:
983 # this means, "skip these, we already compared"
984 omit = ["callable", "value"]
985
986 if not compare_keys:
987 omit.append("key")
988
989 return omit
990
991
992class ColIdentityComparatorStrategy(TraversalComparatorStrategy):
993 def compare_column_element(
994 self, left, right, use_proxies=True, equivalents=(), **kw
995 ):
996 """Compare ColumnElements using proxies and equivalent collections.
997
998 This is a comparison strategy specific to the ORM.
999 """
1000
1001 to_compare = (right,)
1002 if equivalents and right in equivalents:
1003 to_compare = equivalents[right].union(to_compare)
1004
1005 for oth in to_compare:
1006 if use_proxies and left.shares_lineage(oth):
1007 return SKIP_TRAVERSE
1008 elif hash(left) == hash(right):
1009 return SKIP_TRAVERSE
1010 else:
1011 return COMPARE_FAILED
1012
1013 def compare_column(self, left, right, **kw):
1014 return self.compare_column_element(left, right, **kw)
1015
1016 def compare_label(self, left, right, **kw):
1017 return self.compare_column_element(left, right, **kw)
1018
1019 def compare_table(self, left, right, **kw):
1020 # tables compare on identity, since it's not really feasible to
1021 # compare them column by column with the above rules
1022 return SKIP_TRAVERSE if left is right else COMPARE_FAILED
1023 