1 """
2 Description and definition of tables.
3 """
4
5
6
7
8
9
10
11 import functools
12 import re
13
14 from gavo import adql
15 from gavo import base
16 from gavo import dm
17 from gavo import stc
18 from gavo import utils
19 from gavo.rscdef import column
20 from gavo.rscdef import common
21 from gavo.rscdef import group
22 from gavo.rscdef import mixins
23 from gavo.rscdef import rmkfuncs
24
25
26 MS = base.makeStruct
27
28
29 @functools.total_ordering
30 -class DBIndex(base.Structure):
31 """A description of an index in the database.
32
33 In real databases, indices may be fairly complex things; still, the
34 most common usage here will be to just index a single column::
35
36 <index columns="my_col"/>
37
38 To index over functions, use the character content; parentheses are added
39 by DaCHS, so don't have them in the content. An explicit specification
40 of the index expression is also necessary to allow RE pattern matches using
41 indices in character columns (outside of the C locale). That would be::
42
43 <index columns="uri">uri text_pattern_ops</index>
44
45 (you still want to give columns so the metadata engine is aware of the
46 index). See section "Operator Classes and Operator Families" in
47 the Postgres documentation for details.
48
49 For pgsphere-valued columns, you at the time of writing need to specify
50 the method::
51
52 <index columns="coverage" method="GIST"/>
53
54 To define q3c indices, use the ``//scs#q3cindex`` mixin; if you're
55 devious enough to require something more flexible, have a look at
56 that mixin's definition.
57
58 If indexed columns take part in a DaCHS-defined view, DaCHS will not
59 notice. You should still declare the indices so users will see them
60 in the metadata; writing::
61
62 <index columns="col1, col2, col3"/>
63
64 is sufficent for that.
65 """
66 name_ = "index"
67
68 _name = base.UnicodeAttribute("name", default=base.Undefined,
69 description="Name of the index. Defaults to something computed from"
70 " columns; the name of the parent table will be prepended in the DB."
71 " The default will *not* work if you have multiple indices on one"
72 " set of columns.",
73 copyable=True)
74 _columns = base.StringListAttribute("columns", description=
75 "Table columns taking part in the index (must be given even if there"
76 " is an expression building the index and mention all columns taking"
77 " part in the index generated by it", copyable=True)
78 _cluster = base.BooleanAttribute("cluster", default=False,
79 description="Cluster the table according to this index?",
80 copyable=True)
81 _code = base.DataContent(copyable=True, description=
82 "Raw SQL specifying an expression the table should be"
83 " indexed for. If not given, the expression will be generated from"
84 " columns (which is what you usually want).")
85 _method = base.UnicodeAttribute("method", default=None,
86 description="The indexing method, like an index type. In the 8.x,"
87 " series of postgres, you need to set method=GIST for indices"
88 " over pgsphere columns; otherwise, you should not need to"
89 " worry about this.", copyable=True)
90
92 if self.content_ and getattr(ctx, "restricted", False):
93 raise base.RestrictedElement("index", hint="Free-form SQL on indices"
94 " is not allowed in restricted mode")
95 self._completeElementNext(DBIndex, ctx)
96
97 if not self.columns and not self.content_:
98 raise base.StructureError("Index without columns is verboten.")
99
100 if self.name is base.Undefined:
101 self.name = "%s"%(re.sub("[^\w]+", "_", "_".join(self.columns)))
102
103 if not self.content_:
104 self.content_ = "%s"%",".join(self.columns)
105
106
107
108
110 return id(self)==id(other)
111
113 othercluster = getattr(other, "cluster", None)
114 if ((self.cluster and othercluster)
115 or (not self.cluster and not othercluster)):
116 return id(self)<id(other)
117 else:
118 if self.cluster:
119 return True
120 else:
121 return False
122
124 destTableName = self.parent.getQName()
125 usingClause = ""
126 if self.method is not None:
127 usingClause = " USING %s"%self.method
128 yield self.parent.expand("CREATE INDEX %s ON %s%s (%s)"%(
129 self.dbname, destTableName, usingClause, self.content_))
130 if self.cluster:
131 yield self.parent.expand(
132 "CLUSTER %s ON %s"%(self.dbname, destTableName))
133
146
147 - def drop(self, querier):
148 """drops the index if it exists.
149
150 querier is an object mixing in the DBMethodsMixin, usually the
151 DBTable object the index possibly exists on.
152 """
153 iName = self.parent.expand(self.dbname)
154 if querier.hasIndex(self.parent.getQName(), iName):
155 querier.query("DROP INDEX %s.%s"%(self.parent.rd.schema, iName))
156
157 @property
160
163 """is a tuple of column names.
164
165 In a validate method, it checks that the names actually are in parent's
166 fields.
167 """
169 """adds a getPrimaryIn method to the parent class.
170
171 This function will return the value of the primary key in a row
172 passed. The whole thing is a bit dense in that I want to compile
173 that method to avoid having to loop every time it is called. This
174 compilation is done in a descriptor -- ah well, probably it's a waste
175 of time anyway.
176 """
177 def makeGetPrimaryFunction(instance):
178 funcSrc = ('def getPrimaryIn(row):\n'
179 ' return (%s)')%(" ".join(['row["%s"],'%name
180 for name in getattr(instance, self.name_)]))
181 return utils.compileFunction(funcSrc, "getPrimaryIn")
182
183 def getPrimaryIn(self, row):
184 try:
185 return self.__getPrimaryIn(row)
186 except AttributeError:
187 self.__getPrimaryIn = makeGetPrimaryFunction(self)
188 return self.__getPrimaryIn(row)
189 yield "getPrimaryIn", getPrimaryIn
190
198
201 """A description of a foreign key relation between this table and another
202 one.
203 """
204 name_ = "foreignKey"
205
206 _inTable = base.ReferenceAttribute("inTable", default=base.Undefined,
207 description="Reference to the table the foreign key points to.",
208 copyable=True)
209 _source = base.UnicodeAttribute("source", default=base.Undefined,
210 description="Comma-separated list of local columns corresponding"
211 " to the foreign key. No sanity checks are performed here.",
212 copyable=True)
213 _dest = base.UnicodeAttribute("dest", default=base.NotGiven,
214 description="Comma-separated list of columns in the target table"
215 " belonging to its key. No checks for their existence, uniqueness,"
216 " etc. are done here. If not given, defaults to source.")
217 _metaOnly = base.BooleanAttribute("metaOnly", default=False,
218 description="Do not tell the database to actually create the foreign"
219 " key, just declare it in the metadata. This is for when you want"
220 " to document a relationship but don't want the DB to actually"
221 " enforce this. This is typically a wise thing to do when you have, say"
222 " a gigarecord of flux/density pairs and only several thousand metadata"
223 " records -- you may want to update the latter without having"
224 " to tear down the former.")
225
227 return "%s:%s -> %s:%s"%(self.parent.getQName(), ",".join(self.source),
228 self.destTableName, ".".join(self.dest))
229
231 if isinstance(raw, list):
232
233 return raw
234 return [s.strip() for s in raw.split(",") if s.strip()]
235
237 self.destTableName = self.inTable.getQName()
238 self.isADQLKey = self.inTable.adql and self.inTable.adql!='hidden'
239
240 self.source = self._parseList(self.source)
241 if self.dest is base.NotGiven:
242 self.dest = self.source
243 else:
244 self.dest = self._parseList(self.dest)
245 self._onElementCompleteNext(ForeignKey)
246
248 if self.metaOnly:
249 return
250
251 if not querier.foreignKeyExists(self.parent.getQName(),
252 self.destTableName,
253 self.source,
254 self.dest):
255 return querier.query("ALTER TABLE %s ADD FOREIGN KEY (%s)"
256 " REFERENCES %s (%s)"
257 " ON DELETE CASCADE"
258 " DEFERRABLE INITIALLY DEFERRED"%(
259 self.parent.getQName(),
260 ",".join(self.source),
261 self.destTableName,
262 ",".join(self.dest)))
263
265 if self.metaOnly:
266 return
267
268 try:
269 constraintName = querier.getForeignKeyName(self.parent.getQName(),
270 self.destTableName, self.source, self.dest)
271 except (ValueError, base.DBError):
272 return
273 querier.query("ALTER TABLE %s DROP CONSTRAINT %s"%(self.parent.getQName(),
274 constraintName))
275
280
281
282 -class STCDef(base.Structure):
283 """A definition of a space-time coordinate system using STC-S.
284 """
285
286
287
288
289
290 name_ = "stc"
291
292 _source = base.DataContent(copyable=True, description="An STC-S string"
293 " with column references (using quote syntax) instead of values")
294
306
309
312 """An attribute that has values True/False and hidden.
313 """
314 typeDesc_ = "boolean or 'hidden'"
315
321
326
331
334 """A mixin with a few classes and attributes for data that can be
335 published to the VO registry.
336
337 In particular, this contains the publish element (registration attribute).
338 """
339 _registration = base.StructAttribute("registration",
340 default=None,
341 childFactory=common.Registration,
342 copyable=False,
343 description="A registration (to the VO registry) of this table"
344 " or data collection.")
345
347 """returns a sequence of publication elements for the data, suitable
348 for OAI responses for the sets setNames.
349
350 Essentially: if registration is None, or its sets don't match
351 setNames, return an emtpy sequence.
352
353 If the registration mentions services, we turn their publications
354 into auxiliary publications and yield them
355
356 Otherwise, if we're published for ADQL, return the TAP service
357 as an auxiliary publication.
358 """
359 if (self.registration is None
360 or not self.registration.sets & setNames):
361 return
362
363 services = self.registration.services
364 if not services:
365 services = [base.resolveCrossId("//tap#run")]
366
367 for service in services:
368 for pub in service.getPublicationsForSet(setNames):
369 copy = pub.change(parent_=self, auxiliary=True)
370 copy.meta_ = self.registration.meta_
371 yield copy
372
373
374 -class TableDef(base.Structure, base.ComputedMetaMixin, common.PrivilegesMixin,
375 common.IVOMetaMixin, base.StandardMacroMixin, PublishableDataMixin):
376 """A definition of a table, both on-disk and internal.
377
378 Some attributes are ignored for in-memory tables, e.g., roles or adql.
379
380 Properties for tables:
381
382 * supportsModel -- a short name of a data model supported through this
383 table (for TAPRegExt dataModel); you can give multiple names separated
384 by commas.
385 * supportsModelURI -- a URI of a data model supported through this table.
386 You can give multiple URIs separated by blanks.
387
388 If you give multiple data model names or URIs, the sequences of names and
389 URIs must be identical (in particular, each name needs a URI).
390 """
391 name_ = "table"
392
393 resType = "table"
394
395
396
397
398 _id = base.IdAttribute("id",
399 default=base.NotGiven,
400 description="Name of the table (must be SQL-legal for onDisk tables)")
401
402 _cols = common.ColumnListAttribute("columns",
403 childFactory=column.Column,
404 description="Columns making up this table.",
405 copyable=True)
406
407 _params = common.ColumnListAttribute("params",
408 childFactory=column.Param,
409 description='Param ("global columns") for this table.',
410 copyable=True)
411
412 _viewStatement = base.UnicodeAttribute("viewStatement",
413 default=None,
414 description="A single SQL statement to create a view. Setting this"
415 " makes this table a view. The statement will typically be something"
416 " like CREATE VIEW \\\\qName AS (SELECT \\\\colNames FROM...).",
417 copyable=True)
418
419
420
421 _onDisk = base.BooleanAttribute("onDisk",
422 default=False,
423 description="Table in the database rather than in memory?")
424
425 _temporary = base.BooleanAttribute("temporary",
426 default=False,
427 description="If this is an onDisk table, make it temporary?"
428 " This is mostly useful for custom cores and such.",
429 copyable=True)
430
431 _adql = ADQLVisibilityAttribute("adql",
432 default=False,
433 description="Should this table be available for ADQL queries? In"
434 " addition to True/False, this can also be 'hidden' for tables"
435 " readable from the TAP machinery but not published in the"
436 " metadata; this is useful for, e.g., tables contributing to a"
437 " published view. Warning: adql=hidden is incompatible with setting"
438 " readProfiles manually.")
439
440 _system = base.BooleanAttribute("system",
441 default=False,
442 description="Is this a system table? If it is, it will not be"
443 " dropped on normal imports, and accesses to it will not be logged.")
444
445 _forceUnique = base.BooleanAttribute("forceUnique",
446 default=False,
447 description="Enforce dupe policy for primary key (see dupePolicy)?")
448
449 _dupePolicy = base.EnumeratedUnicodeAttribute("dupePolicy",
450 default="check",
451 validValues=["check", "drop", "overwrite", "dropOld"],
452 description= "Handle duplicate rows with identical primary keys manually"
453 " by raising an error if existing and new rows are not identical (check),"
454 " dropping the new one (drop), updating the old one (overwrite), or"
455 " dropping the old one and inserting the new one (dropOld)?")
456
457 _primary = ColumnTupleAttribute("primary",
458 default=(),
459 description="Comma separated names of columns making up the primary key.",
460 copyable=True)
461
462 _indices = base.StructListAttribute("indices",
463 childFactory=DBIndex,
464 description="Indices defined on this table",
465 copyable=True)
466
467 _foreignKeys = base.StructListAttribute("foreignKeys",
468 childFactory=ForeignKey,
469 description="Foreign keys used in this table",
470 copyable=False)
471
472 _groups = base.StructListAttribute("groups",
473 childFactory=group.Group,
474 description="Groups for columns and params of this table",
475 copyable=True)
476
477 _nrows = base.IntAttribute("nrows",
478 description="Approximate number of rows in this table (usually,"
479 " you want to use dachs limits to fill this out; write <nrows>0</nrows>"
480 " to enable that).")
481
482
483
484 _annotations = dm.DataModelRolesAttribute()
485
486 _properties = base.PropertyAttribute()
487
488
489
490
491 _stcs = base.StructListAttribute("stc", description="STC-S definitions"
492 " of coordinate systems.", childFactory=STCDef)
493
494 _rd = common.RDAttribute()
495 _mixins = mixins.MixinAttribute()
496 _original = base.OriginalAttribute()
497 _namePath = common.NamePathAttribute()
498
499 fixupFunction = None
500
501 metaModel = ("title(1), creationDate(1), description(1),"
502 "subject, referenceURL(1)")
503
504 @classmethod
506 """returns a TableDef from a sequence of columns.
507
508 You can give additional constructor arguments. makeStruct is used
509 to build the instance, the mixin hack is applied.
510
511 Columns with identical names will be disambiguated.
512 """
513 res = MS(cls,
514 columns=common.ColumnList(cls.disambiguateColumns(columns)),
515 **kwargs)
516 return res
517
519 return iter(self.columns)
520
527
529 try:
530 return "<Table definition of %s>"%self.getQName()
531 except base.Error:
532 return "<Non-RD table %s>"%self.id
533
562
568
570 if self.adql:
571 self.readProfiles = (self.readProfiles |
572 base.getConfig("db", "adqlProfiles"))
573 self.dictKeys = [c.key for c in self]
574
575 self.indexedColumns = set()
576 for index in self.indices:
577 for col in index.columns:
578 if "\\" in col:
579 try:
580 self.indexedColumns.add(self.expand(col))
581 except (base.Error, ValueError):
582 pass
583 else:
584 self.indexedColumns.add(col)
585 if self.primary:
586 self.indexedColumns |= set(self.primary)
587
588 self._defineFixupFunction()
589
590 self._onElementCompleteNext(TableDef)
591
592 if self.registration:
593 self.registration.register()
594
595
596
597 if not self.annotations:
598 self.updateAnnotationFromChildren()
599
600
601
602
603 self.indices.sort()
604
606 """returns the first of column and param having name name.
607
608 The function raises a NotFoundError if neiter column nor param with
609 name exists.
610 """
611 try:
612 try:
613 return self.columns.getColumnByName(name)
614 except base.NotFoundError:
615 return self.params.getColumnByName(name)
616 except base.NotFoundError as ex:
617 ex.within = "table %s"%self.id
618 raise
619
621 """adds STC related attributes to this tables' columns.
622 """
623 for stcDef in self.stc:
624 for name, type in stcDef.iterColTypes():
625 destCol = self.getColumnByName(name)
626 if destCol.stc is not None:
627
628
629 continue
630
631
632
633
634 destCol.stc = stcDef.compiled
635 destCol.stcUtype = type
636
638 """defines a function to fix up records from column's fixup attributes.
639
640 This will leave a fixupFunction attribute which will be None if
641 no fixups are defined.
642 """
643 fixups = []
644 for col in self:
645 if col.fixup is not None:
646 fixups.append((col.name, col.fixup))
647 if fixups:
648 assignments = []
649 for key, expr in fixups:
650 expr = expr.replace("___", "row['%s']"%key)
651 assignments.append(" row['%s'] = %s"%(key, expr))
652 source = self.expand(
653 "def fixup(row):\n%s\n return row"%("\n".join(assignments)))
654 self.fixupFunction = rmkfuncs.makeProc("fixup", source,
655 "", None)
656
658 if self.temporary:
659 return self.id
660 else:
661 if self.rd is None:
662 raise base.Error("TableDefs without resource descriptor"
663 " have no qualified names")
664 return "%s.%s"%(self.rd.schema, self.id)
665
667 """checks that row is complete and complies with all known constraints on
668 the columns
669
670 The function raises a ValidationError with an appropriate message
671 and the relevant field if not.
672 """
673 for col in self:
674 if col.key not in row:
675 raise base.ValidationError("Column %s missing"%col.name,
676 col.name, row, hint="The table %s has a column named '%s',"
677 " but the input row %s does not give it. This typically means"
678 " bad input or a rowmaker failing on some corner case."%(
679 self.id, col.name, row))
680 try:
681 col.validateValue(row[col.key])
682 except base.ValidationError as ex:
683 ex.row = row
684 raise
685
687 """returns the index of the field named fieldName.
688 """
689 return self.columns.getFieldIndex(fieldName)
690
693
696
699
702
705
708
714
716 """returns the column or param with utype.
717
718 This is supposed to be unique, but the function will just return
719 the first matching item it finds.
720 """
721 try:
722 return self.params.getColumnByUtype(utype)
723 except base.NotFoundError:
724 return self.columns.getColumnByUtype(utype)
725
727 """returns the first param or column matching the first utype
728 matching anything.
729 """
730 for utype in utypes:
731 try:
732 return self.getByUtype(utype)
733 except base.NotFoundError:
734 pass
735 raise base.NotFoundError(", ".join(utypes),
736 what="param or column with utype in",
737 within="table %s"%self.id)
738
740 """returns the column or param with name.
741
742 There is nothing keeping you from having both a column and a param with
743 the same name. If that happens, you will only see the column. But
744 don't do it.
745 """
746 try:
747 return self.columns.getColumnByName(name)
748 except base.NotFoundError:
749 return self.params.getColumnByName(name)
750
752 """returns a row (dict) from a row as returned from the database.
753 """
754 preRes = dict(zip(self.dictKeys, dbTuple))
755 if self.fixupFunction:
756 return self.fixupFunction(preRes)
757 return preRes
758
760 """returns a mapping from column names to defaults to be used when
761 making a row for this table.
762 """
763 defaults = {}
764 for col in self:
765 if col.values:
766 defaults[col.name] = col.values.default
767 elif not col.required:
768 defaults[col.name] = None
769 return defaults
770
772 """returns a set of all STC specs referenced in this table as ASTs.
773 """
774
775
776 stcObjects = utils.uniqueItems(col.stc for col in self)
777 if None in stcObjects:
778 stcObjects.remove(None)
779 return stcObjects
780
782 """returns the table note meta value for noteTag.
783
784 This will raise a NotFoundError if we don't have such a note.
785
786 You will not usually use this to retrieve meta items since columns
787 have the meta values in their note attributes. Columns, of course,
788 use this to get their note attribute value.
789 """
790 mi = self.getMeta("note") or []
791 for mv in mi:
792 if mv.tag==noteTag:
793 return mv
794 else:
795 raise base.NotFoundError(noteTag, what="note tag",
796 within="table %s"%self.id)
797
798 - def getURL(self, rendName, absolute=True):
799 """returns the URL DaCHS will show the table info page for this table
800 under.
801
802 Of course the URL is only valid for imported tables.
803 """
804 basePath = "%stableinfo/%s"%(
805 base.getConfig("web", "nevowRoot"),
806 self.getQName())
807 if absolute:
808 basePath = base.makeAbsoluteURL(basePath)
809 return basePath
810
812 """returns an SQL statement that creates the table.
813 """
814 preTable = ""
815 if self.temporary:
816 preTable = "TEMP "
817 statement = "CREATE %sTABLE %s (%s)"%(
818 preTable,
819 self.getQName(),
820 ", ".join(column.getDDL() for column in self))
821 return statement
822
823 - def getSimpleQuery(self,
824 selectClause=None,
825 fragments="",
826 postfix=""):
827 """returns a query against this table.
828
829 selectClause is a list of column names (in which case the names
830 are validated against the real column names and you can use
831 user input) or a literal string (in which case you must not provide
832 user input or have a SQL injection hole).
833
834 fragments (the WHERE CLAUSE) and postfix are taken as literal strings (so
835 they must not contain user input).
836
837 This is purely a string operation, so you'll have your normal
838 value references in fragments and postfix, and should maintain
839 the parameter dictionaries as usual.
840
841 All parts are optional, defaulting to pulling the entire table.
842 """
843 parts = ["SELECT"]
844
845 if selectClause is None:
846 parts.append("*")
847 elif isinstance(selectClause, list):
848 parts.append(", ".join(
849 self.getColumnByName(colName).name for colName in selectClause))
850 else:
851 parts.append(selectClause)
852
853 parts.append("FROM %s"%self.getQName())
854
855 if fragments:
856 parts.append("WHERE %s"%fragments)
857
858 if postfix:
859 parts.append(postfix)
860
861 return " ".join(parts)
862
863 @property
866
867 - def doSimpleQuery(self,
868 selectClause=None,
869 fragments="",
870 params=None,
871 postfix=""):
872 """runs a query generated via getSimpleQuery and returns a list
873 of rowdicts.
874
875 This uses a table connection and queryToDicts; the keys in the
876 dictionaries will have the right case for this table's columns, though.
877
878 params is a dictionary of fillers for fragments and postfix.
879 """
880 with base.getTableConn() as conn:
881 return list(
882 conn.queryToDicts(
883 self.getSimpleQuery(
884 selectClause,
885 fragments,
886 postfix),
887 params,
888 caseFixer=self.caseFixer))
889
891 """returns an SQL-ready list of column names of this table.
892 """
893 return ", ".join(c.name for c in self.columns)
894
896 """returns the qualified name of the current table.
897
898 (this is identical to the `macro qName`_, which you should prefer
899 in new RDs.)
900 """
901 return self.getQName()
902
904 """returns the qualified name of the current table.
905 """
906 return self.getQName()
907
909 """returns the unqualified name of the current table.
910
911 In most contexts, you will probably need to use the `macro qName`_
912 instead of this.
913 """
914 return self.id
915
917 """returns the (unique!) name of the field having ucd in this table.
918
919 If there is no or more than one field with the ucd in this table,
920 we raise a ValueError.
921 """
922 return self.getColumnByUCD(ucd).name
923
925 """returns the (unique!) name of the field having one
926 of ucds in this table.
927
928 Ucds is a selection of ucds separated by vertical bars
929 (|). The rules for when this raises errors are so crazy
930 you don't want to think about them. This really is
931 only intended for cases where "old" and "new" standards
932 are to be supported, like with pos.eq.*;meta.main and
933 POS_EQ_*_MAIN.
934
935 If there is no or more than one field with the ucd in
936 this table, we raise an exception.
937 """
938 return self.getColumnByUCDs(*(s.strip() for s in ucds.split("|"))).name
939
941 """returns the string representation of the parameter parName.
942
943 This is the parameter as given in the table definition. Any changes
944 to an instance are not reflected here.
945
946 If the parameter named does not exist, an empty string is returned.
947 NULLs/Nones are rendered as NULL; this is mainly a convenience
948 for obscore-like applications and should not be exploited otherwise,
949 since it's ugly and might change at some point.
950
951 If a default is given, it will be returned for both NULL and non-existing
952 params.
953 """
954 try:
955 param = self.params.getColumnByName(parName)
956 except base.NotFoundError:
957 return default
958 if param.content_ is base.NotGiven or param.value is None:
959 return default or "NULL"
960 else:
961 return param.content_
962
963 @staticmethod
965 """returns a sequence of columns without duplicate names.
966 """
967 newColumns, seenNames = [], set()
968 for c in columns:
969 while c.name in seenNames:
970 c.name = c.name+"_"
971 newColumns.append(c)
972 seenNames.add(c.name)
973 return newColumns
974
985
992
995 """returns a TableDef object named names and having the columns cols.
996
997 cols is some sequence of Column objects. You can give arbitrary
998 table attributes in keyword arguments.
999 """
1000 kws = {"id": name, "columns": common.ColumnList(cols)}
1001 kws.update(moreKWs)
1002 return base.makeStruct(TableDef, **kws)
1003