Package gavo :: Package adql :: Module nodes
[frames] | no frames]

Source Code for Module gavo.adql.nodes

   1  """ 
   2  Node classes and factories used in ADQL tree processing. 
   3  """ 
   4   
   5  #c Copyright 2008-2019, the GAVO project 
   6  #c 
   7  #c This program is free software, covered by the GNU GPL.  See the 
   8  #c COPYING file in the source distribution. 
   9   
  10   
  11  import fnmatch 
  12  import re 
  13  import weakref 
  14   
  15  import pyparsing 
  16   
  17  from gavo import stc 
  18  from gavo import utils 
  19  from gavo.adql import common 
  20  from gavo.adql import fieldinfo 
  21  from gavo.adql import fieldinfos 
  22  from gavo.stc import tapstc 
23 24 25 ################ Various helpers 26 27 -class ReplaceNode(utils.ExecutiveAction):
28 """can be raised by code in the constructor of and ADQLNode to replace 29 itself. 30 31 It is constructed with the (single) ADQLNode that should stand in its 32 stead. 33 34 This is intended as a special service for ufuncs that want to insert 35 complex, annotatable expressions. I doubt this is something 36 we should do under other circumstances. 37 """
38 - def __init__(self, replacingNode):
39 self.replacingNode = replacingNode
40
41 42 -def symbolAction(*symbols):
43 """is a decorator to mark functions as being a parseAction for symbol. 44 45 This is evaluated by getADQLGrammar below. Be careful not to alter 46 global state in such a handler. 47 """ 48 def deco(func): 49 for symbol in symbols: 50 if hasattr(func, "parseActionFor"): 51 func.parseActionFor.append(symbol) 52 else: 53 func.parseActionFor = [symbol] 54 func.fromParseResult = func 55 return func
56 return deco 57
58 59 -def getType(arg):
60 """returns the type of an ADQL node or the value of str if arg is a string. 61 """ 62 if isinstance(arg, basestring): 63 return str 64 else: 65 return arg.type
66
67 68 -def flatten(arg):
69 """returns the SQL serialized representation of arg. 70 """ 71 if isinstance(arg, basestring): 72 return arg 73 elif isinstance(arg, (int, float)): 74 return str(arg) 75 elif isinstance(arg, pyparsing.ParseResults): 76 return " ".join(flatten(c) for c in arg) 77 else: 78 return arg.flatten()
79
80 81 -def autocollapse(nodeBuilder, children):
82 """inhibts the construction via nodeBuilder if children consists of 83 a single ADQLNode. 84 85 This function will automatically be inserted into the the constructor 86 chain if the node defines an attribute collapsible=True. 87 """ 88 if len(children)==1 and isinstance(children[0], ADQLNode): 89 return children[0] 90 return nodeBuilder.fromParseResult(children)
91
92 93 -def collectUserData(infoChildren):
94 userData, tainted = (), False 95 for c in infoChildren: 96 userData = userData+c.fieldInfo.userData 97 tainted = tainted or c.fieldInfo.tainted 98 return userData, tainted
99
100 101 -def flattenKWs(obj, *fmtTuples):
102 """returns a string built from the obj according to format tuples. 103 104 A format tuple is consists of a literal string, and 105 an attribute name. If the corresponding attribute is 106 non-None, the plain string and the flattened attribute 107 value are inserted into the result string, otherwise 108 both are ignored. 109 110 Nonexisting attributes are taken to have None values. 111 112 To allow unconditional literals, the attribute name can 113 be None. The corresponding literal is always inserted. 114 115 All contributions are separated by single blanks. 116 117 This is a helper method for flatten methods of parsed-out 118 elements. 119 """ 120 res = [] 121 for literal, attName in fmtTuples: 122 if attName is None: 123 res.append(literal) 124 else: 125 if getattr(obj, attName, None) is not None: 126 if literal: 127 res.append(literal) 128 res.append(flatten(getattr(obj, attName))) 129 return " ".join(res)
130
131 132 -def cleanNamespace(ns):
133 """removes all names starting with an underscore from the dict ns. 134 135 This is intended for _getInitKWs methods. ns is changed in place *and* 136 returned for convenience 137 """ 138 return dict((k,v) for k,v in ns.iteritems() if not k.startswith("_") 139 and k!="cls")
140
141 142 -def getChildrenOfType(nodeSeq, type):
143 """returns a list of children of type typ in the sequence nodeSeq. 144 """ 145 return [c for c in nodeSeq if getType(c)==type]
146
147 148 -def getChildrenOfClass(nodeSeq, cls):
149 return [c for c in nodeSeq if isinstance(c, cls)]
150
151 152 -class BOMB_OUT(object): pass
153
154 -def _uniquify(matches, default, exArgs):
155 # helper method for getChildOfX -- see there 156 if len(matches)==0: 157 if default is not BOMB_OUT: 158 return default 159 raise common.NoChild(*exArgs) 160 if len(matches)!=1: 161 raise common.MoreThanOneChild(*exArgs) 162 return matches[0]
163
164 165 -def getChildOfType(nodeSeq, type, default=BOMB_OUT):
166 """returns the unique node of type in nodeSeq. 167 168 If there is no such node in nodeSeq or more than one, a NoChild or 169 MoreThanOneChild exception is raised, Instead of raising NoChild, 170 default is returned if given. 171 """ 172 return _uniquify(getChildrenOfType(nodeSeq, type), 173 default, (type, nodeSeq))
174
175 176 -def getChildOfClass(nodeSeq, cls, default=BOMB_OUT):
177 """returns the unique node of class in nodeSeq. 178 179 See getChildOfType. 180 """ 181 return _uniquify(getChildrenOfClass(nodeSeq, cls), 182 default, (cls, nodeSeq))
183
184 185 -def parseArgs(parseResult):
186 """returns a sequence of ADQL nodes suitable as function arguments from 187 parseResult. 188 189 This is for cleaning up _parseResults["args"], i.e. stuff from the 190 Args symbol decorator in grammar. 191 """ 192 args = [] 193 for _arg in parseResult: 194 # _arg is either another ParseResult, an ADQL identifier, or an ADQLNode 195 if isinstance(_arg, (ADQLNode, basestring, utils.QuotedName)): 196 args.append(_arg) 197 else: 198 args.append(autocollapse(GenericValueExpression, _arg)) 199 return tuple(args)
200
201 202 ######################### Generic Node definitions 203 204 205 -class ADQLNode(utils.AutoNode):
206 """A node within an ADQL parse tree. 207 208 ADQL nodes may be parsed out; in that case, they have individual attributes 209 and are craftily flattened in special methods. We do this for nodes 210 that are morphed. 211 212 Other nodes basically just have a children attribute, and their flattening 213 is just a concatenation for their flattened children. This is convenient 214 as long as they are not morphed. 215 216 To derive actual classes, define 217 218 - the _a_<name> class attributes you need, 219 - the type (a nonterminal from the ADQL grammar) 220 - plus bindings if the class handles more than one symbol, 221 - a class method _getInitKWs(cls, parseResult); see below. 222 - a method flatten() -> string if you define a parsed ADQLNode. 223 - a method _polish() that is called just before the constructor is 224 done and can be used to create more attributes. There is no need 225 to call _polish of superclasses. 226 227 The _getInitKWs methods must return a dictionary mapping constructor argument 228 names to values. You do not need to manually call superclass _getInitKWs, 229 since the fromParseResult classmethod figures out all _getInitKWs in the 230 inheritance tree itself. It calls all of them in the normal MRO and updates 231 the argument dictionary in reverse order. 232 233 The fromParseResult class method additionally filters out all names starting 234 with an underscore; this is to allow easy returning of locals(). 235 """ 236 type = None 237 238 @classmethod
239 - def fromParseResult(cls, parseResult):
240 initArgs = {} 241 for superclass in reversed(cls.mro()): 242 if hasattr(superclass, "_getInitKWs"): 243 initArgs.update(superclass._getInitKWs(parseResult)) 244 try: 245 return cls(**cleanNamespace(initArgs)) 246 except TypeError: 247 raise common.BadKeywords("%s, %s"%(cls, cleanNamespace(initArgs))) 248 except ReplaceNode as rn: 249 return rn.replacingNode
250
251 - def _setupNode(self):
252 for cls in reversed(self.__class__.mro()): 253 if hasattr(cls, "_polish"): 254 cls._polish(self) 255 self._setupNodeNext(ADQLNode)
256
257 - def __repr__(self):
258 return "<ADQL Node %s>"%(self.type)
259
260 - def flatten(self):
261 """returns a string representation of the text content of the tree. 262 263 This default implementation will only work if you returned all parsed 264 elements as children. This, in turn, is something you only want to 265 do if you are sure that the node is question will not be morphed. 266 267 Otherwise, override it to create an SQL fragment out of the parsed 268 attributes. 269 """ 270 return " ".join(flatten(c) for c in self.children)
271
272 - def getFlattenedChildren(self):
273 """returns a list of all preterminal children of all children of self. 274 275 A child is preterminal if it has string content. 276 """ 277 fl = [c for c in self.children if isinstance(c, basestring)] 278 def recurse(node): 279 for c in node.children: 280 if isinstance(c, ADQLNode): 281 if c.isPreterminal(): 282 fl.append(c) 283 recurse(c)
284 recurse(self) 285 return fl
286
287 - def asTree(self):
288 res = [] 289 for name, val in self.iterChildren(): 290 if isinstance(val, ADQLNode): 291 res.append(val.asTree()) 292 return self._treeRepr()+tuple(res)
293
294 - def _treeRepr(self):
295 return (self.type,)
296
297 - def iterTree(self):
298 for name, val in self.iterChildren(): 299 if isinstance(val, ADQLNode): 300 for item in val.iterTree(): 301 yield item 302 yield name, val
303
304 305 -class TransparentMixin(object):
306 """a mixin just pulling through the children and serializing them. 307 """ 308 _a_children = () 309 310 @classmethod
311 - def _getInitKWs(cls, _parseResult):
312 return {"children": list(_parseResult)}
313
314 315 -class FieldInfoedNode(ADQLNode):
316 """An ADQL node that carries a FieldInfo. 317 318 This is true for basically everything in the tree below a derived 319 column. This class is the basis for column annotation. 320 321 You'll usually have to override addFieldInfo. The default implementation 322 just looks in its immediate children for anything having a fieldInfo, 323 and if there's exactly one such child, it adopts that fieldInfo as 324 its own, not changing anything. 325 326 FieldInfoedNode, when change()d, keep their field info. This is usually 327 what you want when morphing, but sometimes you might need adjustments. 328 """ 329 fieldInfo = None 330
331 - def _getInfoChildren(self):
332 return [c for c in self.iterNodeChildren() if hasattr(c, "fieldInfo")]
333
334 - def addFieldInfo(self, context):
335 infoChildren = self._getInfoChildren() 336 if len(infoChildren)==1: 337 self.fieldInfo = infoChildren[0].fieldInfo 338 else: 339 if len(infoChildren): 340 msg = "More than one" 341 else: 342 msg = "No" 343 raise common.Error("%s child with fieldInfo with" 344 " no behaviour defined in %s, children %s"%( 345 msg, 346 self.__class__.__name__, 347 list(self.iterChildren())))
348
349 - def change(self, **kwargs):
350 other = ADQLNode.change(self, **kwargs) 351 other.fieldInfo = self.fieldInfo 352 return other
353
354 355 -class FunctionNode(FieldInfoedNode):
356 """An ADQLNodes having a function name and arguments. 357 358 The rules having this as action must use the Arg "decorator" in 359 grammar.py around their arguments and must have a string-valued 360 result "fName". 361 362 FunctionNodes have attributes args (unflattened arguments), 363 and funName (a string containing the function name, all upper 364 case). 365 """ 366 _a_args = () 367 _a_funName = None 368 369 @classmethod
370 - def _getInitKWs(cls, _parseResult):
371 try: 372 args = parseArgs(_parseResult["args"]) #noflake: locals returned 373 except KeyError: # Zero-Arg function 374 pass 375 funName = _parseResult["fName"].upper() #noflake: locals returned 376 return locals()
377
378 - def flatten(self):
379 return "%s(%s)"%(self.funName, ", ".join(flatten(a) for a in self.args))
380
381 382 -class ColumnBearingNode(ADQLNode):
383 """A Node types defining selectable columns. 384 385 These are tables, subqueries, etc. This class is the basis for the 386 annotation of tables and subqueries. 387 388 Their getFieldInfo(name)->fi method gives annotation.FieldInfos 389 objects for their columns, None for unknown columns. 390 391 These keep their fieldInfos on a change() 392 """ 393 fieldInfos = None 394 originalTable = None 395
396 - def getFieldInfo(self, name):
397 if self.fieldInfos: 398 return self.fieldInfos.getFieldInfo(name)
399
400 - def getAllNames(self):
401 """yields all relation names mentioned in this node. 402 """ 403 raise TypeError("Override getAllNames for ColumnBearingNodes.")
404
405 - def change(self, **kwargs):
406 other = ADQLNode.change(self, **kwargs) 407 other.fieldInfos = self.fieldInfos 408 return other
409
410 411 ############# Toplevel query language node types (for query analysis) 412 413 -class TableName(ADQLNode):
414 type = "tableName" 415 _a_cat = None 416 _a_schema = None 417 _a_name = None 418
419 - def __eq__(self, other):
420 if hasattr(other, "qName"): 421 return self.qName.lower()==other.qName.lower() 422 try: 423 return self.qName.lower()==other.lower() 424 except AttributeError: 425 # other has no lower, so it's neither a string nor a table name; 426 # thus, fall through to non-equal case 427 pass 428 return False
429
430 - def __ne__(self, other):
431 return not self==other
432
433 - def __nonzero__(self):
434 return bool(self.name)
435
436 - def __str__(self):
437 return "TableName(%s)"%self.qName
438
439 - def _polish(self):
440 # Implementation detail: We map tap_upload to temporary tables 441 # here; therefore, we can just nil out anything called tap_upload. 442 # If we need more flexibility, this probably is the place to implement 443 # the mapping. 444 if self.schema and self.schema.lower()=="tap_upload": 445 self.schema = None 446 447 self.qName = ".".join(flatten(n) 448 for n in (self.cat, self.schema, self.name) if n)
449 450 @classmethod
451 - def _getInitKWs(cls, _parseResult):
452 _parts = _parseResult[::2] 453 cat, schema, name = [None]*(3-len(_parts))+_parts 454 return locals()
455
456 - def flatten(self):
457 return self.qName
458
459 - def lower(self):
460 """returns self's qualified name in lower case. 461 """ 462 return self.qName.lower()
463 464 @staticmethod
465 - def _normalizePart(part):
466 if isinstance(part, utils.QuotedName): 467 return part.name 468 else: 469 return part.lower()
470
471 - def getNormalized(self):
472 """returns self's qualified name lowercased for regular identifiers, 473 in original capitalisation otherwise. 474 """ 475 return ".".join(self._normalizePart(p) 476 for p in [self.cat, self.schema, self.name] 477 if p is not None)
478
479 480 -class PlainTableRef(ColumnBearingNode):
481 """A reference to a simple table. 482 483 The tableName is the name this table can be referenced as from within 484 SQL, originalName is the name within the database; they are equal unless 485 a correlationSpecification has been given. 486 """ 487 type = "possiblyAliasedTable" 488 _a_tableName = None # a TableName instance 489 _a_originalTable = None # a TableName instance 490 491 @classmethod
492 - def _getInitKWs(cls, _parseResult):
493 if _parseResult.get("alias"): 494 tableName = TableName(name=_parseResult.get("alias")) 495 originalTable = _parseResult.get("tableName") 496 else: 497 tableName = getChildOfType(_parseResult, "tableName") 498 originalTable = tableName #noflake: locals returned 499 return locals()
500
501 - def addFieldInfos(self, context):
502 self.fieldInfos = fieldinfos.TableFieldInfos.makeForNode(self, context)
503
504 - def _polish(self):
505 self.qName = flatten(self.tableName)
506
507 - def flatten(self):
508 ot = flatten(self.originalTable) 509 if ot!=self.qName: 510 return "%s AS %s"%(ot, flatten(self.tableName)) 511 else: 512 return self.qName
513
514 - def getAllNames(self):
515 yield self.tableName.qName
516
517 - def getAllTables(self):
518 yield self
519
520 - def makeUpId(self):
521 # for suggestAName 522 n = self.tableName.name 523 if isinstance(n, utils.QuotedName): 524 return "_"+re.sub("[^A-Za-z0-9_]", "", n.name) 525 else: 526 return n
527
528 529 -class DerivedTable(ColumnBearingNode):
530 type = "derivedTable" 531 _a_query = None 532 _a_tableName = None 533
534 - def getFieldInfo(self, name):
535 return self.query.getFieldInfo(name)
536
537 - def _get_fieldInfos(self):
538 return self.query.fieldInfos
539
540 - def _set_fieldInfos(self, val):
541 self.query.fieldInfos = val
542 fieldInfos = property(_get_fieldInfos, _set_fieldInfos) 543 544 @classmethod
545 - def _getInitKWs(cls, _parseResult):
546 tmp = {'tableName': TableName(name=str(_parseResult.get("alias"))), 547 'query': getChildOfClass(_parseResult, SelectQuery), 548 } 549 return tmp
550
551 - def flatten(self):
552 return "(%s) AS %s"%(flatten(self.query), flatten(self.tableName))
553
554 - def getAllNames(self):
555 yield self.tableName.qName
556
557 - def getAllTables(self):
558 yield self
559
560 - def makeUpId(self):
561 # for suggestAName 562 n = self.tableName.name 563 if isinstance(n, utils.QuotedName): 564 return "_"+re.sub("[^A-Za-z0-9_]", "", n.name) 565 else: 566 return n
567
568 569 -class JoinSpecification(ADQLNode, TransparentMixin):
570 """A join specification ("ON" or "USING"). 571 """ 572 type = "joinSpecification" 573 574 _a_children = () 575 _a_predicate = None 576 _a_usingColumns = () 577 578 @classmethod
579 - def _getInitKWs(cls, _parseResult):
580 predicate = _parseResult[0].upper() 581 if predicate=="USING": 582 usingColumns = [ #noflake: locals returned 583 n for n in _parseResult["columnNames"] if n!=','] 584 del n 585 children = list(_parseResult) #noflake: locals returned 586 return locals()
587
588 589 -class JoinOperator(ADQLNode, TransparentMixin):
590 """the complete join operator (including all LEFT, RIGHT, ",", and whatever). 591 """ 592 type = "joinOperator" 593
594 - def isCrossJoin(self):
595 return self.children[0] in (',', 'CROSS')
596
597 598 -class JoinedTable(ColumnBearingNode):
599 """A joined table. 600 601 These aren't made directly by the parser since parsing a join into 602 a binary structure is very hard using pyparsing. Instead, there's 603 the helper function makeJoinedTableTree handling the joinedTable 604 symbol that manually creates a binary tree. 605 """ 606 type = None 607 originalTable = None 608 tableName = TableName() 609 qName = None 610 611 _a_leftOperand = None 612 _a_operator = None 613 _a_rightOperand = None 614 _a_joinSpecification = None 615 616 @classmethod
617 - def _getInitKWs(cls, _parseResult):
618 leftOperand = _parseResult[0] #noflake: locals returned 619 operator = _parseResult[1] #noflake: locals returned 620 rightOperand = _parseResult[2] #noflake: locals returned 621 if len(_parseResult)>3: 622 joinSpecification = _parseResult[3] #noflake: locals returned 623 return locals()
624
625 - def flatten(self):
626 js = "" 627 if self.joinSpecification is not None: 628 js = flatten(self.joinSpecification) 629 return "%s %s %s %s"%( 630 self.leftOperand.flatten(), 631 self.operator.flatten(), 632 self.rightOperand.flatten(), 633 js)
634
635 - def addFieldInfos(self, context):
636 self.fieldInfos = fieldinfos.TableFieldInfos.makeForNode(self, context)
637
638 - def _polish(self):
639 self.joinedTables = [self.leftOperand, self.rightOperand]
640
641 - def getAllNames(self):
642 """iterates over all fully qualified table names mentioned in this 643 (possibly joined) table reference. 644 """ 645 for t in self.joinedTables: 646 yield t.tableName.qName
647
648 - def getTableForName(self, name):
649 return self.fieldInfos.locateTable(name)
650
651 - def makeUpId(self):
652 # for suggestAName 653 return "_".join(t.makeUpId() for t in self.joinedTables)
654
655 - def getJoinType(self):
656 """returns a keyword indicating how result rows are formed in this 657 join. 658 659 This can be NATURAL (all common columns are folded into one), 660 USING (check the joinSpecification what columns are folded), 661 CROSS (no columns are folded). 662 """ 663 if self.operator.isCrossJoin(): 664 if self.joinSpecification is not None: 665 raise common.Error("Cannot use cross join with a join predicate.") 666 return "CROSS" 667 if self.joinSpecification is not None: 668 if self.joinSpecification.predicate=="USING": 669 return "USING" 670 if self.joinSpecification.predicate=="ON": 671 return "CROSS" 672 return "NATURAL"
673
674 - def getAllTables(self):
675 """returns all actual tables and subqueries (not sub-joins) 676 within this join. 677 """ 678 res = [] 679 def collect(node): 680 if hasattr(node.leftOperand, "leftOperand"): 681 collect(node.leftOperand) 682 else: 683 res.append(node.leftOperand) 684 if hasattr(node.rightOperand, "leftOperand"): 685 collect(node.rightOperand) 686 else: 687 res.append(node.rightOperand)
688 collect(self) 689 return res
690
691 692 -class SubJoin(ADQLNode):
693 """A sub join (JoinedTable surrounded by parens). 694 695 The parse result is just the parens and a joinedTable; we need to 696 camouflage as that joinedTable. 697 """ 698 type = "subJoin" 699 _a_joinedTable = None 700 701 @classmethod
702 - def _getInitKWs(cls, _parseResult):
703 return {"joinedTable": _parseResult[1]}
704
705 - def flatten(self):
706 return "("+self.joinedTable.flatten()+")"
707
708 - def __getattr__(self, attName):
709 return getattr(self.joinedTable, attName)
710
711 712 @symbolAction("joinedTable") 713 -def makeBinaryJoinTree(children):
714 """takes the parse result for a join and generates a binary tree of 715 JoinedTable nodes from it. 716 717 It's much easier to do this in a separate step than to force a 718 non-left-recursive grammar to spit out the right parse tree in the 719 first place. 720 """ 721 try: 722 children = list(children) 723 while len(children)>1: 724 if len(children)>3 and isinstance(children[3], JoinSpecification): 725 exprLen = 4 726 else: 727 exprLen = 3 728 args = children[:exprLen] 729 children[:exprLen] = [JoinedTable.fromParseResult(args)] 730 except: 731 # remove this, it's just here for debugging 732 import traceback 733 traceback.print_exc() 734 raise 735 return children[0]
736
737 738 -class TransparentNode(ADQLNode, TransparentMixin):
739 """An abstract base for Nodes that don't parse out anything. 740 """ 741 type = None
742
743 744 -class WhereClause(TransparentNode):
745 type = "whereClause"
746
747 -class Grouping(TransparentNode):
748 type = "groupByClause"
749
750 -class Having(TransparentNode):
751 type = "havingClause"
752
753 -class OrderBy(TransparentNode):
754 type = "sortSpecification"
755
756 -class OffsetSpec(ADQLNode):
757 type = "offsetSpec" 758 759 _a_offset = None 760 761 @classmethod
762 - def _getInitKWs(cls, _parseResult):
763 return {"offset": int(_parseResult[1])}
764
765 - def flatten(self):
766 if self.offset is not None: 767 return "OFFSET %d"%self.offset 768 return ""
769
770 771 -class SelectNoParens(ColumnBearingNode):
772 type = "selectNoParens" 773 774 _a_setQuantifier = None 775 _a_setLimit = None 776 _a_selectList = None 777 _a_fromClause = None 778 _a_whereClause = None 779 _a_groupby = None 780 _a_having = None 781 _a_orderBy = None 782
783 - def _polish(self):
784 self.query = weakref.proxy(self)
785 786 @classmethod
787 - def _getInitKWs(cls, _parseResult):
788 res = {} 789 for name in ["setQuantifier", "setLimit", "fromClause", 790 "whereClause", "groupby", "having", "orderBy"]: 791 res[name] = _parseResult.get(name) 792 res["selectList"] = getChildOfType(_parseResult, "selectList") 793 return res
794
795 - def _iterSelectList(self):
796 for f in self.selectList.selectFields: 797 if isinstance(f, DerivedColumn): 798 yield f 799 elif isinstance(f, QualifiedStar): 800 for sf in self.fromClause.getFieldsForTable(f.sourceTable): 801 yield sf 802 else: 803 raise common.Error("Unexpected %s in select list"%getType(f))
804
805 - def getSelectFields(self):
806 if self.selectList.allFieldsQuery: 807 return self.fromClause.getAllFields() 808 else: 809 return self._iterSelectList()
810
811 - def addFieldInfos(self, context):
812 self.fieldInfos = fieldinfos.QueryFieldInfos.makeForNode(self, context)
813
814 - def resolveField(self, fieldName):
815 return self.fromClause.resolveField(fieldName)
816
817 - def getAllNames(self):
818 return self.fromClause.getAllNames()
819
820 - def flatten(self):
821 return flattenKWs(self, ("SELECT", None), 822 ("", "setQuantifier"), 823 ("TOP", "setLimit"), 824 ("", "selectList"), 825 ("", "fromClause"), 826 ("", "whereClause"), 827 ("", "groupby"), 828 ("", "having"), 829 ("", "orderBy"))
830
831 - def suggestAName(self):
832 """returns a string that may or may not be a nice name for a table 833 resulting from this query. 834 835 Whatever is being returned here, it's a regular SQL identifier. 836 """ 837 try: 838 sources = [tableRef.makeUpId() 839 for tableRef in self.fromClause.getAllTables()] 840 if sources: 841 return "_".join(sources) 842 else: 843 return "query_result" 844 except: # should not happen, but we don't want to bomb from here 845 import traceback;traceback.print_exc() 846 return "weird_table_report_this"
847
848 - def getContributingNames(self):
849 """returns a set of table names mentioned below this node. 850 """ 851 names = set() 852 for name, val in self.iterTree(): 853 if isinstance(val, TableName): 854 names.add(val.flatten()) 855 return names
856
857 858 -class SetOperationNode(ColumnBearingNode, TransparentMixin):
859 """A node containing a set expression. 860 861 This is UNION, INTERSECT, or EXCEPT. In all cases, we need to check 862 all contributing sub-expressions have compatible degree. For now, 863 in violation of SQL1992, we require identical names on all operands -- 864 sql92 in 7.10 says 865 866 [if column names are unequal], the <column name> of the i-th column of TR 867 is implementation-dependent and different from the <column name> of any 868 column, other than itself, of any table referenced by any <table reference> 869 contained in the SQL-statement. 870 871 Yikes. 872 873 These collapse to keep things simple in the typical case. 874 """
876 """errors out if operands have incompatible signatures. 877 878 For convenience, if all are compatible, the common signature (ie, 879 fieldInfos) is returned. 880 """ 881 fieldInfos = None 882 for child in self.children: 883 # Skip WithQueries -- they're not part of set operations. 884 if hasattr(child, "fieldInfos") and not isinstance(child, WithQuery): 885 if fieldInfos is None: 886 fieldInfos = child.fieldInfos 887 else: 888 fieldInfos.assertIsCompatible(child.fieldInfos) 889 return fieldInfos
890
891 - def addFieldInfos(self, context):
892 self.fieldInfos = self._assertFieldInfosCompatible()
893
894 - def getAllNames(self):
895 for index, child in enumerate(self.children): 896 if hasattr(child, "getAllNames"): 897 for name in child.getAllNames(): 898 yield name 899 elif hasattr(child, "suggestAName"): 900 yield child.suggestAName() 901 else: 902 assert False, "no name"
903
904 - def getSelectClauses(self):
905 for child in self.children: 906 for sc in getattr(child, "getSelectClauses", lambda: [])(): 907 yield sc 908 if hasattr(child, "setLimit"): 909 yield child
910
911 912 -class SetTerm(SetOperationNode):
913 type = "setTerm" 914 collapsible = True
915
916 917 -class WithQuery(SetOperationNode):
918 """A query from a with clause. 919 920 This essentially does everything a table does. 921 """ 922 type = "withQuery" 923
924 - def _polish(self):
925 self.name = self.children[0] 926 for c in self.children: 927 # this should be a selectQuery, but this we want to be sure 928 # we don't fail when morphers replace the main query node 929 # (as the pg morpher does) 930 if hasattr(c, "setLimit"): 931 self.select = c 932 break 933 else: 934 raise NotImplementedError("WithQuery without select?")
935
936 937 -class SelectQuery(SetOperationNode):
938 """A complete query excluding CTEs. 939 940 The main ugly thing here is the set limit; the querySpecification has 941 max of the limits of the children, if existing, otherwise to None. 942 943 Other than that, we hand through attribute access to our first child. 944 945 If there is a set expression on the top level, this will have a complex 946 structure; the first-child thing still ought to work since after 947 annotation we'll have errored out if set operator arguments aren't 948 reasonably congurent. 949 """ 950 type = "selectQuery" 951 952 _a_setLimit = None 953 _a_offset= None 954
955 - def getSelectClauses(self):
956 for child in self.children: 957 for sc in getattr(child, "getSelectClauses", lambda: [])(): 958 yield sc 959 if hasattr(child, "setLimit"): 960 yield child
961
962 - def _polish(self):
963 if self.setLimit is None: 964 limits = [selectClause.setLimit 965 for selectClause in self.getSelectClauses()] 966 967 limits = [int(s) for s in limits if s] 968 if limits: 969 self.setLimit = max(limits) 970 971 for child in self.children: 972 if isinstance(child, OffsetSpec) and child.offset is not None: 973 self.offset = child.offset 974 child.offset = None
975
976 - def __getattr__(self, attrName):
977 return getattr(self.children[0], attrName)
978
979 980 -class QuerySpecification(TransparentNode):
981 """The toplevel query objects including CTEs. 982 983 Apart from any CTEs, that's just a SelectQuery (which is always the last 984 child), and we hand through essentially all attribute access to it. 985 """ 986 type = "querySpecification" 987
988 - def _polish(self):
989 self.withTables = [] 990 for child in self.children: 991 if isinstance(child, WithQuery): 992 self.withTables.append(child)
993
994 - def _setSetLimit(self, val):
995 self.children[-1].setLimit = val
996 - def _getSetLimit(self):
997 return self.children[-1].setLimit
998 setLimit = property(_getSetLimit, _setSetLimit) 999 1000
1001 - def __getattr__(self, attrName):
1002 return getattr(self.children[-1], attrName)
1003
1004 1005 -class ColumnReference(FieldInfoedNode):
1006 # normal column references will be handled by the dispatchColumnReference 1007 # function below, hence the binding is missing here. 1008 type = "columnReference" 1009 bindings = ["geometryValue"] 1010 _a_refName = None # if given, a TableName instance 1011 _a_name = None 1012
1013 - def _polish(self):
1014 if not self.refName: 1015 self.refName = None 1016 self.colName = ".".join( 1017 flatten(p) for p in (self.refName, self.name) if p)
1018 1019 @classmethod
1020 - def _getInitKWs(cls, _parseResult):
1021 names = [_c for _c in _parseResult if _c!="."] 1022 names = [None]*(4-len(names))+names 1023 refName = TableName(cat=names[0], 1024 schema=names[1], 1025 name=names[2]) 1026 if not refName: 1027 refName = None 1028 return { 1029 "name": names[-1], 1030 "refName": refName}
1031
1032 - def addFieldInfo(self, context):
1033 self.fieldInfo = context.getFieldInfo(self.name, self.refName) 1034 1035 srcColumn = None 1036 if self.fieldInfo.userData: 1037 srcColumn = self.fieldInfo.userData[0] 1038 if hasattr(srcColumn, "originalName"): 1039 # This is a column from a VOTable upload we have renamed to avoid 1040 # clashes with postgres-reserved column names. Update the name 1041 # so the "bad" name doesn't apprear in the serialised query. 1042 if not isinstance(self.name, utils.QuotedName): 1043 self.name = srcColumn.name 1044 self._polish()
1045
1046 - def flatten(self):
1047 if self.fieldInfo and self.fieldInfo.sqlName: 1048 return ".".join( 1049 flatten(p) for p in (self.refName, self.fieldInfo.sqlName) if p) 1050 return self.colName
1051
1052 - def _treeRepr(self):
1053 return (self.type, self.name)
1054
1055 1056 -class ColumnReferenceByUCD(ColumnReference):
1057 # these are tricky: As, when parsing, we don't know where the columns 1058 # might come from, we have to 1059 type = "columnReferenceByUCD" 1060 bindings = ["columnReferenceByUCD"] 1061 _a_ucdWanted = None 1062 1063 @classmethod
1064 - def _getInitKWs(cls, _parseResult):
1065 return { 1066 "ucdWanted": _parseResult[2].value, 1067 "name": utils.Undefined, 1068 "refName": utils.Undefined}
1069
1070 - def addFieldInfo(self, context):
1071 # I've not really thought about where these might turn up. 1072 # Hence, I just heuristically walk up the ancestor stack 1073 # until I find a from clause. TODO: think about if that's valid. 1074 for ancestor in reversed(context.ancestors): 1075 if hasattr(ancestor, "fromClause"): 1076 break 1077 else: 1078 raise common.Error("UCDCOL outside of query specification with FROM") 1079 1080 for field in ancestor.fromClause.getAllFields(): 1081 if fnmatch.fnmatch(field.fieldInfo.ucd, self.ucdWanted): 1082 self.fieldInfo = field.fieldInfo 1083 self.name = self.colName = field.name 1084 self.refName = None 1085 break 1086 else: 1087 raise utils.NotFoundError(self.ucdWanted, "column matching ucd", 1088 "from clause")
1089
1090 @symbolAction("columnReference") 1091 -def dispatchColumnReference(parseResult):
1092 # this dispatch is there so ColumnReference is not bothered 1093 # by the by-UCD hack in the normal case. It should go if we 1094 # punt UCDCOL, and the columnReference binding should then go 1095 # back to ColumnReference 1096 if len(parseResult)==1 and isinstance(parseResult[0], ColumnReferenceByUCD): 1097 return parseResult[0] 1098 else: 1099 return ColumnReference.fromParseResult(parseResult)
1100
1101 1102 -class FromClause(ADQLNode):
1103 type = "fromClause" 1104 _a_tableReference = () 1105 _a_tables = () 1106 1107 @classmethod
1108 - def _getInitKWs(cls, parseResult):
1109 parseResult = list(parseResult) 1110 if len(parseResult)==1: 1111 tableReference = parseResult[0] 1112 else: 1113 # it's a cross join; to save repeating the logic, we'll 1114 # just build an artificial join as the table reference 1115 tableReference = reduce(lambda left, right: 1116 JoinedTable( 1117 leftOperand=left, 1118 operator=JoinOperator(children=[","]), 1119 rightOperand=right), parseResult) 1120 return { 1121 "tableReference": tableReference, 1122 "tables": parseResult}
1123
1124 - def flatten(self):
1125 return "FROM %s"%(' , '.join(t.flatten() for t in self.tables))
1126
1127 - def getAllNames(self):
1128 """returns the names of all tables taking part in this from clause. 1129 """ 1130 return self.tableReference.getAllNames()
1131
1132 - def resolveField(self, name):
1133 return self.tableReference.getFieldInfo(name)
1134
1135 - def _makeColumnReference(self, sourceTableName, colPair):
1136 """returns a ColumnReference object for a name, colInfo pair from a 1137 table's fieldInfos. 1138 """ 1139 cr = ColumnReference(name=colPair[0], refName=sourceTableName) 1140 cr.fieldInfo = colPair[1] 1141 return cr
1142
1143 - def getAllFields(self):
1144 """returns all fields from all tables in this FROM. 1145 1146 These will be qualified names. Columns taking part in joins are 1147 resolved here. 1148 1149 This will only work for annotated tables. 1150 """ 1151 res = [] 1152 commonColumns = common.computeCommonColumns(self.tableReference) 1153 commonColumnsMade = set() 1154 1155 for table in self.getAllTables(): 1156 for label, fi in table.fieldInfos.seq: 1157 if label in commonColumns: 1158 if label not in commonColumnsMade: 1159 res.append(self._makeColumnReference( 1160 None, (label, fi))) 1161 commonColumnsMade.add(label) 1162 1163 else: 1164 res.append(self._makeColumnReference( 1165 table.tableName, (label, fi))) 1166 1167 return res
1168
1169 - def getFieldsForTable(self, srcTableName):
1170 """returns the fields in srcTable. 1171 1172 srcTableName is a TableName. 1173 """ 1174 if fieldinfos.tableNamesMatch(self.tableReference, srcTableName): 1175 table = self.tableReference 1176 else: 1177 table = self.tableReference.fieldInfos.locateTable(srcTableName) 1178 1179 return [self._makeColumnReference(table.tableName, ci) 1180 for ci in table.fieldInfos.seq]
1181
1182 - def getAllTables(self):
1183 return self.tableReference.getAllTables()
1184
1185 1186 -class DerivedColumn(FieldInfoedNode):
1187 """A column within a select list. 1188 """ 1189 type = "derivedColumn" 1190 _a_expr = None 1191 _a_alias = None 1192 _a_tainted = True 1193
1194 - def _polish(self):
1195 if getType(self.expr)=="columnReference": 1196 self.tainted = False
1197 1198 @property
1199 - def name(self):
1200 # todo: be a bit more careful here to come up with meaningful 1201 # names (users don't like the funny names). Also: do 1202 # we make sure somewhere we're getting unique names? 1203 if self.alias is not None: 1204 return self.alias 1205 1206 elif hasattr(self.expr, "name"): 1207 return self.expr.name 1208 1209 else: 1210 return utils.intToFunnyWord(id(self))
1211 1212 @classmethod
1213 - def _getInitKWs(cls, _parseResult):
1214 expr = _parseResult["expr"] #noflake: locals returned 1215 alias = _parseResult.get("alias") #noflake: locals returned 1216 return locals()
1217
1218 - def flatten(self):
1219 return flattenKWs(self, 1220 ("", "expr"), 1221 ("AS", "alias"))
1222
1223 - def _treeRepr(self):
1224 return (self.type, self.name)
1225
1226 1227 -class QualifiedStar(ADQLNode):
1228 type = "qualifiedStar" 1229 _a_sourceTable = None # A TableName for the column source 1230 1231 @classmethod
1232 - def _getInitKWs(cls, _parseResult):
1233 parts = _parseResult[:-2:2] # kill dots and star 1234 cat, schema, name = [None]*(3-len(parts))+parts 1235 return {"sourceTable": TableName(cat=cat, schema=schema, name=name)}
1236
1237 - def flatten(self):
1238 return "%s.*"%flatten(self.sourceTable)
1239
1240 1241 -class SelectList(ADQLNode):
1242 type = "selectList" 1243 _a_selectFields = () 1244 _a_allFieldsQuery = False 1245 1246 @classmethod
1247 - def _getInitKWs(cls, _parseResult):
1248 allFieldsQuery = _parseResult.get("starSel", False) 1249 if allFieldsQuery: 1250 # Will be filled in by query, we don't have the from clause here. 1251 selectFields = None #noflake: locals returned 1252 else: 1253 selectFields = list(_parseResult.get("fieldSel")) #noflake: locals returned 1254 return locals()
1255
1256 - def flatten(self):
1257 if self.allFieldsQuery: 1258 return self.allFieldsQuery 1259 else: 1260 return ", ".join(flatten(sf) for sf in self.selectFields)
1261
1262 1263 ######## all expression parts we need to consider when inferring units and such 1264 1265 -class Comparison(ADQLNode):
1266 """is required when we want to morph the braindead contains(...)=1 into 1267 a true boolean function call. 1268 """ 1269 type = "comparisonPredicate" 1270 _a_op1 = None 1271 _a_opr = None 1272 _a_op2 = None 1273 1274 @classmethod
1275 - def _getInitKWs(cls, _parseResult):
1276 op1, opr, op2 = _parseResult #noflake: locals returned 1277 return locals()
1278
1279 - def flatten(self):
1280 return "%s %s %s"%(flatten(self.op1), self.opr, flatten(self.op2))
1281
1282 1283 -def _guessNumericType(literal):
1284 """returns a guess for a type suitable to hold a numeric value given in 1285 literal. 1286 1287 I don't want to pull through the literal symbol that matched 1288 from grammar in all cases. Thus, at times I simply guess the type 1289 (and yes, I'm aware that -32768 still is a smallint). 1290 """ 1291 try: 1292 val = int(literal) 1293 if abs(val)<32767: 1294 type = "smallint" 1295 elif abs(val)<2147483648: 1296 type = "integer" 1297 else: 1298 type = "bigint" 1299 except ValueError: 1300 type = "double precision" 1301 return type
1302
1303 1304 -class Factor(FieldInfoedNode, TransparentMixin):
1305 """is a factor within an SQL expression. 1306 1307 factors may have only one (direct) child with a field info and copy 1308 this. They can have no child with a field info, in which case they're 1309 simply numeric (about the weakest assumption: They're doubles). 1310 """ 1311 type = "factor" 1312 collapsible = True 1313
1314 - def addFieldInfo(self, context):
1315 infoChildren = self._getInfoChildren() 1316 if infoChildren: 1317 assert len(infoChildren)==1 1318 self.fieldInfo = infoChildren[0].fieldInfo 1319 else: 1320 self.fieldInfo = fieldinfo.FieldInfo( 1321 _guessNumericType("".join(self.children)), "", "")
1322
1323 1324 -class ArrayReference(FieldInfoedNode, TransparentMixin):
1325 type = "arrayReference" 1326 collapsible = False 1327
1328 - def addFieldInfo(self, context):
1329 infoChild = self.children[0] 1330 childInfo = infoChild.fieldInfo 1331 1332 if childInfo.type is None: 1333 raise common.Error("Cannot subscript a typeless thing in %s"%( 1334 self.flatten())) 1335 1336 lastSubscript = re.search("\[[0-9]*\]$", childInfo.type) 1337 if lastSubscript is None: 1338 raise common.Error("Cannot subscript a non-array in %s"%( 1339 self.flatten())) 1340 1341 self.fieldInfo = fieldinfo.FieldInfo( 1342 childInfo.type[:lastSubscript.start()], 1343 childInfo.unit, 1344 childInfo.ucd, 1345 childInfo.userData, 1346 tainted=True # array might actually have semantics 1347 )
1348
1349 1350 -class CombiningFINode(FieldInfoedNode):
1351 - def addFieldInfo(self, context):
1352 infoChildren = self._getInfoChildren() 1353 if not infoChildren: 1354 if len(self.children)==1: 1355 # probably a naked numeric literal in the grammar, e.g., 1356 # in mathFunction 1357 self.fieldInfo = fieldinfo.FieldInfo( 1358 _guessNumericType(self.children[0]), "", "") 1359 else: 1360 raise common.Error("Oops -- did not expect '%s' when annotating %s"%( 1361 "".join(self.children), self)) 1362 elif len(infoChildren)==1: 1363 self.fieldInfo = infoChildren[0].fieldInfo 1364 else: 1365 self.fieldInfo = self._combineFieldInfos()
1366
1367 1368 -class Term(CombiningFINode, TransparentMixin):
1369 type = "term" 1370 collapsible = True 1371
1372 - def _combineFieldInfos(self):
1373 # These are either multiplication or division 1374 toDo = self.children[:] 1375 opd1 = toDo.pop(0) 1376 fi1 = opd1.fieldInfo 1377 while toDo: 1378 opr = toDo.pop(0) 1379 fi1 = fieldinfo.FieldInfo.fromMulExpression(opr, fi1, 1380 toDo.pop(0).fieldInfo) 1381 return fi1
1382
1383 1384 -class NumericValueExpression(CombiningFINode, TransparentMixin):
1385 type = "numericValueExpression" 1386 collapsible = True 1387
1388 - def _combineFieldInfos(self):
1389 # These are either addition or subtraction 1390 toDo = self.children[:] 1391 fi1 = toDo.pop(0).fieldInfo 1392 while toDo: 1393 opr = toDo.pop(0) 1394 fi1 = fieldinfo.FieldInfo.fromAddExpression( 1395 opr, fi1, toDo.pop(0).fieldInfo) 1396 return fi1
1397
1398 1399 -class StringValueExpression(FieldInfoedNode, TransparentMixin):
1400 type = "stringValueExpression" 1401 collapsible = True 1402
1403 - def addFieldInfo(self, context):
1404 # This is concatenation; we treat is as if we'd be adding numbers 1405 infoChildren = self._getInfoChildren() 1406 if infoChildren: 1407 fi1 = infoChildren.pop(0).fieldInfo 1408 if fi1.type=="unicode": 1409 baseType = "unicode" 1410 else: 1411 baseType = "text" 1412 while infoChildren: 1413 if infoChildren[0].fieldInfo.type=="unicode": 1414 baseType = "unicode" 1415 fi1 = fieldinfo.FieldInfo.fromAddExpression( 1416 "+", fi1, infoChildren.pop(0).fieldInfo, forceType=baseType) 1417 self.fieldInfo = fi1 1418 else: 1419 self.fieldInfo = fieldinfo.FieldInfo( 1420 "text", "", "")
1421
1422 1423 -class GenericValueExpression(CombiningFINode, TransparentMixin):
1424 """A container for value expressions that we don't want to look at 1425 closer. 1426 1427 It is returned by the makeValueExpression factory below to collect 1428 stray children. 1429 """ 1430 type = "valueExpression" 1431 collapsible = True 1432
1433 - def _combineFieldInfos(self):
1434 # we don't really know what these children are. Let's just give up 1435 # unless all child fieldInfos are more or less equal (which of course 1436 # is a wild guess). 1437 childUnits, childUCDs = set(), set() 1438 infoChildren = self._getInfoChildren() 1439 for c in infoChildren: 1440 childUnits.add(c.fieldInfo.unit) 1441 childUCDs.add(c.fieldInfo.ucd) 1442 if len(childUnits)==1 and len(childUCDs)==1: 1443 # let's taint the first info and be done with it 1444 return infoChildren[0].fieldInfo.change(tainted=True) 1445 else: 1446 # if all else fails: let's hope someone can make a string from it 1447 return fieldinfo.FieldInfo("text", "", "")
1448
1449 1450 @symbolAction("valueExpression") 1451 -def makeValueExpression(children):
1452 if len(children)!=1: 1453 res = GenericValueExpression.fromParseResult(children) 1454 res.type = "valueExpression" 1455 return res 1456 else: 1457 return children[0]
1458
1459 1460 -class SetFunction(TransparentMixin, FieldInfoedNode):
1461 """An aggregate function. 1462 1463 These typically amend the ucd by a word from the stat family and copy 1464 over the unit. There are exceptions, however, see table in class def. 1465 """ 1466 type = "setFunctionSpecification" 1467 1468 funcDefs = { 1469 'AVG': ('%s;stat.mean', None, "double precision"), 1470 'MAX': ('%s;stat.max', None, None), 1471 'MIN': ('%s;stat.min', None, None), 1472 'SUM': (None, None, None), 1473 'COUNT': ('meta.number;%s', '', "integer"),} 1474
1475 - def addFieldInfo(self, context):
1476 funcName = self.children[0].upper() 1477 ucdPref, newUnit, newType = self.funcDefs[funcName] 1478 1479 # try to find out about our child 1480 infoChildren = self._getInfoChildren() 1481 if infoChildren: 1482 assert len(infoChildren)==1 1483 fi = infoChildren[0].fieldInfo 1484 else: 1485 fi = fieldinfo.FieldInfo("double precision", "", "") 1486 1487 if ucdPref is None: 1488 # ucd of a sum is the ucd of the summands? 1489 ucd = fi.ucd 1490 elif fi.ucd: 1491 ucd = ucdPref%(fi.ucd) 1492 else: 1493 # no UCD given; if we're count, we're meta.number, otherwise we 1494 # don't know 1495 if funcName=="COUNT": 1496 ucd = "meta.number" 1497 else: 1498 ucd = None 1499 1500 # most of these keep the unit of what they're working on 1501 if newUnit is None: 1502 newUnit = fi.unit 1503 1504 # most of these keep the type of what they're working on 1505 if newType is None: 1506 newType = fi.type 1507 1508 self.fieldInfo = fieldinfo.FieldInfo( 1509 newType, unit=newUnit, ucd=ucd, userData=fi.userData, tainted=fi.tainted)
1510
1511 1512 -class NumericValueFunction(FunctionNode):
1513 """A numeric function. 1514 1515 This is really a mixed bag. We work through handlers here. See table 1516 in class def. Unknown functions result in dimlesses. 1517 """ 1518 type = "numericValueFunction" 1519 collapsible = True # if it's a real function call, it has at least 1520 # a name, parens and an argument and thus won't be collapsed. 1521 1522 funcDefs = { 1523 "ACOS": ('rad', '', None), 1524 "ASIN": ('rad', '', None), 1525 "ATAN": ('rad', '', None), 1526 "ATAN2": ('rad', '', None), 1527 "PI": ('', '', None), 1528 "RAND": ('', '', None), 1529 "EXP": ('', '', None), 1530 "LOG": ('', '', None), 1531 "LOG10": ('', '', None), 1532 "SQRT": ('', '', None), 1533 "SQUARE": ('', '', None), 1534 "POWER": ('', '', None), 1535 "ABS": (None, None, "keepMeta"), 1536 "CEILING": (None, None, "keepMeta"), 1537 "FLOOR": (None, None, "keepMeta"), 1538 "ROUND": (None, None, "keepMeta"), 1539 "TRUNCATE": (None, None, "keepMeta"), 1540 "DEGREES": ('deg', None, "keepMeta"), 1541 "RADIANS": ('rad', None, "keepMeta"), 1542 # bitwise operators: hopeless 1543 } 1544
1545 - def _handle_keepMeta(self, infoChildren):
1546 fi = infoChildren[0].fieldInfo 1547 return fi.unit, fi.ucd
1548
1549 - def addFieldInfo(self, context):
1550 infoChildren = self._getInfoChildren() 1551 unit, ucd = '', '' 1552 overrideUnit, overrideUCD, handlerName = self.funcDefs.get( 1553 self.funName, ('', '', None)) 1554 if handlerName: 1555 unit, ucd = getattr(self, "_handle_"+handlerName)(infoChildren) 1556 if overrideUnit: 1557 unit = overrideUnit 1558 if overrideUCD: 1559 ucd = overrideUCD 1560 self.fieldInfo = fieldinfo.FieldInfo("double precision", 1561 unit, ucd, *collectUserData(infoChildren)) 1562 self.fieldInfo.tainted = True
1563
1564 1565 -class StringValueFunction(FunctionNode):
1566 type = "stringValueFunction" 1567
1568 - def addFieldInfo(self, context):
1569 self.fieldInfo = fieldinfo.FieldInfo("text", "", "", 1570 userData=collectUserData(self._getInfoChildren())[0])
1571
1572 1573 -class TimestampFunction(FunctionNode):
1574 type = "timestampFunction" 1575
1576 - def addFieldInfo(self, context):
1577 subordinates = self._getInfoChildren() 1578 if subordinates: 1579 ucd, stc = subordinates[0].fieldInfo.ucd, subordinates[0].fieldInfo.stc 1580 else: 1581 ucd, stc = None, None 1582 self.fieldInfo = fieldinfo.FieldInfo("timestamp", "", 1583 ucd=ucd, stc=stc, userData=subordinates)
1584
1585 1586 -class InUnitFunction(FieldInfoedNode):
1587 type = "inUnitFunction" 1588 _a_expr = None 1589 _a_unit = None 1590 1591 conversionFactor = None 1592 1593 @classmethod
1594 - def _getInitKWs(cls, _parseResult):
1595 return { 1596 'expr': _parseResult[2], 1597 'unit': _parseResult[4].value, 1598 }
1599
1600 - def addFieldInfo(self, context):
1601 try: 1602 from gavo.base import computeConversionFactor, IncompatibleUnits, BadUnit 1603 except ImportError: 1604 raise utils.ReportableError("in_unit only available with gavo.base" 1605 " installed") 1606 1607 try: 1608 self.conversionFactor = computeConversionFactor( 1609 self.expr.fieldInfo.unit, self.unit) 1610 self.fieldInfo = self.expr.fieldInfo.change(unit=self.unit) 1611 except IncompatibleUnits as msg: 1612 raise common.Error("in_unit error: %s"%msg) 1613 except BadUnit as msg: 1614 raise common.Error("Bad unit passed to in_unit: %s"%msg)
1615
1616 - def flatten(self):
1617 if self.conversionFactor is None: 1618 raise common.Error("in_unit can only be flattened in annotated" 1619 " trees") 1620 1621 if isinstance(self.expr, ColumnReference): 1622 exprPat = "%s" 1623 else: 1624 exprPat = "(%s)" 1625 return "(%s * %s)"%(exprPat%flatten(self.expr), self.conversionFactor)
1626
1627 - def change(self, **kwargs):
1628 copy = FieldInfoedNode.change(self, **kwargs) 1629 copy.conversionFactor = self.conversionFactor 1630 return copy
1631
1632 1633 -class CharacterStringLiteral(FieldInfoedNode):
1634 """according to the current grammar, these are always sequences of 1635 quoted strings. 1636 """ 1637 type = "characterStringLiteral" 1638 bindings = ["characterStringLiteral", "generalLiteral"] 1639 1640 _a_value = None 1641 1642 @classmethod
1643 - def _getInitKWs(cls, _parseResult):
1644 value = "".join(_c[1:-1] for _c in _parseResult) #noflake: locals returned 1645 return locals()
1646
1647 - def flatten(self):
1648 return "'%s'"%self.value
1649
1650 - def addFieldInfo(self, context):
1651 self.fieldInfo = fieldinfo.FieldInfo("text", "", "")
1652
1653 1654 -class CastSpecification(FieldInfoedNode, TransparentMixin):
1655 type = "castSpecification" 1656 _a_value = None 1657 _a_newType = None 1658 1659 @classmethod
1660 - def _getInitKWs(cls, _parseResult):
1661 value = _parseResult["value"] 1662 newType = _parseResult["newType"].lower() 1663 if newType.startswith("char ("): 1664 newType = "text" 1665 elif newType.startswith("national char"): 1666 newType = "unicode" 1667 return locals()
1668
1669 - def addFieldInfo(self, context):
1670 # We copy units and UCDs from the subordinate value (if it's there; 1671 # NULLs have nothing, of course). That has the somewhat unfortunate 1672 # effect that we may be declaring units on strings. Ah well. 1673 if hasattr(self.value, "fieldInfo"): 1674 self.fieldInfo = self.value.fieldInfo.change( 1675 type=self.newType, tainted=True) 1676 else: 1677 self.fieldInfo = fieldinfo.FieldInfo(self.newType, "", "")
1678
1679 1680 ###################### Geometry and stuff that needs morphing into real SQL 1681 1682 -class CoosysMixin(object):
1683 """is a mixin that works cooSys into FieldInfos for ADQL geometries. 1684 """ 1685 _a_cooSys = None 1686 1687 @classmethod
1688 - def _getInitKWs(cls, _parseResult):
1689 refFrame = _parseResult.get("coordSys", "") 1690 if isinstance(refFrame, ColumnReference): 1691 raise NotImplementedError("References frames must not be column" 1692 " references.") 1693 return {"cooSys": refFrame}
1694
1695 1696 -class GeometryNode(CoosysMixin, FieldInfoedNode):
1697 """Nodes for geometry constructors. 1698 1699 In ADQL 2.1, most of these became polymorphous. For instance, circles 1700 can be constructed with a point as the first (or second, if a coosys 1701 is present) argument; that point can also be a column reference. 1702 1703 Also, these will always get morphed in some way (as the database 1704 certainly doesn't understand ADQL geometries). So, we're 1705 trying to give the morphers a fair chance of not getting confused 1706 despite the wild variety of argument forms and types. 1707 1708 stcArgs is a list of symbolic names that *might* contain stc (or similar) 1709 information. Some of the actual attributes will be None. 1710 1711 Flatten is only there for debugging; it'll return invalid SQL. 1712 OrigArgs is not for client consumption; clients must go through the 1713 symbolic names. 1714 """ 1715 _a_origArgs = None 1716
1717 - def flatten(self):
1718 return "%s%s"%(self.type.upper(), 1719 "".join(flatten(arg) for arg in self.origArgs))
1720 1721 @classmethod
1722 - def _getInitKWs(cls, _parseResult):
1723 return {"origArgs": list(_parseResult[1:])}
1724
1725 - def addFieldInfo(self, context):
1726 fis = [attr.fieldInfo 1727 for attr in 1728 (getattr(self, arg) for arg in self.stcArgs if getattr(self, arg)) 1729 if attr and attr.fieldInfo] 1730 childUserData, childUnits = [], [] 1731 thisSystem = tapstc.getSTCForTAP(self.cooSys) 1732 1733 # get reference frame from first child if not given in node and 1734 # one is defined there. 1735 if thisSystem.astroSystem.spaceFrame.refFrame is None: 1736 if fis and fis[0].stc: 1737 thisSystem = fis[0].stc 1738 1739 for index, fi in enumerate(fis): 1740 childUserData.extend(fi.userData) 1741 childUnits.append(fi.unit) 1742 if not context.policy.match(fi.stc, thisSystem): 1743 context.errors.append("When constructing %s: Argument %d has" 1744 " incompatible STC"%(self.type, index+1)) 1745 1746 self.fieldInfo = fieldinfo.FieldInfo( 1747 type=self.sqlType, 1748 unit=",".join(childUnits), 1749 ucd="", 1750 userData=tuple(childUserData), 1751 stc=thisSystem) 1752 self.fieldInfo.properties["xtype"] = self.xtype
1753
1754 1755 -class Point(GeometryNode):
1756 type = "point" 1757 _a_x = _a_y = None 1758 xtype = "point" 1759 sqlType = "spoint" 1760 1761 stcArgs = ("x", "y") 1762
1763 - def flatten(self):
1764 return "%s(%s)"%(self.type.upper(), 1765 ", ".join(flatten(arg) for arg in [self.x, self.y]))
1766 1767 @classmethod
1768 - def _getInitKWs(cls, _parseResult):
1769 x, y = parseArgs(_parseResult["args"]) #noflake: locals returned 1770 return locals()
1771
1772 1773 -class Circle(GeometryNode):
1774 """A circle parsed from ADQL. 1775 1776 There are two ways a circle is specified: either with (x, y, radius) 1777 or as (center, radius). In the second case, center is an spoint-valued 1778 column reference. Cases with a point-valued literal are turned into 1779 the first variant during parsing. 1780 """ 1781 type = "circle" 1782 _a_x = _a_y = _a_radius = None 1783 _a_center = None 1784 stcArgs = ("x", "y","center", "radius") 1785 xtype = "circle" 1786 sqlType = "scircle" 1787 1788 @classmethod
1789 - def _getInitKWs(cls, _parseResult):
1790 args = parseArgs(_parseResult["args"]) 1791 res = {a: None for a in cls.stcArgs} 1792 if len(args)==2: 1793 if args[0].type=="point": 1794 # expand the point inline 1795 res["x"], res["y"], res["radius"] = args[0].x, args[0].y, args[1] 1796 else: 1797 # first arg should be something like a column reference or expression. 1798 # leave it as center 1799 res["center"], res["radius"] = args[0], args[1] 1800 elif len(args)==3: 1801 res["x"], res["y"], res["radius"] = args 1802 else: 1803 raise AssertionError("Grammar let through invalid args to Circle") 1804 return res
1805
1806 1807 -class Box(GeometryNode):
1808 type = "box" 1809 _a_x = _a_y = _a_width = _a_height = None 1810 stcArgs = ("x", "y", "width", "height") 1811 xtype = "polygon" 1812 sqlType = "sbox" 1813 1814 @classmethod
1815 - def _getInitKWs(cls, _parseResult):
1816 x, y, width, height = parseArgs( #noflake: locals returned 1817 _parseResult["args"]) 1818 return locals()
1819
1820 1821 -class PolygonCoos(FieldInfoedNode):
1822 """a base class for the various argument forms of polygons. 1823 1824 We want to tell them apart to let the grammar tell the tree builder 1825 what it thinks the arguments were. Polygon may have to reconsider 1826 this when it learns the types of its arguments, but we don't want 1827 to discard the information coming from the grammar. 1828 """ 1829 _a_args = None 1830 1831 @classmethod
1832 - def _getInitKWs(cls, _parseResult):
1833 return {"args": parseArgs(_parseResult["args"])}
1834
1835 - def addFieldInfo(self, context):
1836 # these fieldInfos are never used because Polygon doesn't ask us. 1837 pass
1838
1839 - def flatten(self):
1840 return ", ".join(flatten(a) for a in self.args)
1841
1842 1843 -class PolygonSplitCooArgs(PolygonCoos):
1844 type = "polygonSplitCooArgs"
1845
1846 1847 -class PolygonPointCooArgs(PolygonCoos):
1848 type = "polygonPointCooArgs"
1849
1850 1851 -class Polygon(GeometryNode):
1852 type = "polygon" 1853 _a_coos = None 1854 _a_points = None 1855 stcArgs = ("coos", "points") 1856 xtype = "polygon" 1857 sqlType = "spoly" 1858 1859 @classmethod
1860 - def _getInitKWs(cls, _parseResult):
1861 # XXX TODO: The grammar will parse even-numbered arguments >=6 into 1862 # splitCooArgs. We can't fix that here as we don't have reliable 1863 # type information at this point. Fix coos/points confusion 1864 # in addFieldInfo, I'd say 1865 arg = parseArgs(_parseResult["args"])[0] 1866 1867 if arg.type=="polygonPointCooArgs": 1868 # geometry-typed arguments 1869 res = {"points": tuple(parseArgs(arg.args))} 1870 1871 # See if they're all literal points, which which case we fall 1872 # back to the split args 1873 for item in res["points"]: 1874 if item.type!="point": 1875 return res 1876 # all points: mutate args to let us fall through to the split coo 1877 # case 1878 arg.type = "polygonSplitCooArgs" 1879 newArgs = [] 1880 for item in res["points"]: 1881 newArgs.extend([item.x, item.y]) 1882 arg.args = newArgs 1883 1884 if arg.type=="polygonSplitCooArgs": 1885 # turn numeric expressions into pairs 1886 coos, toDo = [], list(arg.args) 1887 while toDo: 1888 coos.append(tuple(toDo[:2])) 1889 del toDo[:2] 1890 res = {"coos": coos} 1891 1892 else: 1893 assert False, "Invalid arguments to polygon" 1894 1895 return res
1896
1897 - def addFieldInfo(self, name):
1898 if self.points is not None: 1899 systemSource = self.points 1900 elif self.coos is not None: 1901 systemSource = (c[0] for c in self.coos) 1902 else: 1903 assert False 1904 1905 if self.cooSys and self.cooSys!="UNKNOWN": 1906 thisSystem = tapstc.getSTCForTAP(self.cooSys) 1907 1908 for geo in systemSource: 1909 if geo.fieldInfo.stc and geo.fieldInfo.stc.astroSystem.spaceFrame.refFrame: 1910 thisSystem = geo.fieldInfo.stc 1911 break 1912 else: 1913 thisSystem = tapstc.getSTCForTAP("UNKNOWN") 1914 1915 userData, tainted = collectUserData( 1916 self.points or [c[0] for c in self.coos]+[c[1] for c in self.coos]) 1917 self.fieldInfo = fieldinfo.FieldInfo( 1918 type=self.sqlType, unit="deg", ucd="phys.angArea", 1919 userData=userData, tainted=tainted, 1920 stc=thisSystem)
1921 1922 1923 _regionMakers = []
1924 -def registerRegionMaker(fun):
1925 """adds a region maker to the region resolution chain. 1926 1927 region makers are functions taking the argument to REGION and 1928 trying to do something with it. They should return either some 1929 kind of FieldInfoedNode that will then replace the REGION or None, 1930 in which case the next function will be tried. 1931 1932 As a convention, region specifiers here should always start with 1933 an identifier (like simbad, siapBbox, etc, basically [A-Za-z]+). 1934 The rest is up to the region maker, but whitespace should separate 1935 this rest from the identifier. 1936 1937 The entire region functionality will probably disappear with TAP 1.1. 1938 Don't do anything with it any more. Use ufuncs instead. 1939 """ 1940 _regionMakers.append(fun)
1941
1942 1943 @symbolAction("region") 1944 -def makeRegion(children):
1945 if len(children)!=4 or not isinstance(children[2], CharacterStringLiteral): 1946 raise common.RegionError("Invalid argument to REGION: '%s'."% 1947 "".join(flatten(c) for c in children[2:-1]), 1948 hint="Here, regions must be simple strings; concatenations or" 1949 " non-constant parts are forbidden. Use ADQL geometry expressions" 1950 " instead.") 1951 arg = children[2].value 1952 for r in _regionMakers: 1953 res = r(arg) 1954 if res is not None: 1955 return res 1956 raise common.RegionError("Invalid argument to REGION: '%s'."% 1957 arg, hint="None of the region parsers known to this service could" 1958 " make anything of your string. While STC-S should in general" 1959 " be comprehendable to TAP services, it's probably better to" 1960 " use ADQL geometry functions.")
1961
1962 1963 -class STCSRegion(FieldInfoedNode):
1964 bindings = [] # we're constructed by makeSTCSRegion, not by the parser 1965 type = "stcsRegion" 1966 xtype = "adql:REGION" 1967 1968 _a_tapstcObj = None # from tapstc -- STCSRegion or a utils.pgshere object 1969
1970 - def _polish(self):
1971 self.cooSys = self.tapstcObj.cooSys
1972
1973 - def addFieldInfo(self, context):
1974 # XXX TODO: take type and unit from tapstcObj 1975 self.fieldInfo = fieldinfo.FieldInfo("spoly", unit="deg", ucd=None, 1976 stc=tapstc.getSTCForTAP(self.cooSys))
1977
1978 - def flatten(self):
1979 raise common.FlattenError("STCRegion objectcs cannot be flattened, they" 1980 " must be morphed.")
1981
1982 1983 -def makeSTCSRegion(spec):
1984 try: 1985 return STCSRegion(stc.parseSimpleSTCS(spec)) 1986 except stc.STCSParseError: #Not a valid STC spec, try next region parser 1987 return None
1988 1989 registerRegionMaker(makeSTCSRegion)
1990 1991 1992 -class Centroid(FunctionNode):
1993 type = "centroid" 1994
1995 - def addFieldInfo(self, context):
1996 self.fieldInfo = fieldinfo.FieldInfo(type="spoint", 1997 unit="", ucd="", 1998 userData=collectUserData(self._getInfoChildren())[0])
1999
2000 2001 -class Distance(FunctionNode):
2002 type = "distanceFunction" 2003 _a_pointArguments = False 2004
2005 - def addFieldInfo(self, context):
2006 self.fieldInfo = fieldinfo.FieldInfo(type="double precision", 2007 unit="deg", ucd="pos.angDistance", 2008 userData=collectUserData(self._getInfoChildren())[0])
2009 2010 @classmethod
2011 - def _getInitKWs(cls, _parseResult):
2012 args = parseArgs(_parseResult["args"]) 2013 if len(args)==2: 2014 if args[0].type=="point" and args[1].type=="point": 2015 # expand the points inline for later morphing 2016 args = args[0].x, args[0].y, args[1].x, args[1].y 2017 pointArguments = False 2018 else: 2019 # Leave the point arguments to pgsphere 2020 pointArguments = True 2021 elif len(args)==4: 2022 pointArguments = False 2023 else: 2024 raise AssertionError("Grammar let through invalid args to Distance") 2025 return locals()
2026
2027 2028 -class PredicateGeometryFunction(FunctionNode):
2029 type = "predicateGeometryFunction" 2030 2031 _pgFieldInfo = fieldinfo.FieldInfo("integer", "", "") 2032
2033 - def addFieldInfo(self, context):
2034 # swallow all upstream info, it really doesn't help here 2035 self.fieldInfo = self._pgFieldInfo
2036
2037 - def flatten(self):
2038 return "%s(%s)"%(self.funName, ", ".join(flatten(a) for a in self.args))
2039
2040 2041 -class PointFunction(FunctionNode):
2042 type = "pointFunction" 2043
2044 - def _makeCoordsysFieldInfo(self):
2045 return fieldinfo.FieldInfo("text", unit="", ucd="meta.ref;pos.frame")
2046
2047 - def _makeCoordFieldInfo(self):
2048 # this should pull in the metadata from the 1st or 2nd component 2049 # of the argument. However, given the way geometries are constructed 2050 # in ADQL, what comes back here is in degrees in the frame of the 2051 # child always. We're a bit pickier with the user data -- if there's 2052 # exactly two user data fields in the child, we assume the child 2053 # has been built from individual columns, and we try to retrieve the 2054 # one pulled out. 2055 childFieldInfo = self.args[0].fieldInfo 2056 if len(childFieldInfo.userData)==2: 2057 userData = (childFieldInfo.userData[int(self.funName[-1])-1],) 2058 else: 2059 userData = childFieldInfo.userData 2060 return fieldinfo.FieldInfo("double precision", 2061 ucd=None, unit="deg", userData=userData)
2062
2063 - def addFieldInfo(self, context):
2064 if self.funName=="COORDSYS": 2065 makeFieldInfo = self._makeCoordsysFieldInfo 2066 else: # it's coordN 2067 makeFieldInfo = self._makeCoordFieldInfo 2068 self.fieldInfo = makeFieldInfo()
2069
2070 2071 -class Area(FunctionNode):
2072 type = "area" 2073
2074 - def addFieldInfo(self, context):
2075 self.fieldInfo = fieldinfo.FieldInfo(type="double precision", 2076 unit="deg^2", ucd="phys.angSize", 2077 userData=collectUserData(self._getInfoChildren())[0])
2078