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 itertools as _it
36 import logging as _logging
37 import operator as _op
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(_it.ifilter(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):
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 :Return: New Table instance
149 :Rtype: `Table`
150 """
151 kwargs = {}
152 if '.' in name:
153 schema, name = name.split('.')
154 kwargs['schema'] = schema
155 else:
156 schema = None
157
158 with _warnings.catch_warnings():
159 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
160 message=r'^Did not recognize type ')
161 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
162 message=r'^Unknown column definition ')
163 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
164 message=r'^Incomplete reflection of '
165 r'column definition')
166 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
167 message=r'^Could not instantiate type ')
168
169 table = _sa.Table(name, metadata, autoload=True, **kwargs)
170
171
172
173
174
175
176
177
178
179 return cls(varname, table, schemas, symbols)
180
182 """
183 Make string representation
184
185 :Return: The string representation
186 :Rtype: ``str``
187 """
188 args = [
189 repr(_column.Column.from_sa(col, self._symbols))
190 for col in self.sa_table.columns
191 ]
192 if self.sa_table.schema is not None:
193 args.append('schema=%r' % (_util.unicode(self.sa_table.schema),))
194
195 args = ',\n '.join(args)
196 if args:
197 args = ',\n %s,\n' % args
198 result = "%s(%r, %s%s)" % (
199 self._symbols['table'],
200 _util.unicode(self.sa_table.name),
201 self._symbols['meta'],
202 args,
203 )
204 if self.constraints:
205 result = "\n".join((
206 result, '\n'.join(map(repr, sorted(self.constraints)))
207 ))
208 return result
209
212 """ Referenced table """
213 is_reference = True
214
215 - def __init__(self, varname, table, schema, symbols):
216 """
217 Initialization
218
219 :Parameters:
220 `varname` : ``str``
221 Variable name
222
223 `table` : ``sqlalchemy.Table``
224 Table
225
226 `symbols` : `Symbols`
227 Symbol table
228 """
229 self.varname = varname
230 self.sa_table = table
231 self.constraints = []
232 pkg, mod = schema.rsplit('.', 1)
233 if not mod.startswith('_'):
234 modas = '_' + mod
235 symbols.imports[schema] = 'from %s import %s as %s' % (
236 pkg, mod, modas
237 )
238 mod = modas
239 else:
240 symbols.imports[schema] = 'from %s import %s' % (pkg, mod)
241 symbols[u'table_%s' % table.name] = "%s.%s" % (mod, varname)
242
245 """ Table collection """
246
247 @classmethod
248 - def by_names(cls, metadata, names, schemas, symbols):
249 """
250 Construct by table names
251
252 :Parameters:
253 `metadata` : ``sqlalchemy.MetaData``
254 Metadata
255
256 `names` : iterable
257 Name list (list of tuples (varname, name))
258
259 `symbols` : `Symbols`
260 Symbol table
261
262 :Return: New table collection instance
263 :Rtype: `TableCollection`
264 """
265 objects = dict((table.sa_table.key, table) for table in [
266 Table.by_name(name, varname, metadata, schemas, symbols)
267 for varname, name in names
268 ])
269
270 def map_table(sa_table):
271 """ Map SA table to table object """
272 if sa_table.key not in objects:
273 varname = sa_table.name
274 if _util.py2 and isinstance(varname, _util.unicode):
275 varname = varname.encode('ascii')
276 objects[sa_table.key] = Table(
277 varname, sa_table, schemas, symbols
278 )
279 return objects[sa_table.key]
280
281 tables = list(_it.imap(map_table, metadata.tables.itervalues()))
282 tables.sort(key=lambda x: (not(x.is_reference), x.varname))
283
284 _break_cycles(metadata)
285 seen = set()
286
287 for table in tables:
288 seen.add(table.sa_table.key)
289 for con in table.constraints:
290
291 if type(con) == _constraint.ForeignKeyConstraint:
292 if con.options == 'seen':
293 continue
294
295 remote_key = con.constraint.elements[0].column.table.key
296 if remote_key not in seen:
297 con.options = 'unseen: %s' % (
298 objects[remote_key].varname,
299 )
300 remote_con = con.copy()
301 remote_con.options = 'seen: %s' % (table.varname,)
302 objects[remote_key].constraints.append(remote_con)
303
304 return cls(tables)
305
308 """
309 Find foreign key cycles and break them apart
310
311 :Parameters:
312 `metadata` : ``sqlalchemy.MetaData``
313 Metadata
314 """
315 def break_cycle(e):
316 """ Break foreign key cycle """
317 cycle_keys = set(_it.imap(_op.attrgetter('key'), e.cycles))
318 cycle_path = [
319 (parent, child)
320 for parent, child in e.edges
321 if parent.key in cycle_keys and child.key in cycle_keys
322 ]
323 deps = [cycle_path.pop()]
324 while cycle_path:
325 tmp = []
326 for parent, child in cycle_path:
327 if parent == deps[-1][1]:
328 deps.append((parent, child))
329 else:
330 tmp.append((parent, child))
331 if len(tmp) == len(cycle_path):
332 raise AssertionError("Could not construct sorted cycle path")
333 cycle_path = tmp
334 if deps[0][0].key != deps[-1][1].key:
335 raise AssertionError("Could not construct sorted cycle path")
336
337 deps = list(_it.imap(_op.itemgetter(0), deps))
338 first_dep = list(sorted(deps, key=_op.attrgetter('name')))[0]
339 while first_dep != deps[-1]:
340 deps = [deps[-1]] + deps[:-1]
341 deps.reverse()
342 logger.debug("Found foreign key cycle: %s", " -> ".join([
343 repr(table.name) for table in deps + [deps[0]]
344 ]))
345
346 def visit_foreign_key(fkey):
347 """ Visit foreign key """
348 if fkey.column.table == deps[1]:
349 fkey.use_alter = True
350 fkey.constraint.use_alter = True
351
352 _sa.sql.visitors.traverse(deps[0], dict(schema_visitor=True), dict(
353 foreign_key=visit_foreign_key,
354 ))
355
356 while True:
357 try:
358 metadata.sorted_tables
359 except _sa.exc.CircularDependencyError as e:
360 break_cycle(e)
361 else:
362 break
363