1from collections import OrderedDict
2from functools import partial
3from inspect import isclass
4from operator import attrgetter
5
6import sqlalchemy as sa
7from sqlalchemy.engine.interfaces import Dialect
8from sqlalchemy.ext.hybrid import hybrid_property
9from sqlalchemy.orm import ColumnProperty, mapperlib, RelationshipProperty
10from sqlalchemy.orm.attributes import InstrumentedAttribute
11from sqlalchemy.orm.exc import UnmappedInstanceError
12
13from sqlalchemy.orm.context import _ColumnEntity, _MapperEntity
14
15from sqlalchemy.orm.session import object_session
16from sqlalchemy.orm.util import AliasedInsp
17
18from ..utils import is_sequence
19
20
21def get_class_by_table(base, table, data=None):
22 """
23 Return declarative class associated with given table. If no class is found
24 this function returns `None`. If multiple classes were found (polymorphic
25 cases) additional `data` parameter can be given to hint which class
26 to return.
27
28 ::
29
30 class User(Base):
31 __tablename__ = 'entity'
32 id = sa.Column(sa.Integer, primary_key=True)
33 name = sa.Column(sa.String)
34
35
36 get_class_by_table(Base, User.__table__) # User class
37
38
39 This function also supports models using single table inheritance.
40 Additional data paratemer should be provided in these case.
41
42 ::
43
44 class Entity(Base):
45 __tablename__ = 'entity'
46 id = sa.Column(sa.Integer, primary_key=True)
47 name = sa.Column(sa.String)
48 type = sa.Column(sa.String)
49 __mapper_args__ = {
50 'polymorphic_on': type,
51 'polymorphic_identity': 'entity'
52 }
53
54 class User(Entity):
55 __mapper_args__ = {
56 'polymorphic_identity': 'user'
57 }
58
59
60 # Entity class
61 get_class_by_table(Base, Entity.__table__, {'type': 'entity'})
62
63 # User class
64 get_class_by_table(Base, Entity.__table__, {'type': 'user'})
65
66
67 :param base: Declarative model base
68 :param table: SQLAlchemy Table object
69 :param data: Data row to determine the class in polymorphic scenarios
70 :return: Declarative class or None.
71 """
72 found_classes = {
73 c
74 for c in _get_class_registry(base).values()
75 if hasattr(c, '__table__') and c.__table__ is table
76 }
77 if len(found_classes) > 1:
78 if not data:
79 raise ValueError(
80 "Multiple declarative classes found for table '{}'. "
81 'Please provide data parameter for this function to be able '
82 'to determine polymorphic scenarios.'.format(table.name)
83 )
84 else:
85 for cls in found_classes:
86 mapper = sa.inspect(cls)
87 polymorphic_on = mapper.polymorphic_on.name
88 if polymorphic_on in data:
89 if data[polymorphic_on] == mapper.polymorphic_identity:
90 return cls
91 raise ValueError(
92 "Multiple declarative classes found for table '{}'. Given "
93 'data row does not match any polymorphic identity of the '
94 'found classes.'.format(table.name)
95 )
96 elif found_classes:
97 return found_classes.pop()
98 return None
99
100
101def get_type(expr):
102 """
103 Return the associated type with given Column, InstrumentedAttribute,
104 ColumnProperty, RelationshipProperty or other similar SQLAlchemy construct.
105
106 For constructs wrapping columns this is the column type. For relationships
107 this function returns the relationship mapper class.
108
109 :param expr:
110 SQLAlchemy Column, InstrumentedAttribute, ColumnProperty or other
111 similar SA construct.
112
113 ::
114
115 class User(Base):
116 __tablename__ = 'user'
117 id = sa.Column(sa.Integer, primary_key=True)
118 name = sa.Column(sa.String)
119
120
121 class Article(Base):
122 __tablename__ = 'article'
123 id = sa.Column(sa.Integer, primary_key=True)
124 author_id = sa.Column(sa.Integer, sa.ForeignKey(User.id))
125 author = sa.orm.relationship(User)
126
127
128 get_type(User.__table__.c.name) # sa.String()
129 get_type(User.name) # sa.String()
130 get_type(User.name.property) # sa.String()
131
132 get_type(Article.author) # User
133
134
135 .. versionadded: 0.30.9
136 """
137 if hasattr(expr, 'type'):
138 return expr.type
139 elif isinstance(expr, InstrumentedAttribute):
140 expr = expr.property
141
142 if isinstance(expr, ColumnProperty):
143 return expr.columns[0].type
144 elif isinstance(expr, RelationshipProperty):
145 return expr.mapper.class_
146 raise TypeError("Couldn't inspect type.")
147
148
149def cast_if(expression, type_):
150 """
151 Produce a CAST expression but only if given expression is not of given type
152 already.
153
154 Assume we have a model with two fields id (Integer) and name (String).
155
156 ::
157
158 import sqlalchemy as sa
159 from sqlalchemy_utils import cast_if
160
161
162 cast_if(User.id, sa.Integer) # "user".id
163 cast_if(User.name, sa.String) # "user".name
164 cast_if(User.id, sa.String) # CAST("user".id AS TEXT)
165
166
167 This function supports scalar values as well.
168
169 ::
170
171 cast_if(1, sa.Integer) # 1
172 cast_if('text', sa.String) # 'text'
173 cast_if(1, sa.String) # CAST(1 AS TEXT)
174
175
176 :param expression:
177 A SQL expression, such as a ColumnElement expression or a Python string
178 which will be coerced into a bound literal value.
179 :param type_:
180 A TypeEngine class or instance indicating the type to which the CAST
181 should apply.
182
183 .. versionadded: 0.30.14
184 """
185 try:
186 expr_type = get_type(expression)
187 except TypeError:
188 expr_type = expression
189 check_type = type_().python_type
190 else:
191 check_type = type_
192
193 return (
194 sa.cast(expression, type_)
195 if not isinstance(expr_type, check_type)
196 else expression
197 )
198
199
200def get_column_key(model, column):
201 """
202 Return the key for given column in given model.
203
204 :param model: SQLAlchemy declarative model object
205
206 ::
207
208 class User(Base):
209 __tablename__ = 'user'
210 id = sa.Column(sa.Integer, primary_key=True)
211 name = sa.Column('_name', sa.String)
212
213
214 get_column_key(User, User.__table__.c._name) # 'name'
215
216 .. versionadded: 0.26.5
217
218 .. versionchanged: 0.27.11
219 Throws UnmappedColumnError instead of ValueError when no property was
220 found for given column. This is consistent with how SQLAlchemy works.
221 """
222 mapper = sa.inspect(model)
223 try:
224 return mapper.get_property_by_column(column).key
225 except sa.orm.exc.UnmappedColumnError:
226 for key, c in mapper.columns.items():
227 if c.name == column.name and c.table is column.table:
228 return key
229 raise sa.orm.exc.UnmappedColumnError(
230 f'No column {column} is configured on mapper {mapper}...'
231 )
232
233
234def get_mapper(mixed):
235 """
236 Return related SQLAlchemy Mapper for given SQLAlchemy object.
237
238 :param mixed: SQLAlchemy Table / Alias / Mapper / declarative model object
239
240 ::
241
242 from sqlalchemy_utils import get_mapper
243
244
245 get_mapper(User)
246
247 get_mapper(User())
248
249 get_mapper(User.__table__)
250
251 get_mapper(User.__mapper__)
252
253 get_mapper(sa.orm.aliased(User))
254
255 get_mapper(sa.orm.aliased(User.__table__))
256
257
258 Raises:
259 ValueError: if multiple mappers were found for given argument
260
261 .. versionadded: 0.26.1
262 """
263 if isinstance(mixed, _MapperEntity):
264 mixed = mixed.expr
265 elif isinstance(mixed, sa.Column):
266 mixed = mixed.table
267 elif isinstance(mixed, _ColumnEntity):
268 mixed = mixed.expr
269
270 if isinstance(mixed, sa.orm.Mapper):
271 return mixed
272 if isinstance(mixed, sa.orm.util.AliasedClass):
273 return sa.inspect(mixed).mapper
274 if isinstance(mixed, sa.sql.selectable.Alias):
275 mixed = mixed.element
276 if isinstance(mixed, AliasedInsp):
277 return mixed.mapper
278 if isinstance(mixed, sa.orm.attributes.InstrumentedAttribute):
279 mixed = mixed.class_
280 if isinstance(mixed, sa.Table):
281 all_mappers = set()
282 for mapper_registry in mapperlib._all_registries():
283 all_mappers.update(mapper_registry.mappers)
284 mappers = [mapper for mapper in all_mappers if mixed in mapper.tables]
285 if len(mappers) > 1:
286 raise ValueError("Multiple mappers found for table '%s'." % mixed.name)
287 elif not mappers:
288 raise ValueError("Could not get mapper for table '%s'." % mixed.name)
289 else:
290 return mappers[0]
291 if not isclass(mixed):
292 mixed = type(mixed)
293 return sa.inspect(mixed)
294
295
296def get_bind(obj):
297 """
298 Return the bind for given SQLAlchemy Engine / Connection / declarative
299 model object.
300
301 :param obj: SQLAlchemy Engine / Connection / declarative model object
302
303 ::
304
305 from sqlalchemy_utils import get_bind
306
307
308 get_bind(session) # Connection object
309
310 get_bind(user)
311
312 """
313 if hasattr(obj, 'bind'):
314 conn = obj.bind
315 else:
316 try:
317 conn = object_session(obj).bind
318 except UnmappedInstanceError:
319 conn = obj
320
321 if not hasattr(conn, 'execute'):
322 raise TypeError(
323 'This method accepts only Session, Engine, Connection and '
324 'declarative model objects.'
325 )
326 return conn
327
328
329def get_primary_keys(mixed):
330 """
331 Return an OrderedDict of all primary keys for given Table object,
332 declarative class or declarative class instance.
333
334 :param mixed:
335 SA Table object, SA declarative class or SA declarative class instance
336
337 ::
338
339 get_primary_keys(User)
340
341 get_primary_keys(User())
342
343 get_primary_keys(User.__table__)
344
345 get_primary_keys(User.__mapper__)
346
347 get_primary_keys(sa.orm.aliased(User))
348
349 get_primary_keys(sa.orm.aliased(User.__table__))
350
351
352 .. versionchanged: 0.25.3
353 Made the function return an ordered dictionary instead of generator.
354 This change was made to support primary key aliases.
355
356 Renamed this function to 'get_primary_keys', formerly 'primary_keys'
357
358 .. seealso:: :func:`get_columns`
359 """
360 return OrderedDict(
361 (
362 (key, column)
363 for key, column in get_columns(mixed).items()
364 if column.primary_key
365 )
366 )
367
368
369def get_tables(mixed):
370 """
371 Return a set of tables associated with given SQLAlchemy object.
372
373 Let's say we have three classes which use joined table inheritance
374 TextItem, Article and BlogPost. Article and BlogPost inherit TextItem.
375
376 ::
377
378 get_tables(Article) # set([Table('article', ...), Table('text_item')])
379
380 get_tables(Article())
381
382 get_tables(Article.__mapper__)
383
384
385 If the TextItem entity is using with_polymorphic='*' then this function
386 returns all child tables (article and blog_post) as well.
387
388 ::
389
390
391 get_tables(TextItem) # set([Table('text_item', ...)], ...])
392
393
394 .. versionadded: 0.26.0
395
396 :param mixed:
397 SQLAlchemy Mapper, Declarative class, Column, InstrumentedAttribute or
398 a SA Alias object wrapping any of these objects.
399 """
400 if isinstance(mixed, sa.Table):
401 return [mixed]
402 elif isinstance(mixed, sa.Column):
403 return [mixed.table]
404 elif isinstance(mixed, sa.orm.attributes.InstrumentedAttribute):
405 return mixed.parent.tables
406 elif isinstance(mixed, _ColumnEntity):
407 mixed = mixed.expr
408
409 mapper = get_mapper(mixed)
410
411 polymorphic_mappers = get_polymorphic_mappers(mapper)
412 if polymorphic_mappers:
413 tables = sum((m.tables for m in polymorphic_mappers), [])
414 else:
415 tables = mapper.tables
416 return tables
417
418
419def get_columns(mixed):
420 """
421 Return a collection of all Column objects for given SQLAlchemy
422 object.
423
424 The type of the collection depends on the type of the object to return the
425 columns from.
426
427 ::
428
429 get_columns(User)
430
431 get_columns(User())
432
433 get_columns(User.__table__)
434
435 get_columns(User.__mapper__)
436
437 get_columns(sa.orm.aliased(User))
438
439 get_columns(sa.orm.alised(User.__table__))
440
441
442 :param mixed:
443 SA Table object, SA Mapper, SA declarative class, SA declarative class
444 instance or an alias of any of these objects
445 """
446 if isinstance(mixed, sa.SelectBase):
447 return mixed.subquery().c
448 if isinstance(mixed, sa.sql.selectable.Selectable):
449 return mixed.c
450 if isinstance(mixed, sa.orm.util.AliasedClass):
451 return sa.inspect(mixed).mapper.columns
452 if isinstance(mixed, sa.orm.Mapper):
453 return mixed.columns
454 if isinstance(mixed, InstrumentedAttribute):
455 return mixed.property.columns
456 if isinstance(mixed, ColumnProperty):
457 return mixed.columns
458 if isinstance(mixed, sa.Column):
459 return [mixed]
460 if not isclass(mixed):
461 mixed = mixed.__class__
462 return sa.inspect(mixed).columns
463
464
465def table_name(obj):
466 """
467 Return table name of given target, declarative class or the
468 table name where the declarative attribute is bound to.
469 """
470 class_ = getattr(obj, 'class_', obj)
471
472 try:
473 return class_.__tablename__
474 except AttributeError:
475 pass
476
477 try:
478 return class_.__table__.name
479 except AttributeError:
480 pass
481
482
483def getattrs(obj, attrs):
484 return map(partial(getattr, obj), attrs)
485
486
487def quote(mixed, ident):
488 """
489 Conditionally quote an identifier.
490 ::
491
492
493 from sqlalchemy_utils import quote
494
495
496 engine = create_engine('sqlite:///:memory:')
497
498 quote(engine, 'order')
499 # '"order"'
500
501 quote(engine, 'some_other_identifier')
502 # 'some_other_identifier'
503
504
505 :param mixed: SQLAlchemy Session / Connection / Engine / Dialect object.
506 :param ident: identifier to conditionally quote
507 """
508 if isinstance(mixed, Dialect):
509 dialect = mixed
510 else:
511 dialect = get_bind(mixed).dialect
512 return dialect.preparer(dialect).quote(ident)
513
514
515def _get_query_compile_state(query):
516 return query._compile_state()
517
518
519def get_polymorphic_mappers(mixed):
520 if isinstance(mixed, AliasedInsp):
521 return mixed.with_polymorphic_mappers
522 else:
523 return mixed.polymorphic_map.values()
524
525
526def get_descriptor(entity, attr):
527 mapper = sa.inspect(entity)
528
529 for key, descriptor in get_all_descriptors(mapper).items():
530 if attr == key:
531 prop = descriptor.property if hasattr(descriptor, 'property') else None
532 if isinstance(prop, ColumnProperty):
533 if isinstance(entity, sa.orm.util.AliasedClass):
534 for c in mapper.selectable.c:
535 if c.key == attr:
536 return c
537 else:
538 # If the property belongs to a class that uses
539 # polymorphic inheritance we have to take into account
540 # situations where the attribute exists in child class
541 # but not in parent class.
542 return getattr(prop.parent.class_, attr)
543 else:
544 # Handle synonyms, relationship properties and hybrid
545 # properties
546
547 if isinstance(entity, sa.orm.util.AliasedClass):
548 return getattr(entity, attr)
549 try:
550 return getattr(mapper.class_, attr)
551 except AttributeError:
552 pass
553
554
555def get_all_descriptors(expr):
556 if isinstance(expr, sa.sql.selectable.Selectable):
557 return expr.c
558 insp = sa.inspect(expr)
559 try:
560 polymorphic_mappers = get_polymorphic_mappers(insp)
561 except sa.exc.NoInspectionAvailable:
562 return get_mapper(expr).all_orm_descriptors
563 else:
564 attrs = dict(get_mapper(expr).all_orm_descriptors)
565 for submapper in polymorphic_mappers:
566 for key, descriptor in submapper.all_orm_descriptors.items():
567 if key not in attrs:
568 attrs[key] = descriptor
569 return attrs
570
571
572def get_hybrid_properties(model):
573 """
574 Returns a dictionary of hybrid property keys and hybrid properties for
575 given SQLAlchemy declarative model / mapper.
576
577
578 Consider the following model
579
580 ::
581
582
583 from sqlalchemy.ext.hybrid import hybrid_property
584
585
586 class Category(Base):
587 __tablename__ = 'category'
588 id = sa.Column(sa.Integer, primary_key=True)
589 name = sa.Column(sa.Unicode(255))
590
591 @hybrid_property
592 def lowercase_name(self):
593 return self.name.lower()
594
595 @lowercase_name.expression
596 def lowercase_name(cls):
597 return sa.func.lower(cls.name)
598
599
600 You can now easily get a list of all hybrid property names
601
602 ::
603
604
605 from sqlalchemy_utils import get_hybrid_properties
606
607
608 get_hybrid_properties(Category).keys() # ['lowercase_name']
609
610
611 This function also supports aliased classes
612
613 ::
614
615
616 get_hybrid_properties(
617 sa.orm.aliased(Category)
618 ).keys() # ['lowercase_name']
619
620
621 .. versionchanged: 0.26.7
622 This function now returns a dictionary instead of generator
623
624 .. versionchanged: 0.30.15
625 Added support for aliased classes
626
627 :param model: SQLAlchemy declarative model or mapper
628 """
629 return {
630 key: prop
631 for key, prop in get_mapper(model).all_orm_descriptors.items()
632 if isinstance(prop, hybrid_property)
633 }
634
635
636def get_declarative_base(model):
637 """
638 Returns the declarative base for given model class.
639
640 :param model: SQLAlchemy declarative model
641 """
642 for parent in model.__bases__:
643 try:
644 parent.metadata
645 return get_declarative_base(parent)
646 except AttributeError:
647 pass
648 return model
649
650
651def getdotattr(obj_or_class, dot_path, condition=None):
652 """
653 Allow dot-notated strings to be passed to `getattr`.
654
655 ::
656
657 getdotattr(SubSection, 'section.document')
658
659 getdotattr(subsection, 'section.document')
660
661
662 :param obj_or_class: Any object or class
663 :param dot_path: Attribute path with dot mark as separator
664 """
665 last = obj_or_class
666
667 for path in str(dot_path).split('.'):
668 getter = attrgetter(path)
669
670 if is_sequence(last):
671 tmp = []
672 for element in last:
673 value = getter(element)
674 if is_sequence(value):
675 tmp.extend(value)
676 else:
677 tmp.append(value)
678 last = tmp
679 elif isinstance(last, InstrumentedAttribute):
680 last = getter(last.property.mapper.class_)
681 elif last is None:
682 return None
683 else:
684 last = getter(last)
685 if condition is not None:
686 if is_sequence(last):
687 last = [v for v in last if condition(v)]
688 else:
689 if not condition(last):
690 return None
691
692 return last
693
694
695def is_deleted(obj):
696 return obj in sa.orm.object_session(obj).deleted
697
698
699def has_changes(obj, attrs=None, exclude=None):
700 """
701 Simple shortcut function for checking if given attributes of given
702 declarative model object have changed during the session. Without
703 parameters this checks if given object has any modificiations. Additionally
704 exclude parameter can be given to check if given object has any changes
705 in any attributes other than the ones given in exclude.
706
707
708 ::
709
710
711 from sqlalchemy_utils import has_changes
712
713
714 user = User()
715
716 has_changes(user, 'name') # False
717
718 user.name = 'someone'
719
720 has_changes(user, 'name') # True
721
722 has_changes(user) # True
723
724
725 You can check multiple attributes as well.
726 ::
727
728
729 has_changes(user, ['age']) # True
730
731 has_changes(user, ['name', 'age']) # True
732
733
734 This function also supports excluding certain attributes.
735
736 ::
737
738 has_changes(user, exclude=['name']) # False
739
740 has_changes(user, exclude=['age']) # True
741
742 .. versionchanged: 0.26.6
743 Added support for multiple attributes and exclude parameter.
744
745 :param obj: SQLAlchemy declarative model object
746 :param attrs: Names of the attributes
747 :param exclude: Names of the attributes to exclude
748 """
749 if attrs:
750 if isinstance(attrs, str):
751 return sa.inspect(obj).attrs.get(attrs).history.has_changes()
752 else:
753 return any(has_changes(obj, attr) for attr in attrs)
754 else:
755 if exclude is None:
756 exclude = []
757 return any(
758 attr.history.has_changes()
759 for key, attr in sa.inspect(obj).attrs.items()
760 if key not in exclude
761 )
762
763
764def is_loaded(obj, prop):
765 """
766 Return whether or not given property of given object has been loaded.
767
768 ::
769
770 class Article(Base):
771 __tablename__ = 'article'
772 id = sa.Column(sa.Integer, primary_key=True)
773 name = sa.Column(sa.String)
774 content = sa.orm.deferred(sa.Column(sa.String))
775
776
777 article = session.query(Article).get(5)
778
779 # name gets loaded since its not a deferred property
780 assert is_loaded(article, 'name')
781
782 # content has not yet been loaded since its a deferred property
783 assert not is_loaded(article, 'content')
784
785
786 .. versionadded: 0.27.8
787
788 :param obj: SQLAlchemy declarative model object
789 :param prop: Name of the property or InstrumentedAttribute
790 """
791 return prop not in sa.inspect(obj).unloaded
792
793
794def identity(obj_or_class):
795 """
796 Return the identity of given sqlalchemy declarative model class or instance
797 as a tuple. This differs from obj._sa_instance_state.identity in a way that
798 it always returns the identity even if object is still in transient state (
799 new object that is not yet persisted into database). Also for classes it
800 returns the identity attributes.
801
802 ::
803
804 from sqlalchemy import inspect
805 from sqlalchemy_utils import identity
806
807
808 user = User(name='John Matrix')
809 session.add(user)
810 identity(user) # None
811 inspect(user).identity # None
812
813 session.flush() # User now has id but is still in transient state
814
815 identity(user) # (1,)
816 inspect(user).identity # None
817
818 session.commit()
819
820 identity(user) # (1,)
821 inspect(user).identity # (1, )
822
823
824 You can also use identity for classes::
825
826
827 identity(User) # (User.id, )
828
829 .. versionadded: 0.21.0
830
831 :param obj: SQLAlchemy declarative model object
832 """
833 return tuple(
834 getattr(obj_or_class, column_key)
835 for column_key in get_primary_keys(obj_or_class).keys()
836 )
837
838
839def naturally_equivalent(obj, obj2):
840 """
841 Returns whether or not two given SQLAlchemy declarative instances are
842 naturally equivalent (all their non primary key properties are equivalent).
843
844
845 ::
846
847 from sqlalchemy_utils import naturally_equivalent
848
849
850 user = User(name='someone')
851 user2 = User(name='someone')
852
853 user == user2 # False
854
855 naturally_equivalent(user, user2) # True
856
857
858 :param obj: SQLAlchemy declarative model object
859 :param obj2: SQLAlchemy declarative model object to compare with `obj`
860 """
861 for column_key, column in sa.inspect(obj.__class__).columns.items():
862 if column.primary_key:
863 continue
864
865 if not (getattr(obj, column_key) == getattr(obj2, column_key)):
866 return False
867 return True
868
869
870def _get_class_registry(class_):
871 return class_.registry._class_registry