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

Source Code for Module gavo.adql.fieldinfos

  1  """ 
  2  FieldInfos are collections of inferred metadata on the columns present 
  3  within ADQL relations. 
  4   
  5  In generation, this module distinguishes between query-like (select...) 
  6  and table-like (from table references) field infos.  The functions 
  7  here are called from the addFieldInfo methods of the respective 
  8  nodes classes. 
  9  """ 
 10   
 11  #c Copyright 2008-2019, the GAVO project 
 12  #c 
 13  #c This program is free software, covered by the GNU GPL.  See the 
 14  #c COPYING file in the source distribution. 
 15   
 16   
 17  import weakref 
 18   
 19  # XXX TODO: This is a horrible mess.  Carefully read the SQL specification, 
 20  # figure out the actual rules for name resolution and then write an 
 21  # actual namespace management in annotations.py 
 22   
 23  from gavo.adql import common 
24 25 -def tableNamesMatch(table, toName):
26 """returns true when table could be referred to by toName. 27 28 This means that either the name matches or toName is table's original 29 name. 30 31 toName is a qualified name (i.e., including schema), table is some 32 node that has a tableName. 33 """ 34 if not hasattr(table, "tableName"): # the root query specifiation 35 return toName=="" 36 37 return (table.tableName.qName==toName.qName 38 or ( 39 table.originalTable 40 and 41 table.originalTable==toName.qName))
42
43 44 -class FieldInfos(object):
45 """ 46 A base class for field annotations. 47 48 Subclasses of those are attached to physical tables, joins, and 49 subqueries. 50 51 The information on columns is kept in two places: 52 53 - seq -- a sequence of attributes of the columns in the 54 order in which they are selected 55 - columns -- maps column names to attributes or None if a column 56 name is not unique. Column names are normalized by lowercasing here 57 (which, however, does not affect L{utils.QuotedName}s). 58 59 A FieldInfos object is instanciated with the object it will annotate, 60 and the annotation (i.e., setting of the fieldInfos attribute on 61 the parent) will happen during instanciation. 62 """
63 - def __init__(self, parent, context):
64 self.seq, self.columns = [], {} 65 self.parent = weakref.proxy(parent) 66 parent.fieldInfos = self 67 self._collectSubTables(parent, context)
68
69 - def __repr__(self):
70 return "<Column information %s>"%(repr(self.seq))
71
72 - def locateTable(self, refName):
73 """returns a table instance matching the node.TableName refName. 74 75 If no such table is in scope, the function raises a TableNotFound. 76 """ 77 if tableNamesMatch(self.parent, refName): 78 return self.parent 79 for t in self.subTables: 80 if tableNamesMatch(t, refName): 81 return t 82 raise common.TableNotFound(refName.qName)
83
84 - def addColumn(self, label, info):
85 """adds a new visible column to this info. 86 87 This entails both entering it in self.columns and in self.seq. 88 """ 89 label = label.lower() 90 if label in self.columns: 91 if self.columns[label]!=info: 92 self.columns[label] = None # Sentinel for ambiguous names 93 else: 94 self.columns[label] = info 95 96 # we rename database-reserved column names (oid and friends) 97 # these will still be referenced by their original names in the 98 # queries, and these will need to resolve so the translations 99 # can be done. For these columns, we keep their original names 100 # in the column index, too. I've not quite thought through all 101 # cases in which that could fail. Let's hope it's exotic enough 102 # that people won't probe all the dark corners... 103 if info.userData and getattr(info.userData[0], "originalName", None): 104 col = info.userData[0] 105 self.columns[col.originalName] = info 106 107 self.seq.append((label, info))
108
109 - def getFieldInfo(self, colName, refName=None):
110 """returns a FieldInfo object for colName. 111 112 Unknown columns result in a ColumnNotFound exception. 113 114 refName is ignored here; we may check that it's identical with 115 parent's name later. 116 """ 117 colName = colName.lower() 118 fi = self.columns.get(colName, common.Absent) 119 if fi is common.Absent: 120 raise common.ColumnNotFound(colName) 121 if fi is None: 122 # This can happen on joined tables 123 if refName is not None: 124 return self.locateTable(refName).getFieldInfo(colName) 125 raise common.AmbiguousColumn(colName) 126 return fi
127
128 - def assertIsCompatible(self, other):
129 """raises an IncompatibleTables if the FieldInfos other 130 have different length or names from self. 131 """ 132 if len(self.seq)!=len(other.seq): 133 raise common.IncompatibleTables("Operands in set operation" 134 " have differing result tuple lengths.", "query") 135 136 for ownCol, otherCol in zip(self.seq, other.seq): 137 if ownCol[0]!=otherCol[0]: 138 raise common.IncompatibleTables("Operands if set operation" 139 " have differing names. First differing name: %s vs. %s"%( 140 ownCol[0], otherCol[0]))
141
142 143 -class TableFieldInfos(FieldInfos):
144 """FieldInfos coming from something that's basically a table in the DB. 145 146 This includes joins. 147 148 To instanciate those, use the makeForNode class method below. 149 """ 150 151 @classmethod
152 - def makeForNode(cls, tableNode, context):
153 """returns a TableFieldInfos instance for an ADQL tableNode. 154 155 context is an AnnotationContext. 156 157 Whatever tableNode actually is, it needs an originalTable 158 attribute which is used to retrieve the column info. 159 """ 160 result = cls(tableNode, context) 161 162 if tableNode.originalTable: 163 # add infos for a host table if that's what this is 164 for colName, fieldInfo in context.retrieveFieldInfos( 165 tableNode.originalTable): 166 result.addColumn(colName, fieldInfo) 167 168 # add infos for joined tables as necessary; since we to a postorder 169 # traversal, those have already been annotated. 170 commonColumns = common.computeCommonColumns(tableNode) 171 emittedCommonColumns = set() 172 for jt in getattr(tableNode, "joinedTables", ()): 173 for label, info in jt.fieldInfos.seq: 174 if label in commonColumns: 175 if label not in emittedCommonColumns: 176 result.addColumn(label, info) 177 emittedCommonColumns.add(label) 178 else: 179 result.addColumn(label, info) 180 181 # annotate any join specs present 182 with context.customResolver(result.getFieldInfo): 183 _annotateNodeRecurse(tableNode, context) 184 return result
185
186 - def _collectSubTables(self, node, context):
187 self.subTables = list(node.getAllTables())
188
189 190 -def _annotateNodeRecurse(node, context):
191 """helps QueryFieldInfos. 192 """ 193 for c in node.iterNodeChildren(): 194 _annotateNodeRecurse(c, context) 195 if hasattr(node, "addFieldInfo") and node.fieldInfo is None: 196 node.addFieldInfo(context)
197
198 199 -class QueryFieldInfos(FieldInfos):
200 """FieldInfos inferred from a FROM clause. 201 202 To instanciate those, use the makeForNode class method below. 203 """ 204 205 # enclosingQuery is set non-None when a whereClause is found in the 206 # ancestors in _collectSubTables. It then refers to the query spec 207 # the where clause is a child from. All names from that qs are 208 # also immediately accessible from the current qs. 209 enclosingQuery = None 210 211 212 @classmethod
213 - def makeForNode(cls, queryNode, context):
214 """cf. TableFieldInfos.makeForNode. 215 """ 216 result = cls(queryNode, context) 217 218 # annotate the children of the select clause, using info 219 # from queryNode's queried tables; we must manipulate the context's 220 # name resolution. 221 with context.customResolver(result.getFieldInfoFromSources): 222 for selField in queryNode.getSelectFields(): 223 _annotateNodeRecurse(selField, context) 224 225 # annotate the children of the where clause, too -- their types 226 # and such may be needed by morphers 227 with context.customResolver(result.getFieldInfo): 228 if queryNode.whereClause: 229 _annotateNodeRecurse(queryNode.whereClause, context) 230 231 for col in queryNode.getSelectFields(): 232 result.addColumn(col.name, col.fieldInfo) 233 234 with context.customResolver(result.getFieldInfo): 235 if queryNode.having: 236 _annotateNodeRecurse(queryNode.having, context) 237 if queryNode.groupby: 238 _annotateNodeRecurse(queryNode.groupby, context) 239 240 return result
241
242 - def _getEnclosingQuery(self, context):
243 """returns the enclosing query specification if this is a subquery within 244 a where clause. 245 """ 246 ancs = context.ancestors 247 index = len(ancs)-1 248 while index>=0: 249 if ancs[index].type=="whereClause": 250 return ancs[index-1] 251 index -= 1
252
253 - def _collectSubTables(self, queryNode, context):
254 self.subTables = list( 255 queryNode.fromClause.tableReference.getAllTables()) 256 self.tableReference = queryNode.fromClause.tableReference 257 258 # if we are in a from clause, add its querySpecification, too 259 # (for things like exists(select * from x where a=q.b)) 260 encQS = self._getEnclosingQuery(context) 261 if encQS: 262 self.subTables.append(encQS) 263 self.subTables.extend( 264 encQS.fromClause.tableReference.getAllTables()) 265 self.enclosingQuery = encQS
266
267 - def getFieldInfoFromSources(self, colName, refName=None):
268 """returns a field info for colName from anything in the from clause. 269 270 That is, the columns in the select clause are ignored. Use this to 271 resolve expressions from the queries' select clause. 272 273 See getFieldInfo for refName 274 """ 275 colName = colName.lower() 276 matched = [] 277 if refName is None: 278 # no explicit table reference, in immediate table 279 subCols = self.tableReference.fieldInfos.columns 280 if colName in subCols and subCols[colName]: 281 matched.append(subCols[colName]) 282 if self.enclosingQuery: 283 subCols = (self.enclosingQuery.fromClause. 284 tableReference.fieldInfos.columns) 285 if colName in subCols and subCols[colName]: 286 matched.append(subCols[colName]) 287 288 else: 289 # locate an appropriate table 290 subCols = self.locateTable(refName).fieldInfos.columns 291 if colName in subCols and subCols[colName]: 292 matched.append(subCols[colName]) 293 294 # XXX TODO: make qualified names here and use them in JoindTable.getAllFields 295 return common.getUniqueMatch(matched, colName)
296
297 - def getFieldInfo(self, colName, refName=None):
298 """returns a field info for colName in self or any tables this 299 query takes columns from. 300 301 To do that, it collects all fields of colName in self and subTables and 302 returns the matching field if there's exactly one. Otherwise, it 303 will raise ColumnNotFound or AmbiguousColumn. 304 305 If the node.TableName instance refName is given, the search will be 306 restricted to the matching tables. 307 """ 308 ownMatch = None 309 if refName is None: 310 ownMatch = self.columns.get(colName, None) 311 if ownMatch: 312 return ownMatch 313 else: 314 return self.getFieldInfoFromSources(colName, refName)
315