Source code for gavo.adql.common

"""
Exceptions and helper functions for ADQL processing.
"""

#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


from gavo import utils
from functools import reduce

[docs]class Error(utils.Error): """A base class for the exceptions from this module. """ # XXX todo: We should wrap pyparsing ParseExceptions as well. pass
[docs]class NotImplementedError(Error): """is raised for features we don't (yet) support. """
[docs]class ColumnNotFound(Error, utils.NotFoundError): """is raised if a column name cannot be resolved. """ def __init__(self, colName, hint=None): utils.NotFoundError.__init__(self, colName, "column", "table metadata", hint=hint)
[docs]class TableNotFound(Error, utils.NotFoundError): """is raised when a table name cannot be resolved. """ def __init__(self, tableName, hint=None): utils.NotFoundError.__init__(self, tableName, "table", "table metadata", hint=hint)
[docs]class MorphError(Error): """is raised when the expectations of the to-ADQL morphers are violated. """ pass
[docs]class AmbiguousColumn(Error): """is raised if a column name matches more than one column in a compound query. """
[docs]class NoChild(Error): """is raised if a node is asked for a non-existing child. """ def __init__(self, searchedType, toks): self.searchedType, self.toks = searchedType, toks def __str__(self): return "No %s child found in %s"%(self.searchedType, self.toks)
[docs]class MoreThanOneChild(NoChild): """is raised if a node is asked for a unique child but has more than one. """ def __str__(self): return "Multiple %s children found in %s"%(self.searchedType, self.toks)
[docs]class BadKeywords(Error): # pragma: no cover """is raised when an ADQL node is constructed with bad keywords. This is a development help and should not occur in production code. """ def __str__(self): return "Bad keywords: "+utils.safe_str(self.args)
[docs]class UfuncError(Error): """is raised if something is wrong with a call to a user defined function. """
[docs]class GeometryError(Error): """is raised if something is wrong with a geometry. """
[docs]class RegionError(GeometryError): """is raised if a region specification is in some way bad. """
[docs]class FlattenError(Error): """is raised when something cannot be flattened. """
[docs]class IncompatibleTables(Error): """is raised when the operands of a set operation are not deemed compatible. """
[docs]class Absent(object): """is a sentinel to pass as default to nodes.getChildOfType. """
[docs]def getUniqueMatch(matches, colName): """returns the only item of matches if there is exactly one, raises an appropriate exception if not. """ if len(matches)==1: return matches[0] elif not matches: raise ColumnNotFound(colName) else: # Todo: This kind-of, but not quite, compares whether the references # actually end up at the same column matches = set(matches) if len(matches)!=1: raise AmbiguousColumn(colName) else: return matches.pop()
[docs]def computeCommonColumns(tableNode): """returns a set of column names that only occur once in the result table. For a natural join, that's all column names occurring in all tables, for a USING join, that's all names occurring in USING, else it's an empty set. """ joinType = getattr(tableNode, "getJoinType", lambda: "CROSS")() if joinType=="NATURAL": # NATURAL JOIN, collect common names return reduce(lambda a,b: a&b, [set(t.fieldInfos.columns) for t in tableNode.joinedTables]) elif joinType=="USING": return set(tableNode.joinSpecification.usingColumns) else: # CROSS join, comma, etc. return set()
[docs]class FieldInfoGetter(object): """An abstract class to retrieve table metadata. A subclass of this must be passed into adql.parseAnnotating. Implementations must fill out the getInfosFor(tableName) method, which must return a sequence of (column name, adql.FieldInfo) pairs for the named table. plain strings for table names will be normalised (lowercased). """ def __init__(self): self.extraFieldInfos = {} self.cache = {}
[docs] def normalizeName(self, tableName): if isinstance(tableName, str): return tableName.lower() elif hasattr(tableName, "getNormalized"): # a nodes.TableName, presumably return tableName.getNormalized() else: return tableName
def __call__(self, tableName): normalized = self.normalizeName(tableName) if normalized in self.extraFieldInfos: return self.extraFieldInfos[normalized] if normalized not in self.cache: self.cache[normalized] = self.getInfosFor(normalized) if self.cache[normalized] is None: raise utils.NotFoundError(str(normalized), "table", "system and uploaded tables") return self.cache[normalized]
[docs] def addExtraFieldInfos(self, tableName, fieldInfos): """adds field infos for tableName. fieldInfos must be a sequence of (columnName, adql.FieldInfo) pairs. Note that tableName is normalised to lowercase here. """ self.extraFieldInfos[self.normalizeName(tableName)] = fieldInfos
[docs] def getInfosFor(self, tableName): # pragma: no cover raise NotImplementedError("Abstract FieldInfoGetter used!")