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 warnings as _warnings
38
39 import sqlalchemy as _sa
40
41 from . import _column
42 from . import _constraint
43 from . import _util
44
45 logger = _logging.getLogger(__name__)
46
47
48 -class Table(object):
49 """
50 Reflected table
51
52 :CVariables:
53 `is_reference` : ``bool``
54 Is it a table reference or a table?
55
56 :IVariables:
57 `varname` : ``str``
58 Variable name
59
60 `sa_table` : ``sqlalchemy.Table``
61 Table
62
63 `constraints` : ``list``
64 Constraint list
65
66 `_symbols` : `Symbols`
67 Symbol table
68 """
69 is_reference = False
70
71 - def __new__(cls, varname, table, schemas, symbols):
72 """
73 Construct
74
75 This might actually return a table reference
76
77 :Parameters:
78 `varname` : ``str``
79 Variable name
80
81 `table` : ``sqlalchemy.Table``
82 Table
83
84 `schemas` : ``dict``
85 Schema -> module mapping
86
87 `symbols` : `Symbols`
88 Symbol table
89
90 :Return: `Table` or `TableReference` instance
91 :Rtype: ``Table`` or ``TableReference``
92 """
93 if table.schema in schemas:
94 return TableReference(
95 varname, table, schemas[table.schema], symbols
96 )
97 return super(Table, cls).__new__(cls)
98
99 - def __init__(self, varname, table, schemas, symbols):
100 """
101 Initialization
102
103 :Parameters:
104 `varname` : ``str``
105 Variable name
106
107 `table` : ``sqlalchemy.Table``
108 Table
109
110 `schemas` : ``dict``
111 Schema -> module mapping
112
113 `symbols` : `Symbols`
114 Symbol table
115 """
116
117
118 symbols[u'table_%s' % table.name] = varname
119 self._symbols = symbols
120 self.varname = varname
121 self.sa_table = table
122 self.constraints = list(filter(None, [_constraint.Constraint(
123 con, self.varname, self._symbols,
124 ) for con in table.constraints]))
125
126 @classmethod
127 - def by_name(cls, name, varname, metadata, schemas, symbols):
128 """
129 Construct by name
130
131 :Parameters:
132 `name` : ``str``
133 Table name (possibly qualified)
134
135 `varname` : ``str``
136 Variable name of the table
137
138 `metadata` : SA (bound) metadata
139 Metadata container
140
141 `schemas` : ``dict``
142 Schema -> module mapping
143
144 `symbols` : `Symbols`
145 Symbol table
146
147 :Return: New Table instance
148 :Rtype: `Table`
149 """
150 kwargs = {}
151 if '.' in name:
152 schema, name = name.split('.')
153 kwargs['schema'] = schema
154 else:
155 schema = None
156
157 with _warnings.catch_warnings():
158 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
159 message=r'^Did not recognize type ')
160 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
161 message=r'^Unknown column definition ')
162 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
163 message=r'^Incomplete reflection of '
164 r'column definition')
165 _warnings.filterwarnings('error', category=_sa.exc.SAWarning,
166 message=r'^Could not instantiate type ')
167
168 table = _sa.Table(name, metadata, autoload=True, **kwargs)
169
170
171
172
173
174
175
176
177
178 return cls(varname, table, schemas, symbols)
179
181 """
182 Make string representation
183
184 :Return: The string representation
185 :Rtype: ``str``
186 """
187 args = [
188 repr(_column.Column.from_sa(col, self._symbols))
189 for col in self.sa_table.columns
190 ]
191 if self.sa_table.schema is not None:
192 args.append('schema=%r' % (_util.unicode(self.sa_table.schema),))
193
194 args = ',\n '.join(args)
195 if args:
196 args = ',\n %s,\n' % args
197 result = "%s(%r, %s%s)" % (
198 self._symbols['table'],
199 _util.unicode(self.sa_table.name),
200 self._symbols['meta'],
201 args,
202 )
203 if self.constraints:
204 result = "\n".join((
205 result, '\n'.join(map(repr, sorted(self.constraints)))
206 ))
207 return result
208
211 """ Referenced table """
212 is_reference = True
213
214 - def __init__(self, varname, table, schema, symbols):
215 """
216 Initialization
217
218 :Parameters:
219 `varname` : ``str``
220 Variable name
221
222 `table` : ``sqlalchemy.Table``
223 Table
224
225 `symbols` : `Symbols`
226 Symbol table
227 """
228 self.varname = varname
229 self.sa_table = table
230 self.constraints = []
231 pkg, mod = schema.rsplit('.', 1)
232 if not mod.startswith('_'):
233 modas = '_' + mod
234 symbols.imports[schema] = 'from %s import %s as %s' % (
235 pkg, mod, modas
236 )
237 mod = modas
238 else:
239 symbols.imports[schema] = 'from %s import %s' % (pkg, mod)
240 symbols[u'table_%s' % table.name] = "%s.%s" % (mod, varname)
241
244 """ Table collection """
245
246 @classmethod
247 - def by_names(cls, metadata, names, schemas, symbols):
248 """
249 Construct by table names
250
251 :Parameters:
252 `metadata` : ``sqlalchemy.MetaData``
253 Metadata
254
255 `names` : iterable
256 Name list (list of tuples (varname, name))
257
258 `symbols` : `Symbols`
259 Symbol table
260
261 :Return: New table collection instance
262 :Rtype: `TableCollection`
263 """
264 objects = dict((table.sa_table.key, table) for table in [
265 Table.by_name(name, varname, metadata, schemas, symbols)
266 for varname, name in names
267 ])
268
269 def map_table(sa_table):
270 """ Map SA table to table object """
271 if sa_table.key not in objects:
272 varname = sa_table.name
273 if _util.py2 and isinstance(varname, _util.unicode):
274 varname = varname.encode('ascii')
275 objects[sa_table.key] = Table(
276 varname, sa_table, schemas, symbols
277 )
278 return objects[sa_table.key]
279
280 tables = list(map(map_table, metadata.tables.itervalues()))
281 tables.sort(key=lambda x: (not(x.is_reference), x.varname))
282
283 _break_cycles(metadata)
284 seen = set()
285
286 for table in tables:
287 seen.add(table.sa_table.key)
288 for con in table.constraints:
289
290 if type(con) == _constraint.ForeignKeyConstraint:
291 if con.options == 'seen':
292 continue
293
294 remote_key = con.constraint.elements[0].column.table.key
295 if remote_key not in seen:
296 con.options = 'unseen: %s' % (
297 objects[remote_key].varname,
298 )
299 remote_con = con.copy()
300 remote_con.options = 'seen: %s' % (table.varname,)
301 objects[remote_key].constraints.append(remote_con)
302
303 return cls(tables)
304
307 """
308 Find foreign key cycles and break them apart
309
310 :Parameters:
311 `metadata` : ``sqlalchemy.MetaData``
312 Metadata
313 """
314 def break_cycle(e):
315 """ Break foreign key cycle """
316 cycle_keys = set(map(_op.attrgetter('key'), e.cycles))
317 cycle_path = [
318 (parent, child)
319 for parent, child in e.edges
320 if parent.key in cycle_keys and child.key in cycle_keys
321 ]
322 deps = [cycle_path.pop()]
323 while cycle_path:
324 tmp = []
325 for parent, child in cycle_path:
326 if parent == deps[-1][1]:
327 deps.append((parent, child))
328 else:
329 tmp.append((parent, child))
330 if len(tmp) == len(cycle_path):
331 raise AssertionError("Could not construct sorted cycle path")
332 cycle_path = tmp
333 if deps[0][0].key != deps[-1][1].key:
334 raise AssertionError("Could not construct sorted cycle path")
335
336 deps = list(map(_op.itemgetter(0), deps))
337 first_dep = list(sorted(deps, key=_op.attrgetter('name')))[0]
338 while first_dep != deps[-1]:
339 deps = [deps[-1]] + deps[:-1]
340 deps.reverse()
341 logger.debug("Found foreign key cycle: %s", " -> ".join([
342 repr(table.name) for table in deps + [deps[0]]
343 ]))
344
345 def visit_foreign_key(fkey):
346 """ Visit foreign key """
347 if fkey.column.table == deps[1]:
348 fkey.use_alter = True
349 fkey.constraint.use_alter = True
350
351 _sa.sql.visitors.traverse(deps[0], dict(schema_visitor=True), dict(
352 foreign_key=visit_foreign_key,
353 ))
354
355 while True:
356 try:
357 metadata.sorted_tables
358 except _sa.exc.CircularDependencyError as e:
359 break_cycle(e)
360 else:
361 break
362