1 """
2 Node classes and factories used in ADQL tree processing.
3 """
4
5
6
7
8
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
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 """
39 self.replacingNode = replacingNode
40
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
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
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
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
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
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
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
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
149 return [c for c in nodeSeq if isinstance(c, cls)]
150
153
163
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
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
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
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
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
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
252 for cls in reversed(self.__class__.mro()):
253 if hasattr(cls, "_polish"):
254 cls._polish(self)
255 self._setupNodeNext(ADQLNode)
256
258 return "<ADQL Node %s>"%(self.type)
259
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
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
293
296
303
306 """a mixin just pulling through the children and serializing them.
307 """
308 _a_children = ()
309
310 @classmethod
312 return {"children": list(_parseResult)}
313
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
333
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
353
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
371 try:
372 args = parseArgs(_parseResult["args"])
373 except KeyError:
374 pass
375 funName = _parseResult["fName"].upper()
376 return locals()
377
379 return "%s(%s)"%(self.funName, ", ".join(flatten(a) for a in self.args))
380
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
399
401 """yields all relation names mentioned in this node.
402 """
403 raise TypeError("Override getAllNames for ColumnBearingNodes.")
404
409
414 type = "tableName"
415 _a_cat = None
416 _a_schema = None
417 _a_name = None
418
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
426
427 pass
428 return False
429
431 return not self==other
432
434 return bool(self.name)
435
437 return "TableName(%s)"%self.qName
438
449
450 @classmethod
452 _parts = _parseResult[::2]
453 cat, schema, name = [None]*(3-len(_parts))+_parts
454 return locals()
455
458
460 """returns self's qualified name in lower case.
461 """
462 return self.qName.lower()
463
464 @staticmethod
470
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
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
489 _a_originalTable = None
490
491 @classmethod
500
503
506
513
516
519
527
567
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
580 predicate = _parseResult[0].upper()
581 if predicate=="USING":
582 usingColumns = [
583 n for n in _parseResult["columnNames"] if n!=',']
584 del n
585 children = list(_parseResult)
586 return locals()
587
590 """the complete join operator (including all LEFT, RIGHT, ",", and whatever).
591 """
592 type = "joinOperator"
593
595 return self.children[0] in (',', 'CROSS')
596
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
618 leftOperand = _parseResult[0]
619 operator = _parseResult[1]
620 rightOperand = _parseResult[2]
621 if len(_parseResult)>3:
622 joinSpecification = _parseResult[3]
623 return locals()
624
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
637
639 self.joinedTables = [self.leftOperand, self.rightOperand]
640
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
650
652
653 return "_".join(t.makeUpId() for t in self.joinedTables)
654
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
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
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
703 return {"joinedTable": _parseResult[1]}
704
706 return "("+self.joinedTable.flatten()+")"
707
709 return getattr(self.joinedTable, attName)
710
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
732 import traceback
733 traceback.print_exc()
734 raise
735 return children[0]
736
739 """An abstract base for Nodes that don't parse out anything.
740 """
741 type = None
742
746
748 type = "groupByClause"
749
750 -class Having(TransparentNode):
751 type = "havingClause"
752
754 type = "sortSpecification"
755
757 type = "offsetSpec"
758
759 _a_offset = None
760
761 @classmethod
763 return {"offset": int(_parseResult[1])}
764
766 if self.offset is not None:
767 return "OFFSET %d"%self.offset
768 return ""
769
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
784 self.query = weakref.proxy(self)
785
786 @classmethod
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
804
806 if self.selectList.allFieldsQuery:
807 return self.fromClause.getAllFields()
808 else:
809 return self._iterSelectList()
810
813
816
819
821 return flattenKWs(self, ("SELECT", None),
822 ("", "setQuantifier"),
823 ("TOP", "setLimit"),
824 ("", "selectList"),
825 ("", "fromClause"),
826 ("", "whereClause"),
827 ("", "groupby"),
828 ("", "having"),
829 ("", "orderBy"))
830
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:
845 import traceback;traceback.print_exc()
846 return "weird_table_report_this"
847
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
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 """
890
892 self.fieldInfos = self._assertFieldInfosCompatible()
893
903
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):
915
918 """A query from a with clause.
919
920 This essentially does everything a table does.
921 """
922 type = "withQuery"
923
925 self.name = self.children[0]
926 for c in self.children:
927
928
929
930 if hasattr(c, "setLimit"):
931 self.select = c
932 break
933 else:
934 raise NotImplementedError("WithQuery without select?")
935
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
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
975
977 return getattr(self.children[0], attrName)
978
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
993
998 setLimit = property(_getSetLimit, _setSetLimit)
999
1000
1002 return getattr(self.children[-1], attrName)
1003
1006
1007
1008 type = "columnReference"
1009 bindings = ["geometryValue"]
1010 _a_refName = None
1011 _a_name = None
1012
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
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
1045
1051
1054
1057
1058
1059 type = "columnReferenceByUCD"
1060 bindings = ["columnReferenceByUCD"]
1061 _a_ucdWanted = None
1062
1063 @classmethod
1069
1071
1072
1073
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
1100
1103 type = "fromClause"
1104 _a_tableReference = ()
1105 _a_tables = ()
1106
1107 @classmethod
1109 parseResult = list(parseResult)
1110 if len(parseResult)==1:
1111 tableReference = parseResult[0]
1112 else:
1113
1114
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
1126
1128 """returns the names of all tables taking part in this from clause.
1129 """
1130 return self.tableReference.getAllNames()
1131
1134
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
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
1181
1184
1187 """A column within a select list.
1188 """
1189 type = "derivedColumn"
1190 _a_expr = None
1191 _a_alias = None
1192 _a_tainted = True
1193
1195 if getType(self.expr)=="columnReference":
1196 self.tainted = False
1197
1198 @property
1200
1201
1202
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
1214 expr = _parseResult["expr"]
1215 alias = _parseResult.get("alias")
1216 return locals()
1217
1219 return flattenKWs(self,
1220 ("", "expr"),
1221 ("AS", "alias"))
1222
1225
1228 type = "qualifiedStar"
1229 _a_sourceTable = None
1230
1231 @classmethod
1236
1238 return "%s.*"%flatten(self.sourceTable)
1239
1242 type = "selectList"
1243 _a_selectFields = ()
1244 _a_allFieldsQuery = False
1245
1246 @classmethod
1248 allFieldsQuery = _parseResult.get("starSel", False)
1249 if allFieldsQuery:
1250
1251 selectFields = None
1252 else:
1253 selectFields = list(_parseResult.get("fieldSel"))
1254 return locals()
1255
1257 if self.allFieldsQuery:
1258 return self.allFieldsQuery
1259 else:
1260 return ", ".join(flatten(sf) for sf in self.selectFields)
1261
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
1276 op1, opr, op2 = _parseResult
1277 return locals()
1278
1280 return "%s %s %s"%(flatten(self.op1), self.opr, flatten(self.op2))
1281
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
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
1325 type = "arrayReference"
1326 collapsible = False
1327
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
1347 )
1348
1352 infoChildren = self._getInfoChildren()
1353 if not infoChildren:
1354 if len(self.children)==1:
1355
1356
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):
1382
1397
1421
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
1434
1435
1436
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
1444 return infoChildren[0].fieldInfo.change(tainted=True)
1445 else:
1446
1447 return fieldinfo.FieldInfo("text", "", "")
1448
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
1476 funcName = self.children[0].upper()
1477 ucdPref, newUnit, newType = self.funcDefs[funcName]
1478
1479
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
1489 ucd = fi.ucd
1490 elif fi.ucd:
1491 ucd = ucdPref%(fi.ucd)
1492 else:
1493
1494
1495 if funcName=="COUNT":
1496 ucd = "meta.number"
1497 else:
1498 ucd = None
1499
1500
1501 if newUnit is None:
1502 newUnit = fi.unit
1503
1504
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
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
1520
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
1543 }
1544
1548
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
1566 type = "stringValueFunction"
1567
1571
1574 type = "timestampFunction"
1575
1584
1587 type = "inUnitFunction"
1588 _a_expr = None
1589 _a_unit = None
1590
1591 conversionFactor = None
1592
1593 @classmethod
1595 return {
1596 'expr': _parseResult[2],
1597 'unit': _parseResult[4].value,
1598 }
1599
1615
1626
1631
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
1644 value = "".join(_c[1:-1] for _c in _parseResult)
1645 return locals()
1646
1648 return "'%s'"%self.value
1649
1652
1655 type = "castSpecification"
1656 _a_value = None
1657 _a_newType = None
1658
1659 @classmethod
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
1678
1683 """is a mixin that works cooSys into FieldInfos for ADQL geometries.
1684 """
1685 _a_cooSys = None
1686
1687 @classmethod
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
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
1718 return "%s%s"%(self.type.upper(),
1719 "".join(flatten(arg) for arg in self.origArgs))
1720
1721 @classmethod
1723 return {"origArgs": list(_parseResult[1:])}
1724
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
1734
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
1764 return "%s(%s)"%(self.type.upper(),
1765 ", ".join(flatten(arg) for arg in [self.x, self.y]))
1766
1767 @classmethod
1769 x, y = parseArgs(_parseResult["args"])
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
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
1795 res["x"], res["y"], res["radius"] = args[0].x, args[0].y, args[1]
1796 else:
1797
1798
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):
1819
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
1833 return {"args": parseArgs(_parseResult["args"])}
1834
1838
1840 return ", ".join(flatten(a) for a in self.args)
1841
1844 type = "polygonSplitCooArgs"
1845
1848 type = "polygonPointCooArgs"
1849
1852 type = "polygon"
1853 _a_coos = None
1854 _a_points = None
1855 stcArgs = ("coos", "points")
1856 xtype = "polygon"
1857 sqlType = "spoly"
1858
1859 @classmethod
1861
1862
1863
1864
1865 arg = parseArgs(_parseResult["args"])[0]
1866
1867 if arg.type=="polygonPointCooArgs":
1868
1869 res = {"points": tuple(parseArgs(arg.args))}
1870
1871
1872
1873 for item in res["points"]:
1874 if item.type!="point":
1875 return res
1876
1877
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
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
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 = []
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
1964 bindings = []
1965 type = "stcsRegion"
1966 xtype = "adql:REGION"
1967
1968 _a_tapstcObj = None
1969
1971 self.cooSys = self.tapstcObj.cooSys
1972
1977
1979 raise common.FlattenError("STCRegion objectcs cannot be flattened, they"
1980 " must be morphed.")
1981
1988
1989 registerRegionMaker(makeSTCSRegion)
1990
1991
1992 -class Centroid(FunctionNode):
1993 type = "centroid"
1994
1999
2002 type = "distanceFunction"
2003 _a_pointArguments = False
2004
2009
2010 @classmethod
2012 args = parseArgs(_parseResult["args"])
2013 if len(args)==2:
2014 if args[0].type=="point" and args[1].type=="point":
2015
2016 args = args[0].x, args[0].y, args[1].x, args[1].y
2017 pointArguments = False
2018 else:
2019
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
2039
2042 type = "pointFunction"
2043
2046
2048
2049
2050
2051
2052
2053
2054
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
2069
2070
2071 -class Area(FunctionNode):
2078