1# engine/reflection.py
2# Copyright (C) 2005-2016 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 .. import exc, sql
29from ..sql import schema as sa_schema
30from .. import util
31from ..sql.type_api import TypeEngine
32from ..util import deprecated
33from ..util import topological
34from .. import inspection
35from .base import Connectable
36
37
38@util.decorator
39def cache(fn, self, con, *args, **kw):
40    info_cache = kw.get('info_cache', None)
41    if info_cache is None:
42        return fn(self, con, *args, **kw)
43    key = (
44        fn.__name__,
45        tuple(a for a in args if isinstance(a, util.string_types)),
46        tuple((k, v) for k, v in kw.items() if
47              isinstance(v,
48                         util.string_types + util.int_types + (float, )
49                         )
50              )
51    )
52    ret = info_cache.get(key)
53    if ret is None:
54        ret = fn(self, con, *args, **kw)
55        info_cache[key] = ret
56    return ret
57
58
59class Inspector(object):
60    """Performs database schema inspection.
61
62    The Inspector acts as a proxy to the reflection methods of the
63    :class:`~sqlalchemy.engine.interfaces.Dialect`, providing a
64    consistent interface as well as caching support for previously
65    fetched metadata.
66
67    A :class:`.Inspector` object is usually created via the
68    :func:`.inspect` function::
69
70        from sqlalchemy import inspect, create_engine
71        engine = create_engine('...')
72        insp = inspect(engine)
73
74    The inspection method above is equivalent to using the
75    :meth:`.Inspector.from_engine` method, i.e.::
76
77        engine = create_engine('...')
78        insp = Inspector.from_engine(engine)
79
80    Where above, the :class:`~sqlalchemy.engine.interfaces.Dialect` may opt
81    to return an :class:`.Inspector` subclass that provides additional
82    methods specific to the dialect's target database.
83
84    """
85
86    def __init__(self, bind):
87        """Initialize a new :class:`.Inspector`.
88
89        :param bind: a :class:`~sqlalchemy.engine.Connectable`,
90          which is typically an instance of
91          :class:`~sqlalchemy.engine.Engine` or
92          :class:`~sqlalchemy.engine.Connection`.
93
94        For a dialect-specific instance of :class:`.Inspector`, see
95        :meth:`.Inspector.from_engine`
96
97        """
98        # this might not be a connection, it could be an engine.
99        self.bind = bind
100
101        # set the engine
102        if hasattr(bind, 'engine'):
103            self.engine = bind.engine
104        else:
105            self.engine = bind
106
107        if self.engine is bind:
108            # if engine, ensure initialized
109            bind.connect().close()
110
111        self.dialect = self.engine.dialect
112        self.info_cache = {}
113
114    @classmethod
115    def from_engine(cls, bind):
116        """Construct a new dialect-specific Inspector object from the given
117        engine or connection.
118
119        :param bind: a :class:`~sqlalchemy.engine.Connectable`,
120          which is typically an instance of
121          :class:`~sqlalchemy.engine.Engine` or
122          :class:`~sqlalchemy.engine.Connection`.
123
124        This method differs from direct a direct constructor call of
125        :class:`.Inspector` in that the
126        :class:`~sqlalchemy.engine.interfaces.Dialect` is given a chance to
127        provide a dialect-specific :class:`.Inspector` instance, which may
128        provide additional methods.
129
130        See the example at :class:`.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
156        if hasattr(self.dialect, 'get_schema_names'):
157            return self.dialect.get_schema_names(self.bind,
158                                                 info_cache=self.info_cache)
159        return []
160
161    def get_table_names(self, schema=None, order_by=None):
162        """Return all table names in referred to within a particular schema.
163
164        The names are expected to be real tables only, not views.
165        Views are instead returned using the :meth:`.Inspector.get_view_names`
166        method.
167
168
169        :param schema: Schema name. If ``schema`` is left at ``None``, the
170         database's default schema is
171         used, else the named schema is searched.  If the database does not
172         support named schemas, behavior is undefined if ``schema`` is not
173         passed as ``None``.  For special quoting, use :class:`.quoted_name`.
174
175        :param order_by: Optional, may be the string "foreign_key" to sort
176         the result on foreign key dependencies.  Does not automatically
177         resolve cycles, and will raise :class:`.CircularDependencyError`
178         if cycles exist.
179
180         .. deprecated:: 1.0.0 - see
181            :meth:`.Inspector.get_sorted_table_and_fkc_names` for a version
182            of this which resolves foreign key cycles between tables
183            automatically.
184
185         .. versionchanged:: 0.8 the "foreign_key" sorting sorts tables
186            in order of dependee to dependent; that is, in creation
187            order, rather than in drop order.  This is to maintain
188            consistency with similar features such as
189            :attr:`.MetaData.sorted_tables` and :func:`.util.sort_tables`.
190
191        .. seealso::
192
193            :meth:`.Inspector.get_sorted_table_and_fkc_names`
194
195            :attr:`.MetaData.sorted_tables`
196
197        """
198
199        if hasattr(self.dialect, 'get_table_names'):
200            tnames = self.dialect.get_table_names(
201                self.bind, schema, info_cache=self.info_cache)
202        else:
203            tnames = self.engine.table_names(schema)
204        if order_by == 'foreign_key':
205            tuples = []
206            for tname in tnames:
207                for fkey in self.get_foreign_keys(tname, schema):
208                    if tname != fkey['referred_table']:
209                        tuples.append((fkey['referred_table'], tname))
210            tnames = list(topological.sort(tuples, tnames))
211        return tnames
212
213    def get_sorted_table_and_fkc_names(self, schema=None):
214        """Return dependency-sorted table and foreign key constraint names in
215        referred to within a particular schema.
216
217        This will yield 2-tuples of
218        ``(tablename, [(tname, fkname), (tname, fkname), ...])``
219        consisting of table names in CREATE order grouped with the foreign key
220        constraint names that are not detected as belonging to a cycle.
221        The final element
222        will be ``(None, [(tname, fkname), (tname, fkname), ..])``
223        which will consist of remaining
224        foreign key constraint names that would require a separate CREATE
225        step after-the-fact, based on dependencies between tables.
226
227        .. versionadded:: 1.0.-
228
229        .. seealso::
230
231            :meth:`.Inspector.get_table_names`
232
233            :func:`.sort_tables_and_constraints` - similar method which works
234             with an already-given :class:`.MetaData`.
235
236        """
237        if hasattr(self.dialect, 'get_table_names'):
238            tnames = self.dialect.get_table_names(
239                self.bind, schema, info_cache=self.info_cache)
240        else:
241            tnames = self.engine.table_names(schema)
242
243        tuples = set()
244        remaining_fkcs = set()
245
246        fknames_for_table = {}
247        for tname in tnames:
248            fkeys = self.get_foreign_keys(tname, schema)
249            fknames_for_table[tname] = set(
250                [fk['name'] for fk in fkeys]
251            )
252            for fkey in fkeys:
253                if tname != fkey['referred_table']:
254                    tuples.add((fkey['referred_table'], tname))
255        try:
256            candidate_sort = list(topological.sort(tuples, tnames))
257        except exc.CircularDependencyError as err:
258            for edge in err.edges:
259                tuples.remove(edge)
260                remaining_fkcs.update(
261                    (edge[1], fkc)
262                    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    def get_temp_view_names(self):
284        """return a list of temporary view names for the current bind.
285
286        This method is unsupported by most dialects; currently
287        only SQLite implements it.
288
289        .. versionadded:: 1.0.0
290
291        """
292        return self.dialect.get_temp_view_names(
293            self.bind, info_cache=self.info_cache)
294
295    def get_table_options(self, table_name, schema=None, **kw):
296        """Return a dictionary of options specified when the table of the
297        given name was created.
298
299        This currently includes some options that apply to MySQL tables.
300
301        :param table_name: string name of the table.  For special quoting,
302         use :class:`.quoted_name`.
303
304        :param schema: string schema name; if omitted, uses the default schema
305         of the database connection.  For special quoting,
306         use :class:`.quoted_name`.
307
308        """
309        if hasattr(self.dialect, 'get_table_options'):
310            return self.dialect.get_table_options(
311                self.bind, table_name, schema,
312                info_cache=self.info_cache, **kw)
313        return {}
314
315    def get_view_names(self, schema=None):
316        """Return all view names in `schema`.
317
318        :param schema: Optional, retrieve names from a non-default schema.
319         For special quoting, use :class:`.quoted_name`.
320
321        """
322
323        return self.dialect.get_view_names(self.bind, schema,
324                                           info_cache=self.info_cache)
325
326    def get_view_definition(self, view_name, schema=None):
327        """Return definition for `view_name`.
328
329        :param schema: Optional, retrieve names from a non-default schema.
330         For special quoting, use :class:`.quoted_name`.
331
332        """
333
334        return self.dialect.get_view_definition(
335            self.bind, view_name, schema, info_cache=self.info_cache)
336
337    def get_columns(self, table_name, schema=None, **kw):
338        """Return information about columns in `table_name`.
339
340        Given a string `table_name` and an optional string `schema`, return
341        column information as a list of dicts with these keys:
342
343        name
344          the column's name
345
346        type
347          :class:`~sqlalchemy.types.TypeEngine`
348
349        nullable
350          boolean
351
352        default
353          the column's default value
354
355        attrs
356          dict containing optional column attributes
357
358        :param table_name: string name of the table.  For special quoting,
359         use :class:`.quoted_name`.
360
361        :param schema: string schema name; if omitted, uses the default schema
362         of the database connection.  For special quoting,
363         use :class:`.quoted_name`.
364
365        """
366
367        col_defs = self.dialect.get_columns(self.bind, table_name, schema,
368                                            info_cache=self.info_cache,
369                                            **kw)
370        for col_def in col_defs:
371            # make this easy and only return instances for coltype
372            coltype = col_def['type']
373            if not isinstance(coltype, TypeEngine):
374                col_def['type'] = coltype()
375        return col_defs
376
377    @deprecated('0.7', 'Call to deprecated method get_primary_keys.'
378                '  Use get_pk_constraint instead.')
379    def get_primary_keys(self, table_name, schema=None, **kw):
380        """Return information about primary keys in `table_name`.
381
382        Given a string `table_name`, and an optional string `schema`, return
383        primary key information as a list of column names.
384        """
385
386        return self.dialect.get_pk_constraint(self.bind, table_name, schema,
387                                              info_cache=self.info_cache,
388                                              **kw)['constrained_columns']
389
390    def get_pk_constraint(self, table_name, schema=None, **kw):
391        """Return information about primary key constraint on `table_name`.
392
393        Given a string `table_name`, and an optional string `schema`, return
394        primary key information as a dictionary with these keys:
395
396        constrained_columns
397          a list of column names that make up the primary key
398
399        name
400          optional name of the primary key constraint.
401
402        :param table_name: string name of the table.  For special quoting,
403         use :class:`.quoted_name`.
404
405        :param schema: string schema name; if omitted, uses the default schema
406         of the database connection.  For special quoting,
407         use :class:`.quoted_name`.
408
409        """
410        return self.dialect.get_pk_constraint(self.bind, table_name, schema,
411                                              info_cache=self.info_cache,
412                                              **kw)
413
414    def get_foreign_keys(self, table_name, schema=None, **kw):
415        """Return information about foreign_keys in `table_name`.
416
417        Given a string `table_name`, and an optional string `schema`, return
418        foreign key information as a list of dicts with these keys:
419
420        constrained_columns
421          a list of column names that make up the foreign key
422
423        referred_schema
424          the name of the referred schema
425
426        referred_table
427          the name of the referred table
428
429        referred_columns
430          a list of column names in the referred table that correspond to
431          constrained_columns
432
433        name
434          optional name of the foreign key constraint.
435
436        :param table_name: string name of the table.  For special quoting,
437         use :class:`.quoted_name`.
438
439        :param schema: string schema name; if omitted, uses the default schema
440         of the database connection.  For special quoting,
441         use :class:`.quoted_name`.
442
443        """
444
445        return self.dialect.get_foreign_keys(self.bind, table_name, schema,
446                                             info_cache=self.info_cache,
447                                             **kw)
448
449    def get_indexes(self, table_name, schema=None, **kw):
450        """Return information about indexes in `table_name`.
451
452        Given a string `table_name` and an optional string `schema`, return
453        index information as a list of dicts with these keys:
454
455        name
456          the index's name
457
458        column_names
459          list of column names in order
460
461        unique
462          boolean
463
464        dialect_options
465          dict of dialect-specific index options.  May not be present
466          for all dialects.
467
468          .. versionadded:: 1.0.0
469
470        :param table_name: string name of the table.  For special quoting,
471         use :class:`.quoted_name`.
472
473        :param schema: string schema name; if omitted, uses the default schema
474         of the database connection.  For special quoting,
475         use :class:`.quoted_name`.
476
477        """
478
479        return self.dialect.get_indexes(self.bind, table_name,
480                                        schema,
481                                        info_cache=self.info_cache, **kw)
482
483    def get_unique_constraints(self, table_name, schema=None, **kw):
484        """Return information about unique constraints in `table_name`.
485
486        Given a string `table_name` and an optional string `schema`, return
487        unique constraint information as a list of dicts with these keys:
488
489        name
490          the unique constraint's name
491
492        column_names
493          list of column names in order
494
495        :param table_name: string name of the table.  For special quoting,
496         use :class:`.quoted_name`.
497
498        :param schema: string schema name; if omitted, uses the default schema
499         of the database connection.  For special quoting,
500         use :class:`.quoted_name`.
501
502        .. versionadded:: 0.8.4
503
504        """
505
506        return self.dialect.get_unique_constraints(
507            self.bind, table_name, schema, info_cache=self.info_cache, **kw)
508
509    def reflecttable(self, table, include_columns, exclude_columns=()):
510        """Given a Table object, load its internal constructs based on
511        introspection.
512
513        This is the underlying method used by most dialects to produce
514        table reflection.  Direct usage is like::
515
516            from sqlalchemy import create_engine, MetaData, Table
517            from sqlalchemy.engine import reflection
518
519            engine = create_engine('...')
520            meta = MetaData()
521            user_table = Table('user', meta)
522            insp = Inspector.from_engine(engine)
523            insp.reflecttable(user_table, None)
524
525        :param table: a :class:`~sqlalchemy.schema.Table` instance.
526        :param include_columns: a list of string column names to include
527          in the reflection process.  If ``None``, all columns are reflected.
528
529        """
530        dialect = self.bind.dialect
531
532        schema = table.schema
533        table_name = table.name
534
535        # get table-level arguments that are specifically
536        # intended for reflection, e.g. oracle_resolve_synonyms.
537        # these are unconditionally passed to related Table
538        # objects
539        reflection_options = dict(
540            (k, table.dialect_kwargs.get(k))
541            for k in dialect.reflection_options
542            if k in table.dialect_kwargs
543        )
544
545        # reflect table options, like mysql_engine
546        tbl_opts = self.get_table_options(
547            table_name, schema, **table.dialect_kwargs)
548        if tbl_opts:
549            # add additional kwargs to the Table if the dialect
550            # returned them
551            table._validate_dialect_kwargs(tbl_opts)
552
553        if util.py2k:
554            if isinstance(schema, str):
555                schema = schema.decode(dialect.encoding)
556            if isinstance(table_name, str):
557                table_name = table_name.decode(dialect.encoding)
558
559        found_table = False
560        cols_by_orig_name = {}
561
562        for col_d in self.get_columns(
563                table_name, schema, **table.dialect_kwargs):
564            found_table = True
565
566            self._reflect_column(
567                table, col_d, include_columns,
568                exclude_columns, cols_by_orig_name)
569
570        if not found_table:
571            raise exc.NoSuchTableError(table.name)
572
573        self._reflect_pk(
574            table_name, schema, table, cols_by_orig_name, exclude_columns)
575
576        self._reflect_fk(
577            table_name, schema, table, cols_by_orig_name,
578            exclude_columns, reflection_options)
579
580        self._reflect_indexes(
581            table_name, schema, table, cols_by_orig_name,
582            include_columns, exclude_columns, reflection_options)
583
584        self._reflect_unique_constraints(
585            table_name, schema, table, cols_by_orig_name,
586            include_columns, exclude_columns, reflection_options)
587
588    def _reflect_column(
589        self, table, col_d, include_columns,
590            exclude_columns, cols_by_orig_name):
591
592        orig_name = col_d['name']
593
594        table.dispatch.column_reflect(self, table, col_d)
595
596        # fetch name again as column_reflect is allowed to
597        # change it
598        name = col_d['name']
599        if (include_columns and name not in include_columns) \
600                or (exclude_columns and name in exclude_columns):
601            return
602
603        coltype = col_d['type']
604
605        col_kw = dict(
606            (k, col_d[k])
607            for k in ['nullable', 'autoincrement', 'quote', 'info', 'key']
608            if k in col_d
609        )
610
611        colargs = []
612        if col_d.get('default') is not None:
613            # the "default" value is assumed to be a literal SQL
614            # expression, so is wrapped in text() so that no quoting
615            # occurs on re-issuance.
616            colargs.append(
617                sa_schema.DefaultClause(
618                    sql.text(col_d['default']), _reflected=True
619                )
620            )
621
622        if 'sequence' in col_d:
623            self._reflect_col_sequence(col_d, colargs)
624
625        cols_by_orig_name[orig_name] = col = \
626            sa_schema.Column(name, coltype, *colargs, **col_kw)
627
628        if col.key in table.primary_key:
629            col.primary_key = True
630        table.append_column(col)
631
632    def _reflect_col_sequence(self, col_d, colargs):
633        if 'sequence' in col_d:
634            # TODO: mssql and sybase are using this.
635            seq = col_d['sequence']
636            sequence = sa_schema.Sequence(seq['name'], 1, 1)
637            if 'start' in seq:
638                sequence.start = seq['start']
639            if 'increment' in seq:
640                sequence.increment = seq['increment']
641            colargs.append(sequence)
642
643    def _reflect_pk(
644            self, table_name, schema, table,
645            cols_by_orig_name, exclude_columns):
646        pk_cons = self.get_pk_constraint(
647            table_name, schema, **table.dialect_kwargs)
648        if pk_cons:
649            pk_cols = [
650                cols_by_orig_name[pk]
651                for pk in pk_cons['constrained_columns']
652                if pk in cols_by_orig_name and pk not in exclude_columns
653            ]
654
655            # update pk constraint name
656            table.primary_key.name = pk_cons.get('name')
657
658            # tell the PKConstraint to re-initialize
659            # its column collection
660            table.primary_key._reload(pk_cols)
661
662    def _reflect_fk(
663            self, table_name, schema, table, cols_by_orig_name,
664            exclude_columns, reflection_options):
665        fkeys = self.get_foreign_keys(
666            table_name, schema, **table.dialect_kwargs)
667        for fkey_d in fkeys:
668            conname = fkey_d['name']
669            # look for columns by orig name in cols_by_orig_name,
670            # but support columns that are in-Python only as fallback
671            constrained_columns = [
672                cols_by_orig_name[c].key
673                if c in cols_by_orig_name else c
674                for c in fkey_d['constrained_columns']
675            ]
676            if exclude_columns and set(constrained_columns).intersection(
677                    exclude_columns):
678                continue
679            referred_schema = fkey_d['referred_schema']
680            referred_table = fkey_d['referred_table']
681            referred_columns = fkey_d['referred_columns']
682            refspec = []
683            if referred_schema is not None:
684                sa_schema.Table(referred_table, table.metadata,
685                                autoload=True, schema=referred_schema,
686                                autoload_with=self.bind,
687                                **reflection_options
688                                )
689                for column in referred_columns:
690                    refspec.append(".".join(
691                        [referred_schema, referred_table, column]))
692            else:
693                sa_schema.Table(referred_table, table.metadata, autoload=True,
694                                autoload_with=self.bind,
695                                schema=sa_schema.BLANK_SCHEMA,
696                                **reflection_options
697                                )
698                for column in referred_columns:
699                    refspec.append(".".join([referred_table, column]))
700            if 'options' in fkey_d:
701                options = fkey_d['options']
702            else:
703                options = {}
704            table.append_constraint(
705                sa_schema.ForeignKeyConstraint(constrained_columns, refspec,
706                                               conname, link_to_name=True,
707                                               **options))
708
709    def _reflect_indexes(
710        self, table_name, schema, table, cols_by_orig_name,
711            include_columns, exclude_columns, reflection_options):
712        # Indexes
713        indexes = self.get_indexes(table_name, schema)
714        for index_d in indexes:
715            name = index_d['name']
716            columns = index_d['column_names']
717            unique = index_d['unique']
718            flavor = index_d.get('type', 'index')
719            dialect_options = index_d.get('dialect_options', {})
720
721            duplicates = index_d.get('duplicates_constraint')
722            if include_columns and \
723                    not set(columns).issubset(include_columns):
724                util.warn(
725                    "Omitting %s key for (%s), key covers omitted columns." %
726                    (flavor, ', '.join(columns)))
727                continue
728            if duplicates:
729                continue
730            # look for columns by orig name in cols_by_orig_name,
731            # but support columns that are in-Python only as fallback
732            idx_cols = []
733            for c in columns:
734                try:
735                    idx_col = cols_by_orig_name[c] \
736                        if c in cols_by_orig_name else table.c[c]
737                except KeyError:
738                    util.warn(
739                        "%s key '%s' was not located in "
740                        "columns for table '%s'" % (
741                            flavor, c, table_name
742                        ))
743                else:
744                    idx_cols.append(idx_col)
745
746            sa_schema.Index(
747                name, *idx_cols,
748                **dict(list(dialect_options.items()) + [('unique', unique)])
749            )
750
751    def _reflect_unique_constraints(
752        self, table_name, schema, table, cols_by_orig_name,
753            include_columns, exclude_columns, reflection_options):
754
755        # Unique Constraints
756        try:
757            constraints = self.get_unique_constraints(table_name, schema)
758        except NotImplementedError:
759            # optional dialect feature
760            return
761
762        for const_d in constraints:
763            conname = const_d['name']
764            columns = const_d['column_names']
765            duplicates = const_d.get('duplicates_index')
766            if include_columns and \
767                    not set(columns).issubset(include_columns):
768                util.warn(
769                    "Omitting unique constraint key for (%s), "
770                    "key covers omitted columns." %
771                    ', '.join(columns))
772                continue
773            if duplicates:
774                continue
775            # look for columns by orig name in cols_by_orig_name,
776            # but support columns that are in-Python only as fallback
777            constrained_cols = []
778            for c in columns:
779                try:
780                    constrained_col = cols_by_orig_name[c] \
781                        if c in cols_by_orig_name else table.c[c]
782                except KeyError:
783                    util.warn(
784                        "unique constraint key '%s' was not located in "
785                        "columns for table '%s'" % (c, table_name))
786                else:
787                    constrained_cols.append(constrained_col)
788            table.append_constraint(
789                sa_schema.UniqueConstraint(*constrained_cols, name=conname))
790