1
2 r"""
3 =====================================
4 Table inspection and representation
5 =====================================
6
7 Table inspection and representation
8
9 :Copyright:
10
11 Copyright 2010 - 2016
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
168 with _warnings.catch_warnings():
169 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
170 message=r'^Did not recognize type ')
171 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
172 message=r'^Unknown column definition ')
173 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
174 message=r'^Incomplete reflection of '
175 r'column definition')
176 _warnings.filterwarnings('ignore', category=_sa.exc.SAWarning,
177 message=r'^Could not instantiate type ')
178 _warnings.filterwarnings('ignore', category=_sa.exc.SAWarning,
179 message=r'^Skipped unsupported '
180 r'reflection of expression-based'
181 r' index ')
182 _warnings.filterwarnings('ignore', category=_sa.exc.SAWarning,
183 message=r'^Predicate of partial index ')
184
185 seen = set()
186 while True:
187 try:
188 table = _sa.Table(name, metadata, autoload=True, **kwargs)
189 except _sa.exc.SAWarning as e:
190 if types is not None:
191 match = tmatch(e.args[0])
192 if match:
193 type_name = match.group(1).strip()
194 if type_name.startswith(('"', "'")):
195 type_name = type_name[1:-1]
196 if type_name and type_name not in seen:
197 types(type_name, metadata, symbols)
198 seen.add(type_name)
199 continue
200 raise
201 else:
202 break
203
204 return cls(varname, table, schemas, symbols)
205
207 """
208 Make string representation
209
210 :Return: The string representation
211 :Rtype: ``str``
212 """
213 args = [
214 repr(_column.Column.from_sa(col, self._symbols))
215 for col in self.sa_table.columns
216 ]
217 if self.sa_table.schema is not None:
218 args.append('schema=%r' % (_util.unicode(self.sa_table.schema),))
219
220 args = ',\n '.join(args)
221 if args:
222 args = ',\n %s,\n' % args
223 result = "%s(%r, %s%s)" % (
224 self._symbols['table'],
225 _util.unicode(self.sa_table.name),
226 self._symbols['meta'],
227 args,
228 )
229 if self.constraints:
230 result = "\n".join((
231 result, '\n'.join(map(repr, sorted(self.constraints)))
232 ))
233 return result
234
237 """ Referenced table """
238 is_reference = True
239
240 - def __init__(self, varname, table, schema, symbols):
241 """
242 Initialization
243
244 :Parameters:
245 `varname` : ``str``
246 Variable name
247
248 `table` : ``sqlalchemy.Table``
249 Table
250
251 `symbols` : `Symbols`
252 Symbol table
253 """
254 self.varname = varname
255 self.sa_table = table
256 self.constraints = []
257 pkg, mod = schema.rsplit('.', 1)
258 if not mod.startswith('_'):
259 modas = '_' + mod
260 symbols.imports[schema] = 'from %s import %s as %s' % (
261 pkg, mod, modas
262 )
263 mod = modas
264 else:
265 symbols.imports[schema] = 'from %s import %s' % (pkg, mod)
266 symbols[u'table_%s' % table.name] = "%s.%s" % (mod, varname)
267
270 """ Table collection """
271
272 @classmethod
273 - def by_names(cls, metadata, names, schemas, symbols, types=None):
274 """
275 Construct by table names
276
277 :Parameters:
278 `metadata` : ``sqlalchemy.MetaData``
279 Metadata
280
281 `names` : iterable
282 Name list (list of tuples (varname, name))
283
284 `symbols` : `Symbols`
285 Symbol table
286
287 `types` : callable
288 Extra type loader. If the type reflection fails, because
289 SQLAlchemy cannot resolve it, the type loader will be called with
290 the type name, (bound) metadata and the symbol table. It is
291 responsible for modifying the symbols and imports *and* the
292 dialect's ``ischema_names``. If omitted or ``None``, the reflector
293 will always fail on unknown types.
294
295 :Return: New table collection instance
296 :Rtype: `TableCollection`
297 """
298 objects = dict((table.sa_table.key, table) for table in [
299 Table.by_name(name, varname, metadata, schemas, symbols,
300 types=types)
301 for varname, name in names
302 ])
303
304 def map_table(sa_table):
305 """ Map SA table to table object """
306 if sa_table.key not in objects:
307 varname = sa_table.name
308 if _util.py2 and \
309 isinstance(varname,
310 _util.unicode):
311 varname = varname.encode('ascii')
312 objects[sa_table.key] = Table(
313 varname, sa_table, schemas, symbols
314 )
315 return objects[sa_table.key]
316
317 tables = list(map(map_table, metadata.tables.values()))
318 tables.sort(key=lambda x: (not(x.is_reference), x.varname))
319
320 _break_cycles(metadata)
321 seen = set()
322
323 for table in tables:
324 seen.add(table.sa_table.key)
325 for con in table.constraints:
326
327 if type(con) == _constraint.ForeignKeyConstraint:
328 if con.options == 'seen':
329 continue
330
331 remote_key = con.constraint.elements[0].column.table.key
332 if remote_key not in seen:
333 con.options = 'unseen: %s' % (
334 objects[remote_key].varname,
335 )
336 remote_con = con.copy()
337 remote_con.options = 'seen: %s' % (table.varname,)
338 objects[remote_key].constraints.append(remote_con)
339
340 return cls(tables)
341
344 """
345 Find foreign key cycles and break them apart
346
347 :Parameters:
348 `metadata` : ``sqlalchemy.MetaData``
349 Metadata
350 """
351 def break_cycle(e):
352 """ Break foreign key cycle """
353 cycle_keys = set(map(_op.attrgetter('key'), e.cycles))
354 cycle_path = [
355 (parent, child)
356 for parent, child in e.edges
357 if parent.key in cycle_keys and child.key in cycle_keys
358 ]
359 deps = [cycle_path.pop()]
360 while cycle_path:
361 tmp = []
362 for parent, child in cycle_path:
363 if parent == deps[-1][1]:
364 deps.append((parent, child))
365 else:
366 tmp.append((parent, child))
367 if len(tmp) == len(cycle_path):
368 raise AssertionError("Could not construct sorted cycle path")
369 cycle_path = tmp
370 if deps[0][0].key != deps[-1][1].key:
371 raise AssertionError("Could not construct sorted cycle path")
372
373 deps = list(map(_op.itemgetter(0), deps))
374 first_dep = list(sorted(deps, key=_op.attrgetter('name')))[0]
375 while first_dep != deps[-1]:
376 deps = [deps[-1]] + deps[:-1]
377 deps.reverse()
378 logger.debug("Found foreign key cycle: %s", " -> ".join([
379 repr(table.name) for table in deps + [deps[0]]
380 ]))
381
382 def visit_foreign_key(fkey):
383 """ Visit foreign key """
384 if fkey.column.table == deps[1]:
385 fkey.use_alter = True
386 fkey.constraint.use_alter = True
387
388 _sa.sql.visitors.traverse(deps[0], dict(schema_visitor=True), dict(
389 foreign_key=visit_foreign_key,
390 ))
391
392 while True:
393 try:
394 metadata.sorted_tables
395 except _sa.exc.CircularDependencyError as e:
396 break_cycle(e)
397 else:
398 break
399