1# engine/reflection.py
2# Copyright (C) 2005-2021 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: http://www.opensource.org/licenses/mit-license.php
7
8"""Provides an abstraction for obtaining database schema information.
9
10Usage Notes:
11
12Here are some general conventions when accessing the low level inspector
13methods such as get_table_names, get_columns, etc.
14
151. Inspector methods return lists of dicts in most cases for the following
16 reasons:
17
18 * They're both standard types that can be serialized.
19 * Using a dict instead of a tuple allows easy expansion of attributes.
20 * Using a list for the outer structure maintains order and is easy to work
21 with (e.g. list comprehension [d['name'] for d in cols]).
22
232. Records that contain a name, such as the column name in a column record
24 use the key 'name'. So for most return values, each record will have a
25 'name' attribute..
26"""
27
28from .base import Connectable
29from .. import exc
30from .. import inspection
31from .. import sql
32from .. import util
33from ..sql import operators
34from ..sql import schema as sa_schema
35from ..sql.type_api import TypeEngine
36from ..util import deprecated
37from ..util import topological
38
39
40@util.decorator
41def cache(fn, self, con, *args, **kw):
42 info_cache = kw.get("info_cache", None)
43 if info_cache is None:
44 return fn(self, con, *args, **kw)
45 key = (
46 fn.__name__,
47 tuple(a for a in args if isinstance(a, util.string_types)),
48 tuple((k, v) for k, v in kw.items() if k != "info_cache"),
49 )
50 ret = info_cache.get(key)
51 if ret is None:
52 ret = fn(self, con, *args, **kw)
53 info_cache[key] = ret
54 return ret
55
56
57class Inspector(object):
58 """Performs database schema inspection.
59
60 The Inspector acts as a proxy to the reflection methods of the
61 :class:`~sqlalchemy.engine.interfaces.Dialect`, providing a
62 consistent interface as well as caching support for previously
63 fetched metadata.
64
65 A :class:`_reflection.Inspector` object is usually created via the
66 :func:`_sa.inspect` function::
67
68 from sqlalchemy import inspect, create_engine
69 engine = create_engine('...')
70 insp = inspect(engine)
71
72 The inspection method above is equivalent to using the
73 :meth:`_reflection.Inspector.from_engine` method, i.e.::
74
75 engine = create_engine('...')
76 insp = Inspector.from_engine(engine)
77
78 Where above, the :class:`~sqlalchemy.engine.interfaces.Dialect` may opt
79 to return an :class:`_reflection.Inspector`
80 subclass that provides additional
81 methods specific to the dialect's target database.
82
83 """
84
85 def __init__(self, bind):
86 """Initialize a new :class:`_reflection.Inspector`.
87
88 :param bind: a :class:`~sqlalchemy.engine.Connectable`,
89 which is typically an instance of
90 :class:`~sqlalchemy.engine.Engine` or
91 :class:`~sqlalchemy.engine.Connection`.
92
93 For a dialect-specific instance of :class:`_reflection.Inspector`, see
94 :meth:`_reflection.Inspector.from_engine`
95
96 """
97 # this might not be a connection, it could be an engine.
98 self.bind = bind
99
100 # set the engine
101 if hasattr(bind, "engine"):
102 self.engine = bind.engine
103 else:
104 self.engine = bind
105
106 if self.engine is bind:
107 # if engine, ensure initialized
108 bind.connect().close()
109
110 self.dialect = self.engine.dialect
111 self.info_cache = {}
112
113 @classmethod
114 def from_engine(cls, bind):
115 """Construct a new dialect-specific Inspector object from the given
116 engine or connection.
117
118 :param bind: a :class:`~sqlalchemy.engine.Connectable`,
119 which is typically an instance of
120 :class:`~sqlalchemy.engine.Engine` or
121 :class:`~sqlalchemy.engine.Connection`.
122
123 This method differs from direct a direct constructor call of
124 :class:`_reflection.Inspector` in that the
125 :class:`~sqlalchemy.engine.interfaces.Dialect` is given a chance to
126 provide a dialect-specific :class:`_reflection.Inspector` instance,
127 which may
128 provide additional methods.
129
130 See the example at :class:`_reflection.Inspector`.
131
132 """
133 if hasattr(bind.dialect, "inspector"):
134 return bind.dialect.inspector(bind)
135 return Inspector(bind)
136
137 @inspection._inspects(Connectable)
138 def _insp(bind):
139 return Inspector.from_engine(bind)
140
141 @property
142 def default_schema_name(self):
143 """Return the default schema name presented by the dialect
144 for the current engine's database user.
145
146 E.g. this is typically ``public`` for PostgreSQL and ``dbo``
147 for SQL Server.
148
149 """
150 return self.dialect.default_schema_name
151
152 def get_schema_names(self):
153 """Return all schema names."""
154
155 if hasattr(self.dialect, "get_schema_names"):
156 return self.dialect.get_schema_names(
157 self.bind, info_cache=self.info_cache
158 )
159 return []
160
161 @util.deprecated_params(
162 order_by=(
163 "1.0",
164 "The :paramref:`get_table_names.order_by` parameter is deprecated "
165 "and will be removed in a future release. Please refer to "
166 ":meth:`_reflection.Inspector.get_sorted_table_and_fkc_names` "
167 "for a "
168 "more comprehensive solution to resolving foreign key cycles "
169 "between tables.",
170 )
171 )
172 def get_table_names(self, schema=None, order_by=None):
173 """Return all table names in referred to within a particular schema.
174
175 The names are expected to be real tables only, not views.
176 Views are instead returned using the
177 :meth:`_reflection.Inspector.get_view_names`
178 method.
179
180
181 :param schema: Schema name. If ``schema`` is left at ``None``, the
182 database's default schema is
183 used, else the named schema is searched. If the database does not
184 support named schemas, behavior is undefined if ``schema`` is not
185 passed as ``None``. For special quoting, use :class:`.quoted_name`.
186
187 :param order_by: Optional, may be the string "foreign_key" to sort
188 the result on foreign key dependencies. Does not automatically
189 resolve cycles, and will raise :class:`.CircularDependencyError`
190 if cycles exist.
191
192 .. seealso::
193
194 :meth:`_reflection.Inspector.get_sorted_table_and_fkc_names`
195
196 :attr:`_schema.MetaData.sorted_tables`
197
198 """
199
200 if hasattr(self.dialect, "get_table_names"):
201 tnames = self.dialect.get_table_names(
202 self.bind, schema, info_cache=self.info_cache
203 )
204 else:
205 tnames = self.engine.table_names(schema)
206 if order_by == "foreign_key":
207 tuples = []
208 for tname in tnames:
209 for fkey in self.get_foreign_keys(tname, schema):
210 if tname != fkey["referred_table"]:
211 tuples.append((fkey["referred_table"], tname))
212 tnames = list(topological.sort(tuples, tnames))
213 return tnames
214
215 def get_sorted_table_and_fkc_names(self, schema=None):
216 """Return dependency-sorted table and foreign key constraint names in
217 referred to within a particular schema.
218
219 This will yield 2-tuples of
220 ``(tablename, [(tname, fkname), (tname, fkname), ...])``
221 consisting of table names in CREATE order grouped with the foreign key
222 constraint names that are not detected as belonging to a cycle.
223 The final element
224 will be ``(None, [(tname, fkname), (tname, fkname), ..])``
225 which will consist of remaining
226 foreign key constraint names that would require a separate CREATE
227 step after-the-fact, based on dependencies between tables.
228
229 .. versionadded:: 1.0.-
230
231 .. seealso::
232
233 :meth:`_reflection.Inspector.get_table_names`
234
235 :func:`.sort_tables_and_constraints` - similar method which works
236 with an already-given :class:`_schema.MetaData`.
237
238 """
239 if hasattr(self.dialect, "get_table_names"):
240 tnames = self.dialect.get_table_names(
241 self.bind, schema, info_cache=self.info_cache
242 )
243 else:
244 tnames = self.engine.table_names(schema)
245
246 tuples = set()
247 remaining_fkcs = set()
248
249 fknames_for_table = {}
250 for tname in tnames:
251 fkeys = self.get_foreign_keys(tname, schema)
252 fknames_for_table[tname] = set([fk["name"] for fk in fkeys])
253 for fkey in fkeys:
254 if tname != fkey["referred_table"]:
255 tuples.add((fkey["referred_table"], tname))
256 try:
257 candidate_sort = list(topological.sort(tuples, tnames))
258 except exc.CircularDependencyError as err:
259 for edge in err.edges:
260 tuples.remove(edge)
261 remaining_fkcs.update(
262 (edge[1], fkc) for fkc in fknames_for_table[edge[1]]
263 )
264
265 candidate_sort = list(topological.sort(tuples, tnames))
266 return [
267 (tname, fknames_for_table[tname].difference(remaining_fkcs))
268 for tname in candidate_sort
269 ] + [(None, list(remaining_fkcs))]
270
271 def get_temp_table_names(self):
272 """Return a list of temporary table names for the current bind.
273
274 This method is unsupported by most dialects; currently
275 only SQLite implements it.
276
277 .. versionadded:: 1.0.0
278
279 """
280 return self.dialect.get_temp_table_names(
281 self.bind, info_cache=self.info_cache
282 )
283
284 def get_temp_view_names(self):
285 """Return a list of temporary view names for the current bind.
286
287 This method is unsupported by most dialects; currently
288 only SQLite implements it.
289
290 .. versionadded:: 1.0.0
291
292 """
293 return self.dialect.get_temp_view_names(
294 self.bind, info_cache=self.info_cache
295 )
296
297 def get_table_options(self, table_name, schema=None, **kw):
298 """Return a dictionary of options specified when the table of the
299 given name was created.
300
301 This currently includes some options that apply to MySQL tables.
302
303 :param table_name: string name of the table. For special quoting,
304 use :class:`.quoted_name`.
305
306 :param schema: string schema name; if omitted, uses the default schema
307 of the database connection. For special quoting,
308 use :class:`.quoted_name`.
309
310 """
311 if hasattr(self.dialect, "get_table_options"):
312 return self.dialect.get_table_options(
313 self.bind, table_name, schema, info_cache=self.info_cache, **kw
314 )
315 return {}
316
317 def get_view_names(self, schema=None):
318 """Return all view names in `schema`.
319
320 :param schema: Optional, retrieve names from a non-default schema.
321 For special quoting, use :class:`.quoted_name`.
322
323 """
324
325 return self.dialect.get_view_names(
326 self.bind, schema, info_cache=self.info_cache
327 )
328
329 def get_view_definition(self, view_name, schema=None):
330 """Return definition for `view_name`.
331
332 :param schema: Optional, retrieve names from a non-default schema.
333 For special quoting, use :class:`.quoted_name`.
334
335 """
336
337 return self.dialect.get_view_definition(
338 self.bind, view_name, schema, info_cache=self.info_cache
339 )
340
341 def get_columns(self, table_name, schema=None, **kw):
342 """Return information about columns in `table_name`.
343
344 Given a string `table_name` and an optional string `schema`, return
345 column information as a list of dicts with these keys:
346
347 * ``name`` - the column's name
348
349 * ``type`` - the type of this column; an instance of
350 :class:`~sqlalchemy.types.TypeEngine`
351
352 * ``nullable`` - boolean flag if the column is NULL or NOT NULL
353
354 * ``default`` - the column's server default value - this is returned
355 as a string SQL expression.
356
357 * ``autoincrement`` - indicates that the column is auto incremented -
358 this is returned as a boolean or 'auto'
359
360 * ``comment`` - (optional) the comment on the column. Only some
361 dialects return this key
362
363 * ``computed`` - (optional) when present it indicates that this column
364 is computed by the database. Only some dialects return this key.
365 Returned as a dict with the keys:
366
367 * ``sqltext`` - the expression used to generate this column returned
368 as a string SQL expression
369
370 * ``persisted`` - (optional) boolean that indicates if the column is
371 stored in the table
372
373 .. versionadded:: 1.3.16 - added support for computed reflection.
374
375 * ``dialect_options`` - (optional) a dict with dialect specific options
376
377
378 :param table_name: string name of the table. For special quoting,
379 use :class:`.quoted_name`.
380
381 :param schema: string schema name; if omitted, uses the default schema
382 of the database connection. For special quoting,
383 use :class:`.quoted_name`.
384
385 :return: list of dictionaries, each representing the definition of
386 a database column.
387
388 """
389
390 col_defs = self.dialect.get_columns(
391 self.bind, table_name, schema, info_cache=self.info_cache, **kw
392 )
393 for col_def in col_defs:
394 # make this easy and only return instances for coltype
395 coltype = col_def["type"]
396 if not isinstance(coltype, TypeEngine):
397 col_def["type"] = coltype()
398 return col_defs
399
400 @deprecated(
401 "0.7",
402 "The :meth:`_reflection.Inspector.get_primary_keys` "
403 "method is deprecated and "
404 "will be removed in a future release. Please refer to the "
405 ":meth:`_reflection.Inspector.get_pk_constraint` method.",
406 )
407 def get_primary_keys(self, table_name, schema=None, **kw):
408 """Return information about primary keys in `table_name`.
409
410 Given a string `table_name`, and an optional string `schema`, return
411 primary key information as a list of column names.
412 """
413
414 return self.dialect.get_pk_constraint(
415 self.bind, table_name, schema, info_cache=self.info_cache, **kw
416 )["constrained_columns"]
417
418 def get_pk_constraint(self, table_name, schema=None, **kw):
419 """Return information about primary key constraint on `table_name`.
420
421 Given a string `table_name`, and an optional string `schema`, return
422 primary key information as a dictionary with these keys:
423
424 * ``constrained_columns`` -
425 a list of column names that make up the primary key
426
427 * ``name`` -
428 optional name of the primary key constraint.
429
430 :param table_name: string name of the table. For special quoting,
431 use :class:`.quoted_name`.
432
433 :param schema: string schema name; if omitted, uses the default schema
434 of the database connection. For special quoting,
435 use :class:`.quoted_name`.
436
437 """
438 return self.dialect.get_pk_constraint(
439 self.bind, table_name, schema, info_cache=self.info_cache, **kw
440 )
441
442 def get_foreign_keys(self, table_name, schema=None, **kw):
443 """Return information about foreign_keys in `table_name`.
444
445 Given a string `table_name`, and an optional string `schema`, return
446 foreign key information as a list of dicts with these keys:
447
448 * ``constrained_columns`` -
449 a list of column names that make up the foreign key
450
451 * ``referred_schema`` -
452 the name of the referred schema
453
454 * ``referred_table`` -
455 the name of the referred table
456
457 * ``referred_columns`` -
458 a list of column names in the referred table that correspond to
459 constrained_columns
460
461 * ``name`` -
462 optional name of the foreign key constraint.
463
464 :param table_name: string name of the table. For special quoting,
465 use :class:`.quoted_name`.
466
467 :param schema: string schema name; if omitted, uses the default schema
468 of the database connection. For special quoting,
469 use :class:`.quoted_name`.
470
471 """
472
473 return self.dialect.get_foreign_keys(
474 self.bind, table_name, schema, info_cache=self.info_cache, **kw
475 )
476
477 def get_indexes(self, table_name, schema=None, **kw):
478 """Return information about indexes in `table_name`.
479
480 Given a string `table_name` and an optional string `schema`, return
481 index information as a list of dicts with these keys:
482
483 * ``name`` -
484 the index's name
485
486 * ``column_names`` -
487 list of column names in order
488
489 * ``unique`` -
490 boolean
491
492 * ``column_sorting`` -
493 optional dict mapping column names to tuple of sort keywords,
494 which may include ``asc``, ``desc``, ``nullsfirst``, ``nullslast``.
495
496 .. versionadded:: 1.3.5
497
498 * ``dialect_options`` -
499 dict of dialect-specific index options. May not be present
500 for all dialects.
501
502 .. versionadded:: 1.0.0
503
504 :param table_name: string name of the table. For special quoting,
505 use :class:`.quoted_name`.
506
507 :param schema: string schema name; if omitted, uses the default schema
508 of the database connection. For special quoting,
509 use :class:`.quoted_name`.
510
511 """
512
513 return self.dialect.get_indexes(
514 self.bind, table_name, schema, info_cache=self.info_cache, **kw
515 )
516
517 def get_unique_constraints(self, table_name, schema=None, **kw):
518 """Return information about unique constraints in `table_name`.
519
520 Given a string `table_name` and an optional string `schema`, return
521 unique constraint information as a list of dicts with these keys:
522
523 * ``name`` -
524 the unique constraint's name
525
526 * ``column_names`` -
527 list of column names in order
528
529 :param table_name: string name of the table. For special quoting,
530 use :class:`.quoted_name`.
531
532 :param schema: string schema name; if omitted, uses the default schema
533 of the database connection. For special quoting,
534 use :class:`.quoted_name`.
535
536 """
537
538 return self.dialect.get_unique_constraints(
539 self.bind, table_name, schema, info_cache=self.info_cache, **kw
540 )
541
542 def get_table_comment(self, table_name, schema=None, **kw):
543 """Return information about the table comment for ``table_name``.
544
545 Given a string ``table_name`` and an optional string ``schema``,
546 return table comment information as a dictionary with these keys:
547
548 * ``text`` -
549 text of the comment.
550
551 Raises ``NotImplementedError`` for a dialect that does not support
552 comments.
553
554 .. versionadded:: 1.2
555
556 """
557
558 return self.dialect.get_table_comment(
559 self.bind, table_name, schema, info_cache=self.info_cache, **kw
560 )
561
562 def get_check_constraints(self, table_name, schema=None, **kw):
563 """Return information about check constraints in `table_name`.
564
565 Given a string `table_name` and an optional string `schema`, return
566 check constraint information as a list of dicts with these keys:
567
568 * ``name`` -
569 the check constraint's name
570
571 * ``sqltext`` -
572 the check constraint's SQL expression
573
574 * ``dialect_options`` -
575 may or may not be present; a dictionary with additional
576 dialect-specific options for this CHECK constraint
577
578 .. versionadded:: 1.3.8
579
580 :param table_name: string name of the table. For special quoting,
581 use :class:`.quoted_name`.
582
583 :param schema: string schema name; if omitted, uses the default schema
584 of the database connection. For special quoting,
585 use :class:`.quoted_name`.
586
587 .. versionadded:: 1.1.0
588
589 """
590
591 return self.dialect.get_check_constraints(
592 self.bind, table_name, schema, info_cache=self.info_cache, **kw
593 )
594
595 def reflecttable(
596 self,
597 table,
598 include_columns,
599 exclude_columns=(),
600 resolve_fks=True,
601 _extend_on=None,
602 ):
603 """Given a :class:`_schema.Table` object, load its internal
604 constructs based on introspection.
605
606 This is the underlying method used by most dialects to produce
607 table reflection. Direct usage is like::
608
609 from sqlalchemy import create_engine, MetaData, Table
610 from sqlalchemy.engine.reflection import Inspector
611
612 engine = create_engine('...')
613 meta = MetaData()
614 user_table = Table('user', meta)
615 insp = Inspector.from_engine(engine)
616 insp.reflecttable(user_table, None)
617
618 :param table: a :class:`~sqlalchemy.schema.Table` instance.
619 :param include_columns: a list of string column names to include
620 in the reflection process. If ``None``, all columns are reflected.
621
622 """
623
624 if _extend_on is not None:
625 if table in _extend_on:
626 return
627 else:
628 _extend_on.add(table)
629
630 dialect = self.bind.dialect
631
632 schema = self.bind.schema_for_object(table)
633
634 table_name = table.name
635
636 # get table-level arguments that are specifically
637 # intended for reflection, e.g. oracle_resolve_synonyms.
638 # these are unconditionally passed to related Table
639 # objects
640 reflection_options = dict(
641 (k, table.dialect_kwargs.get(k))
642 for k in dialect.reflection_options
643 if k in table.dialect_kwargs
644 )
645
646 # reflect table options, like mysql_engine
647 tbl_opts = self.get_table_options(
648 table_name, schema, **table.dialect_kwargs
649 )
650 if tbl_opts:
651 # add additional kwargs to the Table if the dialect
652 # returned them
653 table._validate_dialect_kwargs(tbl_opts)
654
655 if util.py2k:
656 if isinstance(schema, str):
657 schema = schema.decode(dialect.encoding)
658 if isinstance(table_name, str):
659 table_name = table_name.decode(dialect.encoding)
660
661 found_table = False
662 cols_by_orig_name = {}
663
664 for col_d in self.get_columns(
665 table_name, schema, **table.dialect_kwargs
666 ):
667 found_table = True
668
669 self._reflect_column(
670 table,
671 col_d,
672 include_columns,
673 exclude_columns,
674 cols_by_orig_name,
675 )
676
677 if not found_table:
678 raise exc.NoSuchTableError(table.name)
679
680 self._reflect_pk(
681 table_name, schema, table, cols_by_orig_name, exclude_columns
682 )
683
684 self._reflect_fk(
685 table_name,
686 schema,
687 table,
688 cols_by_orig_name,
689 exclude_columns,
690 resolve_fks,
691 _extend_on,
692 reflection_options,
693 )
694
695 self._reflect_indexes(
696 table_name,
697 schema,
698 table,
699 cols_by_orig_name,
700 include_columns,
701 exclude_columns,
702 reflection_options,
703 )
704
705 self._reflect_unique_constraints(
706 table_name,
707 schema,
708 table,
709 cols_by_orig_name,
710 include_columns,
711 exclude_columns,
712 reflection_options,
713 )
714
715 self._reflect_check_constraints(
716 table_name,
717 schema,
718 table,
719 cols_by_orig_name,
720 include_columns,
721 exclude_columns,
722 reflection_options,
723 )
724
725 self._reflect_table_comment(
726 table_name, schema, table, reflection_options
727 )
728
729 def _reflect_column(
730 self, table, col_d, include_columns, exclude_columns, cols_by_orig_name
731 ):
732
733 orig_name = col_d["name"]
734
735 table.dispatch.column_reflect(self, table, col_d)
736
737 # fetch name again as column_reflect is allowed to
738 # change it
739 name = col_d["name"]
740 if (include_columns and name not in include_columns) or (
741 exclude_columns and name in exclude_columns
742 ):
743 return
744
745 coltype = col_d["type"]
746
747 col_kw = dict(
748 (k, col_d[k])
749 for k in [
750 "nullable",
751 "autoincrement",
752 "quote",
753 "info",
754 "key",
755 "comment",
756 ]
757 if k in col_d
758 )
759
760 if "dialect_options" in col_d:
761 col_kw.update(col_d["dialect_options"])
762
763 colargs = []
764 if col_d.get("default") is not None:
765 default = col_d["default"]
766 if isinstance(default, sql.elements.TextClause):
767 default = sa_schema.DefaultClause(default, _reflected=True)
768 elif not isinstance(default, sa_schema.FetchedValue):
769 default = sa_schema.DefaultClause(
770 sql.text(col_d["default"]), _reflected=True
771 )
772
773 colargs.append(default)
774
775 if "computed" in col_d:
776 computed = sa_schema.Computed(**col_d["computed"])
777 colargs.append(computed)
778
779 if "sequence" in col_d:
780 self._reflect_col_sequence(col_d, colargs)
781
782 cols_by_orig_name[orig_name] = col = sa_schema.Column(
783 name, coltype, *colargs, **col_kw
784 )
785
786 if col.key in table.primary_key:
787 col.primary_key = True
788 table.append_column(col)
789
790 def _reflect_col_sequence(self, col_d, colargs):
791 if "sequence" in col_d:
792 # TODO: mssql and sybase are using this.
793 seq = col_d["sequence"]
794 sequence = sa_schema.Sequence(seq["name"], 1, 1)
795 if "start" in seq:
796 sequence.start = seq["start"]
797 if "increment" in seq:
798 sequence.increment = seq["increment"]
799 colargs.append(sequence)
800
801 def _reflect_pk(
802 self, table_name, schema, table, cols_by_orig_name, exclude_columns
803 ):
804 pk_cons = self.get_pk_constraint(
805 table_name, schema, **table.dialect_kwargs
806 )
807 if pk_cons:
808 pk_cols = [
809 cols_by_orig_name[pk]
810 for pk in pk_cons["constrained_columns"]
811 if pk in cols_by_orig_name and pk not in exclude_columns
812 ]
813
814 # update pk constraint name
815 table.primary_key.name = pk_cons.get("name")
816
817 # tell the PKConstraint to re-initialize
818 # its column collection
819 table.primary_key._reload(pk_cols)
820
821 def _reflect_fk(
822 self,
823 table_name,
824 schema,
825 table,
826 cols_by_orig_name,
827 exclude_columns,
828 resolve_fks,
829 _extend_on,
830 reflection_options,
831 ):
832 fkeys = self.get_foreign_keys(
833 table_name, schema, **table.dialect_kwargs
834 )
835 for fkey_d in fkeys:
836 conname = fkey_d["name"]
837 # look for columns by orig name in cols_by_orig_name,
838 # but support columns that are in-Python only as fallback
839 constrained_columns = [
840 cols_by_orig_name[c].key if c in cols_by_orig_name else c
841 for c in fkey_d["constrained_columns"]
842 ]
843 if exclude_columns and set(constrained_columns).intersection(
844 exclude_columns
845 ):
846 continue
847 referred_schema = fkey_d["referred_schema"]
848 referred_table = fkey_d["referred_table"]
849 referred_columns = fkey_d["referred_columns"]
850 refspec = []
851 if referred_schema is not None:
852 if resolve_fks:
853 sa_schema.Table(
854 referred_table,
855 table.metadata,
856 autoload=True,
857 schema=referred_schema,
858 autoload_with=self.bind,
859 _extend_on=_extend_on,
860 **reflection_options
861 )
862 for column in referred_columns:
863 refspec.append(
864 ".".join([referred_schema, referred_table, column])
865 )
866 else:
867 if resolve_fks:
868 sa_schema.Table(
869 referred_table,
870 table.metadata,
871 autoload=True,
872 autoload_with=self.bind,
873 schema=sa_schema.BLANK_SCHEMA,
874 _extend_on=_extend_on,
875 **reflection_options
876 )
877 for column in referred_columns:
878 refspec.append(".".join([referred_table, column]))
879 if "options" in fkey_d:
880 options = fkey_d["options"]
881 else:
882 options = {}
883 table.append_constraint(
884 sa_schema.ForeignKeyConstraint(
885 constrained_columns,
886 refspec,
887 conname,
888 link_to_name=True,
889 **options
890 )
891 )
892
893 _index_sort_exprs = [
894 ("asc", operators.asc_op),
895 ("desc", operators.desc_op),
896 ("nullsfirst", operators.nullsfirst_op),
897 ("nullslast", operators.nullslast_op),
898 ]
899
900 def _reflect_indexes(
901 self,
902 table_name,
903 schema,
904 table,
905 cols_by_orig_name,
906 include_columns,
907 exclude_columns,
908 reflection_options,
909 ):
910 # Indexes
911 indexes = self.get_indexes(table_name, schema)
912 for index_d in indexes:
913 name = index_d["name"]
914 columns = index_d["column_names"]
915 column_sorting = index_d.get("column_sorting", {})
916 unique = index_d["unique"]
917 flavor = index_d.get("type", "index")
918 dialect_options = index_d.get("dialect_options", {})
919
920 duplicates = index_d.get("duplicates_constraint")
921 if include_columns and not set(columns).issubset(include_columns):
922 util.warn(
923 "Omitting %s key for (%s), key covers omitted columns."
924 % (flavor, ", ".join(columns))
925 )
926 continue
927 if duplicates:
928 continue
929 # look for columns by orig name in cols_by_orig_name,
930 # but support columns that are in-Python only as fallback
931 idx_cols = []
932 for c in columns:
933 try:
934 idx_col = (
935 cols_by_orig_name[c]
936 if c in cols_by_orig_name
937 else table.c[c]
938 )
939 except KeyError:
940 util.warn(
941 "%s key '%s' was not located in "
942 "columns for table '%s'" % (flavor, c, table_name)
943 )
944 continue
945 c_sorting = column_sorting.get(c, ())
946 for k, op in self._index_sort_exprs:
947 if k in c_sorting:
948 idx_col = op(idx_col)
949 idx_cols.append(idx_col)
950
951 sa_schema.Index(
952 name,
953 *idx_cols,
954 _table=table,
955 **dict(list(dialect_options.items()) + [("unique", unique)])
956 )
957
958 def _reflect_unique_constraints(
959 self,
960 table_name,
961 schema,
962 table,
963 cols_by_orig_name,
964 include_columns,
965 exclude_columns,
966 reflection_options,
967 ):
968
969 # Unique Constraints
970 try:
971 constraints = self.get_unique_constraints(table_name, schema)
972 except NotImplementedError:
973 # optional dialect feature
974 return
975
976 for const_d in constraints:
977 conname = const_d["name"]
978 columns = const_d["column_names"]
979 duplicates = const_d.get("duplicates_index")
980 if include_columns and not set(columns).issubset(include_columns):
981 util.warn(
982 "Omitting unique constraint key for (%s), "
983 "key covers omitted columns." % ", ".join(columns)
984 )
985 continue
986 if duplicates:
987 continue
988 # look for columns by orig name in cols_by_orig_name,
989 # but support columns that are in-Python only as fallback
990 constrained_cols = []
991 for c in columns:
992 try:
993 constrained_col = (
994 cols_by_orig_name[c]
995 if c in cols_by_orig_name
996 else table.c[c]
997 )
998 except KeyError:
999 util.warn(
1000 "unique constraint key '%s' was not located in "
1001 "columns for table '%s'" % (c, table_name)
1002 )
1003 else:
1004 constrained_cols.append(constrained_col)
1005 table.append_constraint(
1006 sa_schema.UniqueConstraint(*constrained_cols, name=conname)
1007 )
1008
1009 def _reflect_check_constraints(
1010 self,
1011 table_name,
1012 schema,
1013 table,
1014 cols_by_orig_name,
1015 include_columns,
1016 exclude_columns,
1017 reflection_options,
1018 ):
1019 try:
1020 constraints = self.get_check_constraints(table_name, schema)
1021 except NotImplementedError:
1022 # optional dialect feature
1023 return
1024
1025 for const_d in constraints:
1026 table.append_constraint(sa_schema.CheckConstraint(**const_d))
1027
1028 def _reflect_table_comment(
1029 self, table_name, schema, table, reflection_options
1030 ):
1031 try:
1032 comment_dict = self.get_table_comment(table_name, schema)
1033 except NotImplementedError:
1034 return
1035 else:
1036 table.comment = comment_dict.get("text", None)