1
2 r"""
3 =====================================
4 Table inspection and representation
5 =====================================
6
7 Table inspection and representation
8
9 :Copyright:
10
11 Copyright 2010 - 2017
12 Andr\xe9 Malo or his licensors, as applicable
13
14 :License:
15
16 Licensed under the Apache License, Version 2.0 (the "License");
17 you may not use this file except in compliance with the License.
18 You may obtain a copy of the License at
19
20 http://www.apache.org/licenses/LICENSE-2.0
21
22 Unless required by applicable law or agreed to in writing, software
23 distributed under the License is distributed on an "AS IS" BASIS,
24 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 See the License for the specific language governing permissions and
26 limitations under the License.
27
28 """
29 if __doc__:
30
31 __doc__ = __doc__.encode('ascii').decode('unicode_escape')
32 __author__ = r"Andr\xe9 Malo".encode('ascii').decode('unicode_escape')
33 __docformat__ = "restructuredtext en"
34
35 import logging as _logging
36 import operator as _op
37 import re as _re
38 import warnings as _warnings
39
40 import sqlalchemy as _sa
41
42 from . import _column
43 from . import _constraint
44 from . import _util
45
46 logger = _logging.getLogger(__name__)
47
48
49 -class Table(object):
50 """
51 Reflected table
52
53 :CVariables:
54 `is_reference` : ``bool``
55 Is it a table reference or a table?
56
57 :IVariables:
58 `varname` : ``str``
59 Variable name
60
61 `sa_table` : ``sqlalchemy.Table``
62 Table
63
64 `constraints` : ``list``
65 Constraint list
66
67 `_symbols` : `Symbols`
68 Symbol table
69 """
70 is_reference = False
71
72 - def __new__(cls, varname, table, schemas, symbols):
73 """
74 Construct
75
76 This might actually return a table reference
77
78 :Parameters:
79 `varname` : ``str``
80 Variable name
81
82 `table` : ``sqlalchemy.Table``
83 Table
84
85 `schemas` : ``dict``
86 Schema -> module mapping
87
88 `symbols` : `Symbols`
89 Symbol table
90
91 :Return: `Table` or `TableReference` instance
92 :Rtype: ``Table`` or ``TableReference``
93 """
94 if table.schema in schemas:
95 return TableReference(
96 varname, table, schemas[table.schema], symbols
97 )
98 return super(Table, cls).__new__(cls)
99
100 - def __init__(self, varname, table, schemas, symbols):
101 """
102 Initialization
103
104 :Parameters:
105 `varname` : ``str``
106 Variable name
107
108 `table` : ``sqlalchemy.Table``
109 Table
110
111 `schemas` : ``dict``
112 Schema -> module mapping
113
114 `symbols` : `Symbols`
115 Symbol table
116 """
117
118
119 symbols[u'table_%s' % table.name] = varname
120 self._symbols = symbols
121 self.varname = varname
122 self.sa_table = table
123 self.constraints = list(filter(None, [_constraint.Constraint(
124 con, self.varname, self._symbols,
125 ) for con in table.constraints]))
126
127 @classmethod
128 - def by_name(cls, name, varname, metadata, schemas, symbols, types=None):
129 """
130 Construct by name
131
132 :Parameters:
133 `name` : ``str``
134 Table name (possibly qualified)
135
136 `varname` : ``str``
137 Variable name of the table
138
139 `metadata` : SA (bound) metadata
140 Metadata container
141
142 `schemas` : ``dict``
143 Schema -> module mapping
144
145 `symbols` : `Symbols`
146 Symbol table
147
148 `types` : callable
149 Extra type loader. If the type reflection fails, because
150 SQLAlchemy cannot resolve it, the type loader will be called with
151 the type name, (bound) metadata and the symbol table. It is
152 responsible for modifying the symbols and imports *and* the
153 dialect's ``ischema_names``. If omitted or ``None``, the reflector
154 will always fail on unknown types.
155
156 :Return: New Table instance
157 :Rtype: `Table`
158 """
159 kwargs = {}
160 if '.' in name:
161 schema, name = name.split('.')
162 kwargs['schema'] = schema
163 else:
164 schema = None
165
166 tmatch = _re.compile(u"^Did not recognize type (.+) of column").match
167 def type_name(e):
168 """ Extract type name from exception """
169 match = tmatch(e.args[0])
170 if match:
171 type_name = match.group(1).strip()
172 if type_name.startswith(('"', "'")):
173 type_name = type_name[1:-1]
174 return type_name or None
175
176 with _warnings.catch_warnings():
177 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
178 message=r'^Did not recognize type ')
179 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
180 message=r'^Unknown column definition ')
181 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
182 message=r'^Incomplete reflection of '
183 r'column definition')
184 _warnings.filterwarnings('ignore', category=_sa.exc.SAWarning,
185 message=r'^Could not instantiate type ')
186 _warnings.filterwarnings('ignore', category=_sa.exc.SAWarning,
187 message=r'^Skipped unsupported '
188 r'reflection of expression-based'
189 r' index ')
190 _warnings.filterwarnings('ignore', category=_sa.exc.SAWarning,
191 message=r'^Predicate of partial index ')
192
193 seen = set()
194 while True:
195 try:
196 table = _sa.Table(name, metadata, autoload=True, **kwargs)
197 except _sa.exc.SAWarning as e:
198 if types is not None:
199 tname = type_name(e)
200 if tname and tname not in seen:
201 stack = [tname]
202 while stack:
203 try:
204 types(stack[-1], metadata, symbols)
205 except _sa.exc.SAWarning as e:
206 tname = type_name(e)
207 if tname and tname not in stack and \
208 tname not in seen:
209 stack.append(tname)
210 continue
211 raise
212 else:
213 seen.add(stack.pop())
214 continue
215 raise
216 else:
217 break
218
219 return cls(varname, table, schemas, symbols)
220
222 """
223 Make string representation
224
225 :Return: The string representation
226 :Rtype: ``str``
227 """
228 args = [
229 repr(_column.Column.from_sa(col, self._symbols))
230 for col in self.sa_table.columns
231 ]
232 if self.sa_table.schema is not None:
233 args.append('schema=%r' % (_util.unicode(self.sa_table.schema),))
234
235 args = ',\n '.join(args)
236 if args:
237 args = ',\n %s,\n' % args
238 result = "%s(%r, %s%s)" % (
239 self._symbols['table'],
240 _util.unicode(self.sa_table.name),
241 self._symbols['meta'],
242 args,
243 )
244 if self.constraints:
245 result = "\n".join((
246 result, '\n'.join(map(repr, sorted(self.constraints)))
247 ))
248 return result
249
252 """ Referenced table """
253 is_reference = True
254
255 - def __init__(self, varname, table, schema, symbols):
256 """
257 Initialization
258
259 :Parameters:
260 `varname` : ``str``
261 Variable name
262
263 `table` : ``sqlalchemy.Table``
264 Table
265
266 `symbols` : `Symbols`
267 Symbol table
268 """
269 self.varname = varname
270 self.sa_table = table
271 self.constraints = []
272 pkg, mod = schema.rsplit('.', 1)
273 if not mod.startswith('_'):
274 modas = '_' + mod
275 symbols.imports[schema] = 'from %s import %s as %s' % (
276 pkg, mod, modas
277 )
278 mod = modas
279 else:
280 symbols.imports[schema] = 'from %s import %s' % (pkg, mod)
281 symbols[u'table_%s' % table.name] = "%s.%s" % (mod, varname)
282
285 """ Table collection """
286
287 @classmethod
288 - def by_names(cls, metadata, names, schemas, symbols, types=None):
289 """
290 Construct by table names
291
292 :Parameters:
293 `metadata` : ``sqlalchemy.MetaData``
294 Metadata
295
296 `names` : iterable
297 Name list (list of tuples (varname, name))
298
299 `symbols` : `Symbols`
300 Symbol table
301
302 `types` : callable
303 Extra type loader. If the type reflection fails, because
304 SQLAlchemy cannot resolve it, the type loader will be called with
305 the type name, (bound) metadata and the symbol table. It is
306 responsible for modifying the symbols and imports *and* the
307 dialect's ``ischema_names``. If omitted or ``None``, the reflector
308 will always fail on unknown types.
309
310 :Return: New table collection instance
311 :Rtype: `TableCollection`
312 """
313 objects = dict((table.sa_table.key, table) for table in [
314 Table.by_name(name, varname, metadata, schemas, symbols,
315 types=types)
316 for varname, name in names
317 ])
318
319 def map_table(sa_table):
320 """ Map SA table to table object """
321 if sa_table.key not in objects:
322 varname = sa_table.name
323 if _util.py2 and \
324 isinstance(varname,
325 _util.unicode):
326 varname = varname.encode('ascii')
327 objects[sa_table.key] = Table(
328 varname, sa_table, schemas, symbols
329 )
330 return objects[sa_table.key]
331
332 tables = list(map(map_table, metadata.tables.values()))
333 tables.sort(key=lambda x: (not(x.is_reference), x.varname))
334
335 _break_cycles(metadata)
336 seen = set()
337
338 for table in tables:
339 seen.add(table.sa_table.key)
340 for con in table.constraints:
341
342 if type(con) == _constraint.ForeignKeyConstraint:
343 if con.options == 'seen':
344 continue
345
346 remote_key = con.constraint.elements[0].column.table.key
347 if remote_key not in seen:
348 con.options = 'unseen: %s' % (
349 objects[remote_key].varname,
350 )
351 remote_con = con.copy()
352 remote_con.options = 'seen: %s' % (table.varname,)
353 objects[remote_key].constraints.append(remote_con)
354
355 return cls(tables)
356
359 """
360 Find foreign key cycles and break them apart
361
362 :Parameters:
363 `metadata` : ``sqlalchemy.MetaData``
364 Metadata
365 """
366 def break_cycle(e):
367 """ Break foreign key cycle """
368 cycle_keys = set(map(_op.attrgetter('key'), e.cycles))
369 cycle_path = [
370 (parent, child)
371 for parent, child in e.edges
372 if parent.key in cycle_keys and child.key in cycle_keys
373 ]
374 deps = [cycle_path.pop()]
375 while cycle_path:
376 tmp = []
377 for parent, child in cycle_path:
378 if parent == deps[-1][1]:
379 deps.append((parent, child))
380 else:
381 tmp.append((parent, child))
382 if len(tmp) == len(cycle_path):
383 raise AssertionError("Could not construct sorted cycle path")
384 cycle_path = tmp
385 if deps[0][0].key != deps[-1][1].key:
386 raise AssertionError("Could not construct sorted cycle path")
387
388 deps = list(map(_op.itemgetter(0), deps))
389 first_dep = list(sorted(deps, key=_op.attrgetter('name')))[0]
390 while first_dep != deps[-1]:
391 deps = [deps[-1]] + deps[:-1]
392 deps.reverse()
393 logger.debug("Found foreign key cycle: %s", " -> ".join([
394 repr(table.name) for table in deps + [deps[0]]
395 ]))
396
397 def visit_foreign_key(fkey):
398 """ Visit foreign key """
399 if fkey.column.table == deps[1]:
400 fkey.use_alter = True
401 fkey.constraint.use_alter = True
402
403 _sa.sql.visitors.traverse(deps[0], dict(schema_visitor=True), dict(
404 foreign_key=visit_foreign_key,
405 ))
406
407 while True:
408 try:
409 metadata.sorted_tables
410 except _sa.exc.CircularDependencyError as e:
411 break_cycle(e)
412 else:
413 break
414