"""
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 getInfosFor(self, tableName): # pragma: no cover
raise NotImplementedError("Abstract FieldInfoGetter used!")