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