Show More
@@ -1,701 +1,715 b'' | |||
|
1 | 1 | """ |
|
2 | 2 | Schema module providing common schema operations. |
|
3 | 3 | """ |
|
4 | 4 | import abc |
|
5 | 5 | try: # Python 3 |
|
6 | 6 | from collections.abc import MutableMapping as DictMixin |
|
7 | 7 | except ImportError: # Python 2 |
|
8 | 8 | from UserDict import DictMixin |
|
9 | 9 | import warnings |
|
10 | 10 | |
|
11 | 11 | import sqlalchemy |
|
12 | 12 | |
|
13 | 13 | from sqlalchemy.schema import ForeignKeyConstraint |
|
14 | 14 | from sqlalchemy.schema import UniqueConstraint |
|
15 | 15 | |
|
16 | 16 | from rhodecode.lib.dbmigrate.migrate.exceptions import * |
|
17 | 17 | from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07, SQLA_08 |
|
18 | 18 | from rhodecode.lib.dbmigrate.migrate.changeset import util |
|
19 | 19 | from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import ( |
|
20 | 20 | get_engine_visitor, run_single_visitor) |
|
21 | 21 | |
|
22 | 22 | |
|
23 | 23 | __all__ = [ |
|
24 | 24 | 'create_column', |
|
25 | 25 | 'drop_column', |
|
26 | 26 | 'alter_column', |
|
27 | 27 | 'rename_table', |
|
28 | 28 | 'rename_index', |
|
29 | 29 | 'ChangesetTable', |
|
30 | 30 | 'ChangesetColumn', |
|
31 | 31 | 'ChangesetIndex', |
|
32 | 32 | 'ChangesetDefaultClause', |
|
33 | 33 | 'ColumnDelta', |
|
34 | 34 | ] |
|
35 | 35 | |
|
36 | 36 | |
|
37 | 37 | def create_column(column, table=None, *p, **kw): |
|
38 | 38 | """Create a column, given the table. |
|
39 | 39 | |
|
40 | 40 | API to :meth:`ChangesetColumn.create`. |
|
41 | 41 | """ |
|
42 | 42 | if table is not None: |
|
43 | 43 | return table.create_column(column, *p, **kw) |
|
44 | 44 | return column.create(*p, **kw) |
|
45 | 45 | |
|
46 | 46 | |
|
47 | 47 | def drop_column(column, table=None, *p, **kw): |
|
48 | 48 | """Drop a column, given the table. |
|
49 | 49 | |
|
50 | 50 | API to :meth:`ChangesetColumn.drop`. |
|
51 | 51 | """ |
|
52 | 52 | if table is not None: |
|
53 | 53 | return table.drop_column(column, *p, **kw) |
|
54 | 54 | return column.drop(*p, **kw) |
|
55 | 55 | |
|
56 | 56 | |
|
57 | 57 | def rename_table(table, name, engine=None, **kw): |
|
58 | 58 | """Rename a table. |
|
59 | 59 | |
|
60 | 60 | If Table instance is given, engine is not used. |
|
61 | 61 | |
|
62 | 62 | API to :meth:`ChangesetTable.rename`. |
|
63 | 63 | |
|
64 | 64 | :param table: Table to be renamed. |
|
65 | 65 | :param name: New name for Table. |
|
66 | 66 | :param engine: Engine instance. |
|
67 | 67 | :type table: string or Table instance |
|
68 | 68 | :type name: string |
|
69 | 69 | :type engine: obj |
|
70 | 70 | """ |
|
71 | 71 | table = _to_table(table, engine) |
|
72 | 72 | table.rename(name, **kw) |
|
73 | 73 | |
|
74 | 74 | |
|
75 | 75 | def rename_index(index, name, table=None, engine=None, **kw): |
|
76 | 76 | """Rename an index. |
|
77 | 77 | |
|
78 | 78 | If Index instance is given, |
|
79 | 79 | table and engine are not used. |
|
80 | 80 | |
|
81 | 81 | API to :meth:`ChangesetIndex.rename`. |
|
82 | 82 | |
|
83 | 83 | :param index: Index to be renamed. |
|
84 | 84 | :param name: New name for index. |
|
85 | 85 | :param table: Table to which Index is reffered. |
|
86 | 86 | :param engine: Engine instance. |
|
87 | 87 | :type index: string or Index instance |
|
88 | 88 | :type name: string |
|
89 | 89 | :type table: string or Table instance |
|
90 | 90 | :type engine: obj |
|
91 | 91 | """ |
|
92 | 92 | index = _to_index(index, table, engine) |
|
93 | 93 | index.rename(name, **kw) |
|
94 | 94 | |
|
95 | 95 | |
|
96 | 96 | def alter_column(*p, **k): |
|
97 | 97 | """Alter a column. |
|
98 | 98 | |
|
99 | 99 | This is a helper function that creates a :class:`ColumnDelta` and |
|
100 | 100 | runs it. |
|
101 | 101 | |
|
102 | 102 | :argument column: |
|
103 | 103 | The name of the column to be altered or a |
|
104 | 104 | :class:`ChangesetColumn` column representing it. |
|
105 | 105 | |
|
106 | 106 | :param table: |
|
107 | 107 | A :class:`~sqlalchemy.schema.Table` or table name to |
|
108 | 108 | for the table where the column will be changed. |
|
109 | 109 | |
|
110 | 110 | :param engine: |
|
111 | 111 | The :class:`~sqlalchemy.engine.base.Engine` to use for table |
|
112 | 112 | reflection and schema alterations. |
|
113 | 113 | |
|
114 | 114 | :returns: A :class:`ColumnDelta` instance representing the change. |
|
115 | 115 | |
|
116 | 116 | |
|
117 | 117 | """ |
|
118 | 118 | |
|
119 | 119 | if 'table' not in k and isinstance(p[0], sqlalchemy.Column): |
|
120 | 120 | k['table'] = p[0].table |
|
121 | 121 | if 'engine' not in k: |
|
122 | 122 | k['engine'] = k['table'].bind |
|
123 | 123 | |
|
124 | 124 | # deprecation |
|
125 | 125 | if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column): |
|
126 | 126 | warnings.warn( |
|
127 | 127 | "Passing a Column object to alter_column is deprecated." |
|
128 | 128 | " Just pass in keyword parameters instead.", |
|
129 | 129 | MigrateDeprecationWarning |
|
130 | 130 | ) |
|
131 | 131 | engine = k['engine'] |
|
132 | 132 | |
|
133 | 133 | # enough tests seem to break when metadata is always altered |
|
134 | 134 | # that this crutch has to be left in until they can be sorted |
|
135 | 135 | # out |
|
136 | 136 | k['alter_metadata']=True |
|
137 | 137 | |
|
138 | 138 | delta = ColumnDelta(*p, **k) |
|
139 | 139 | |
|
140 | 140 | visitorcallable = get_engine_visitor(engine, 'schemachanger') |
|
141 |
|
|
|
141 | _run_visitor(engine, visitorcallable, delta) | |
|
142 | 142 | |
|
143 | 143 | return delta |
|
144 | 144 | |
|
145 | 145 | |
|
146 | 146 | def _to_table(table, engine=None): |
|
147 | 147 | """Return if instance of Table, else construct new with metadata""" |
|
148 | 148 | if isinstance(table, sqlalchemy.Table): |
|
149 | 149 | return table |
|
150 | 150 | |
|
151 | 151 | # Given: table name, maybe an engine |
|
152 | 152 | meta = sqlalchemy.MetaData() |
|
153 | 153 | if engine is not None: |
|
154 | 154 | meta.bind = engine |
|
155 | 155 | return sqlalchemy.Table(table, meta) |
|
156 | 156 | |
|
157 | 157 | |
|
158 | 158 | def _to_index(index, table=None, engine=None): |
|
159 | 159 | """Return if instance of Index, else construct new with metadata""" |
|
160 | 160 | if isinstance(index, sqlalchemy.Index): |
|
161 | 161 | return index |
|
162 | 162 | |
|
163 | 163 | # Given: index name; table name required |
|
164 | 164 | table = _to_table(table, engine) |
|
165 | 165 | ret = sqlalchemy.Index(index) |
|
166 | 166 | ret.table = table |
|
167 | 167 | return ret |
|
168 | 168 | |
|
169 | 169 | |
|
170 | def _run_visitor( | |
|
171 | connectable, visitorcallable, element, connection=None, **kwargs | |
|
172 | ): | |
|
173 | if connection is not None: | |
|
174 | visitorcallable( | |
|
175 | connection.dialect, connection, **kwargs).traverse_single(element) | |
|
176 | else: | |
|
177 | conn = connectable.connect() | |
|
178 | try: | |
|
179 | visitorcallable( | |
|
180 | conn.dialect, conn, **kwargs).traverse_single(element) | |
|
181 | finally: | |
|
182 | conn.close() | |
|
183 | ||
|
170 | 184 | |
|
171 | 185 | # Python3: if we just use: |
|
172 | 186 | # |
|
173 | 187 | # class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem): |
|
174 | 188 | # ... |
|
175 | 189 | # |
|
176 | 190 | # We get the following error: |
|
177 | 191 | # TypeError: metaclass conflict: the metaclass of a derived class must be a |
|
178 | 192 | # (non-strict) subclass of the metaclasses of all its bases. |
|
179 | 193 | # |
|
180 | 194 | # The complete inheritance/metaclass relationship list of ColumnDelta can be |
|
181 | 195 | # summarized by this following dot file: |
|
182 | 196 | # |
|
183 | 197 | # digraph test123 { |
|
184 | 198 | # ColumnDelta -> MutableMapping; |
|
185 | 199 | # MutableMapping -> Mapping; |
|
186 | 200 | # Mapping -> {Sized Iterable Container}; |
|
187 | 201 | # {Sized Iterable Container} -> ABCMeta[style=dashed]; |
|
188 | 202 | # |
|
189 | 203 | # ColumnDelta -> SchemaItem; |
|
190 | 204 | # SchemaItem -> {SchemaEventTarget Visitable}; |
|
191 | 205 | # SchemaEventTarget -> object; |
|
192 | 206 | # Visitable -> {VisitableType object} [style=dashed]; |
|
193 | 207 | # VisitableType -> type; |
|
194 | 208 | # } |
|
195 | 209 | # |
|
196 | 210 | # We need to use a metaclass that inherits from all the metaclasses of |
|
197 | 211 | # DictMixin and sqlalchemy.schema.SchemaItem. Let's call it "MyMeta". |
|
198 | 212 | class MyMeta(sqlalchemy.sql.visitors.VisitableType, abc.ABCMeta, object): |
|
199 | 213 | pass |
|
200 | 214 | |
|
201 | 215 | |
|
202 | 216 | class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem, metaclass=MyMeta): |
|
203 | 217 | """Extracts the differences between two columns/column-parameters |
|
204 | 218 | |
|
205 | 219 | May receive parameters arranged in several different ways: |
|
206 | 220 | |
|
207 | 221 | * **current_column, new_column, \*p, \*\*kw** |
|
208 | 222 | Additional parameters can be specified to override column |
|
209 | 223 | differences. |
|
210 | 224 | |
|
211 | 225 | * **current_column, \*p, \*\*kw** |
|
212 | 226 | Additional parameters alter current_column. Table name is extracted |
|
213 | 227 | from current_column object. |
|
214 | 228 | Name is changed to current_column.name from current_name, |
|
215 | 229 | if current_name is specified. |
|
216 | 230 | |
|
217 | 231 | * **current_col_name, \*p, \*\*kw** |
|
218 | 232 | Table kw must specified. |
|
219 | 233 | |
|
220 | 234 | :param table: Table at which current Column should be bound to.\ |
|
221 | 235 | If table name is given, reflection will be used. |
|
222 | 236 | :type table: string or Table instance |
|
223 | 237 | |
|
224 | 238 | :param metadata: A :class:`MetaData` instance to store |
|
225 | 239 | reflected table names |
|
226 | 240 | |
|
227 | 241 | :param engine: When reflecting tables, either engine or metadata must \ |
|
228 | 242 | be specified to acquire engine object. |
|
229 | 243 | :type engine: :class:`Engine` instance |
|
230 | 244 | :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \ |
|
231 | 245 | `result_column` through :func:`dict` alike object. |
|
232 | 246 | |
|
233 | 247 | * :class:`ColumnDelta`.result_column is altered column with new attributes |
|
234 | 248 | |
|
235 | 249 | * :class:`ColumnDelta`.current_name is current name of column in db |
|
236 | 250 | |
|
237 | 251 | |
|
238 | 252 | """ |
|
239 | 253 | |
|
240 | 254 | # Column attributes that can be altered |
|
241 | 255 | diff_keys = ('name', 'type', 'primary_key', 'nullable', |
|
242 | 256 | 'server_onupdate', 'server_default', 'autoincrement') |
|
243 | 257 | diffs = dict() |
|
244 | 258 | __visit_name__ = 'column' |
|
245 | 259 | |
|
246 | 260 | def __init__(self, *p, **kw): |
|
247 | 261 | # 'alter_metadata' is not a public api. It exists purely |
|
248 | 262 | # as a crutch until the tests that fail when 'alter_metadata' |
|
249 | 263 | # behaviour always happens can be sorted out |
|
250 | 264 | self.alter_metadata = kw.pop("alter_metadata", False) |
|
251 | 265 | |
|
252 | 266 | self.meta = kw.pop("metadata", None) |
|
253 | 267 | self.engine = kw.pop("engine", None) |
|
254 | 268 | |
|
255 | 269 | # Things are initialized differently depending on how many column |
|
256 | 270 | # parameters are given. Figure out how many and call the appropriate |
|
257 | 271 | # method. |
|
258 | 272 | if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column): |
|
259 | 273 | # At least one column specified |
|
260 | 274 | if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column): |
|
261 | 275 | # Two columns specified |
|
262 | 276 | diffs = self.compare_2_columns(*p, **kw) |
|
263 | 277 | else: |
|
264 | 278 | # Exactly one column specified |
|
265 | 279 | diffs = self.compare_1_column(*p, **kw) |
|
266 | 280 | else: |
|
267 | 281 | # Zero columns specified |
|
268 | 282 | if not len(p) or not isinstance(p[0], str): |
|
269 | 283 | raise ValueError("First argument must be column name") |
|
270 | 284 | diffs = self.compare_parameters(*p, **kw) |
|
271 | 285 | |
|
272 | 286 | self.apply_diffs(diffs) |
|
273 | 287 | |
|
274 | 288 | def __repr__(self): |
|
275 | 289 | return '<ColumnDelta altermetadata=%r, %s>' % ( |
|
276 | 290 | self.alter_metadata, |
|
277 | 291 | super(ColumnDelta, self).__repr__() |
|
278 | 292 | ) |
|
279 | 293 | |
|
280 | 294 | def __getitem__(self, key): |
|
281 | 295 | if key not in list(self.keys()): |
|
282 | 296 | raise KeyError("No such diff key, available: %s" % self.diffs ) |
|
283 | 297 | return getattr(self.result_column, key) |
|
284 | 298 | |
|
285 | 299 | def __setitem__(self, key, value): |
|
286 | 300 | if key not in list(self.keys()): |
|
287 | 301 | raise KeyError("No such diff key, available: %s" % self.diffs ) |
|
288 | 302 | setattr(self.result_column, key, value) |
|
289 | 303 | |
|
290 | 304 | def __delitem__(self, key): |
|
291 | 305 | raise NotImplementedError |
|
292 | 306 | |
|
293 | 307 | def __len__(self): |
|
294 | 308 | raise NotImplementedError |
|
295 | 309 | |
|
296 | 310 | def __iter__(self): |
|
297 | 311 | raise NotImplementedError |
|
298 | 312 | |
|
299 | 313 | def keys(self): |
|
300 | 314 | return list(self.diffs.keys()) |
|
301 | 315 | |
|
302 | 316 | def compare_parameters(self, current_name, *p, **k): |
|
303 | 317 | """Compares Column objects with reflection""" |
|
304 | 318 | self.table = k.pop('table') |
|
305 | 319 | self.result_column = self._table.c.get(current_name) |
|
306 | 320 | if len(p): |
|
307 | 321 | k = self._extract_parameters(p, k, self.result_column) |
|
308 | 322 | return k |
|
309 | 323 | |
|
310 | 324 | def compare_1_column(self, col, *p, **k): |
|
311 | 325 | """Compares one Column object""" |
|
312 | 326 | self.table = k.pop('table', None) |
|
313 | 327 | if self.table is None: |
|
314 | 328 | self.table = col.table |
|
315 | 329 | self.result_column = col |
|
316 | 330 | if len(p): |
|
317 | 331 | k = self._extract_parameters(p, k, self.result_column) |
|
318 | 332 | return k |
|
319 | 333 | |
|
320 | 334 | def compare_2_columns(self, old_col, new_col, *p, **k): |
|
321 | 335 | """Compares two Column objects""" |
|
322 | 336 | self.process_column(new_col) |
|
323 | 337 | self.table = k.pop('table', None) |
|
324 | 338 | # we cannot use bool() on table in SA06 |
|
325 | 339 | if self.table is None: |
|
326 | 340 | self.table = old_col.table |
|
327 | 341 | if self.table is None: |
|
328 | 342 | new_col.table |
|
329 | 343 | self.result_column = old_col |
|
330 | 344 | |
|
331 | 345 | # set differences |
|
332 | 346 | # leave out some stuff for later comp |
|
333 | 347 | for key in (set(self.diff_keys) - set(('type',))): |
|
334 | 348 | val = getattr(new_col, key, None) |
|
335 | 349 | if getattr(self.result_column, key, None) != val: |
|
336 | 350 | k.setdefault(key, val) |
|
337 | 351 | |
|
338 | 352 | # inspect types |
|
339 | 353 | if not self.are_column_types_eq(self.result_column.type, new_col.type): |
|
340 | 354 | k.setdefault('type', new_col.type) |
|
341 | 355 | |
|
342 | 356 | if len(p): |
|
343 | 357 | k = self._extract_parameters(p, k, self.result_column) |
|
344 | 358 | return k |
|
345 | 359 | |
|
346 | 360 | def apply_diffs(self, diffs): |
|
347 | 361 | """Populate dict and column object with new values""" |
|
348 | 362 | self.diffs = diffs |
|
349 | 363 | for key in self.diff_keys: |
|
350 | 364 | if key in diffs: |
|
351 | 365 | setattr(self.result_column, key, diffs[key]) |
|
352 | 366 | |
|
353 | 367 | self.process_column(self.result_column) |
|
354 | 368 | |
|
355 | 369 | # create an instance of class type if not yet |
|
356 | 370 | if 'type' in diffs and callable(self.result_column.type): |
|
357 | 371 | self.result_column.type = self.result_column.type() |
|
358 | 372 | |
|
359 | 373 | # add column to the table |
|
360 | 374 | if self.table is not None and self.alter_metadata: |
|
361 | 375 | self.result_column.add_to_table(self.table) |
|
362 | 376 | |
|
363 | 377 | def are_column_types_eq(self, old_type, new_type): |
|
364 | 378 | """Compares two types to be equal""" |
|
365 | 379 | ret = old_type.__class__ == new_type.__class__ |
|
366 | 380 | |
|
367 | 381 | # String length is a special case |
|
368 | 382 | if ret and isinstance(new_type, sqlalchemy.types.String): |
|
369 | 383 | ret = (getattr(old_type, 'length', None) == \ |
|
370 | 384 | getattr(new_type, 'length', None)) |
|
371 | 385 | return ret |
|
372 | 386 | |
|
373 | 387 | def _extract_parameters(self, p, k, column): |
|
374 | 388 | """Extracts data from p and modifies diffs""" |
|
375 | 389 | p = list(p) |
|
376 | 390 | while len(p): |
|
377 | 391 | if isinstance(p[0], str): |
|
378 | 392 | k.setdefault('name', p.pop(0)) |
|
379 | 393 | elif isinstance(p[0], sqlalchemy.types.TypeEngine): |
|
380 | 394 | k.setdefault('type', p.pop(0)) |
|
381 | 395 | elif callable(p[0]): |
|
382 | 396 | p[0] = p[0]() |
|
383 | 397 | else: |
|
384 | 398 | break |
|
385 | 399 | |
|
386 | 400 | if len(p): |
|
387 | 401 | new_col = column.copy_fixed() |
|
388 | 402 | new_col._init_items(*p) |
|
389 | 403 | k = self.compare_2_columns(column, new_col, **k) |
|
390 | 404 | return k |
|
391 | 405 | |
|
392 | 406 | def process_column(self, column): |
|
393 | 407 | """Processes default values for column""" |
|
394 | 408 | # XXX: this is a snippet from SA processing of positional parameters |
|
395 | 409 | toinit = list() |
|
396 | 410 | |
|
397 | 411 | if column.server_default is not None: |
|
398 | 412 | if isinstance(column.server_default, sqlalchemy.FetchedValue): |
|
399 | 413 | toinit.append(column.server_default) |
|
400 | 414 | else: |
|
401 | 415 | toinit.append(sqlalchemy.DefaultClause(column.server_default)) |
|
402 | 416 | if column.server_onupdate is not None: |
|
403 | 417 | if isinstance(column.server_onupdate, FetchedValue): |
|
404 | 418 | toinit.append(column.server_default) |
|
405 | 419 | else: |
|
406 | 420 | toinit.append(sqlalchemy.DefaultClause(column.server_onupdate, |
|
407 | 421 | for_update=True)) |
|
408 | 422 | if toinit: |
|
409 | 423 | column._init_items(*toinit) |
|
410 | 424 | |
|
411 | 425 | def _get_table(self): |
|
412 | 426 | return getattr(self, '_table', None) |
|
413 | 427 | |
|
414 | 428 | def _set_table(self, table): |
|
415 | 429 | if isinstance(table, str): |
|
416 | 430 | if self.alter_metadata: |
|
417 | 431 | if not self.meta: |
|
418 | 432 | raise ValueError("metadata must be specified for table" |
|
419 | 433 | " reflection when using alter_metadata") |
|
420 | 434 | meta = self.meta |
|
421 | 435 | if self.engine: |
|
422 | 436 | meta.bind = self.engine |
|
423 | 437 | else: |
|
424 | 438 | if not self.engine and not self.meta: |
|
425 | 439 | raise ValueError("engine or metadata must be specified" |
|
426 | 440 | " to reflect tables") |
|
427 | 441 | if not self.engine: |
|
428 | 442 | self.engine = self.meta.bind |
|
429 | 443 | meta = sqlalchemy.MetaData(bind=self.engine) |
|
430 | 444 | self._table = sqlalchemy.Table(table, meta, autoload=True) |
|
431 | 445 | elif isinstance(table, sqlalchemy.Table): |
|
432 | 446 | self._table = table |
|
433 | 447 | if not self.alter_metadata: |
|
434 | 448 | self._table.meta = sqlalchemy.MetaData(bind=self._table.bind) |
|
435 | 449 | def _get_result_column(self): |
|
436 | 450 | return getattr(self, '_result_column', None) |
|
437 | 451 | |
|
438 | 452 | def _set_result_column(self, column): |
|
439 | 453 | """Set Column to Table based on alter_metadata evaluation.""" |
|
440 | 454 | self.process_column(column) |
|
441 | 455 | if not hasattr(self, 'current_name'): |
|
442 | 456 | self.current_name = column.name |
|
443 | 457 | if self.alter_metadata: |
|
444 | 458 | self._result_column = column |
|
445 | 459 | else: |
|
446 | 460 | self._result_column = column.copy_fixed() |
|
447 | 461 | |
|
448 | 462 | table = property(_get_table, _set_table) |
|
449 | 463 | result_column = property(_get_result_column, _set_result_column) |
|
450 | 464 | |
|
451 | 465 | |
|
452 | 466 | class ChangesetTable(object): |
|
453 | 467 | """Changeset extensions to SQLAlchemy tables.""" |
|
454 | 468 | |
|
455 | 469 | def create_column(self, column, *p, **kw): |
|
456 | 470 | """Creates a column. |
|
457 | 471 | |
|
458 | 472 | The column parameter may be a column definition or the name of |
|
459 | 473 | a column in this table. |
|
460 | 474 | |
|
461 | 475 | API to :meth:`ChangesetColumn.create` |
|
462 | 476 | |
|
463 | 477 | :param column: Column to be created |
|
464 | 478 | :type column: Column instance or string |
|
465 | 479 | """ |
|
466 | 480 | if not isinstance(column, sqlalchemy.Column): |
|
467 | 481 | # It's a column name |
|
468 | 482 | column = getattr(self.c, str(column)) |
|
469 | 483 | column.create(table=self, *p, **kw) |
|
470 | 484 | |
|
471 | 485 | def drop_column(self, column, *p, **kw): |
|
472 | 486 | """Drop a column, given its name or definition. |
|
473 | 487 | |
|
474 | 488 | API to :meth:`ChangesetColumn.drop` |
|
475 | 489 | |
|
476 | 490 | :param column: Column to be droped |
|
477 | 491 | :type column: Column instance or string |
|
478 | 492 | """ |
|
479 | 493 | if not isinstance(column, sqlalchemy.Column): |
|
480 | 494 | # It's a column name |
|
481 | 495 | try: |
|
482 | 496 | column = getattr(self.c, str(column)) |
|
483 | 497 | except AttributeError: |
|
484 | 498 | # That column isn't part of the table. We don't need |
|
485 | 499 | # its entire definition to drop the column, just its |
|
486 | 500 | # name, so create a dummy column with the same name. |
|
487 | 501 | column = sqlalchemy.Column(str(column), sqlalchemy.Integer()) |
|
488 | 502 | column.drop(table=self, *p, **kw) |
|
489 | 503 | |
|
490 | 504 | def rename(self, name, connection=None, **kwargs): |
|
491 | 505 | """Rename this table. |
|
492 | 506 | |
|
493 | 507 | :param name: New name of the table. |
|
494 | 508 | :type name: string |
|
495 | 509 | :param connection: reuse connection istead of creating new one. |
|
496 | 510 | :type connection: :class:`sqlalchemy.engine.base.Connection` instance |
|
497 | 511 | """ |
|
498 | 512 | engine = self.bind |
|
499 | 513 | self.new_name = name |
|
500 | 514 | visitorcallable = get_engine_visitor(engine, 'schemachanger') |
|
501 | 515 | run_single_visitor(engine, visitorcallable, self, connection, **kwargs) |
|
502 | 516 | |
|
503 | 517 | # Fix metadata registration |
|
504 | 518 | self.name = name |
|
505 | 519 | self.deregister() |
|
506 | 520 | self._set_parent(self.metadata) |
|
507 | 521 | |
|
508 | 522 | def _meta_key(self): |
|
509 | 523 | """Get the meta key for this table.""" |
|
510 | 524 | return sqlalchemy.schema._get_table_key(self.name, self.schema) |
|
511 | 525 | |
|
512 | 526 | def deregister(self): |
|
513 | 527 | """Remove this table from its metadata""" |
|
514 | 528 | if SQLA_07: |
|
515 | 529 | self.metadata._remove_table(self.name, self.schema) |
|
516 | 530 | else: |
|
517 | 531 | key = self._meta_key() |
|
518 | 532 | meta = self.metadata |
|
519 | 533 | if key in meta.tables: |
|
520 | 534 | del meta.tables[key] |
|
521 | 535 | |
|
522 | 536 | |
|
523 | 537 | class ChangesetColumn(object): |
|
524 | 538 | """Changeset extensions to SQLAlchemy columns.""" |
|
525 | 539 | |
|
526 | 540 | def alter(self, *p, **k): |
|
527 | 541 | """Makes a call to :func:`alter_column` for the column this |
|
528 | 542 | method is called on. |
|
529 | 543 | """ |
|
530 | 544 | if 'table' not in k: |
|
531 | 545 | k['table'] = self.table |
|
532 | 546 | if 'engine' not in k: |
|
533 | 547 | k['engine'] = k['table'].bind |
|
534 | 548 | return alter_column(self, *p, **k) |
|
535 | 549 | |
|
536 | 550 | def create(self, table=None, index_name=None, unique_name=None, |
|
537 | 551 | primary_key_name=None, populate_default=True, connection=None, **kwargs): |
|
538 | 552 | """Create this column in the database. |
|
539 | 553 | |
|
540 | 554 | Assumes the given table exists. ``ALTER TABLE ADD COLUMN``, |
|
541 | 555 | for most databases. |
|
542 | 556 | |
|
543 | 557 | :param table: Table instance to create on. |
|
544 | 558 | :param index_name: Creates :class:`ChangesetIndex` on this column. |
|
545 | 559 | :param unique_name: Creates :class:\ |
|
546 | 560 | `~migrate.changeset.constraint.UniqueConstraint` on this column. |
|
547 | 561 | :param primary_key_name: Creates :class:\ |
|
548 | 562 | `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column. |
|
549 | 563 | :param populate_default: If True, created column will be \ |
|
550 | 564 | populated with defaults |
|
551 | 565 | :param connection: reuse connection istead of creating new one. |
|
552 | 566 | :type table: Table instance |
|
553 | 567 | :type index_name: string |
|
554 | 568 | :type unique_name: string |
|
555 | 569 | :type primary_key_name: string |
|
556 | 570 | :type populate_default: bool |
|
557 | 571 | :type connection: :class:`sqlalchemy.engine.base.Connection` instance |
|
558 | 572 | |
|
559 | 573 | :returns: self |
|
560 | 574 | """ |
|
561 | 575 | self.populate_default = populate_default |
|
562 | 576 | self.index_name = index_name |
|
563 | 577 | self.unique_name = unique_name |
|
564 | 578 | self.primary_key_name = primary_key_name |
|
565 | 579 | for cons in ('index_name', 'unique_name', 'primary_key_name'): |
|
566 | 580 | self._check_sanity_constraints(cons) |
|
567 | 581 | |
|
568 | 582 | self.add_to_table(table) |
|
569 | 583 | engine = self.table.bind |
|
570 | 584 | visitorcallable = get_engine_visitor(engine, 'columngenerator') |
|
571 |
|
|
|
585 | _run_visitor(engine, visitorcallable, self, connection, **kwargs) | |
|
572 | 586 | |
|
573 | 587 | # TODO: reuse existing connection |
|
574 | 588 | if self.populate_default and self.default is not None: |
|
575 | 589 | stmt = table.update().values({self: engine._execute_default(self.default)}) |
|
576 | 590 | engine.execute(stmt) |
|
577 | 591 | |
|
578 | 592 | return self |
|
579 | 593 | |
|
580 | 594 | def drop(self, table=None, connection=None, **kwargs): |
|
581 | 595 | """Drop this column from the database, leaving its table intact. |
|
582 | 596 | |
|
583 | 597 | ``ALTER TABLE DROP COLUMN``, for most databases. |
|
584 | 598 | |
|
585 | 599 | :param connection: reuse connection istead of creating new one. |
|
586 | 600 | :type connection: :class:`sqlalchemy.engine.base.Connection` instance |
|
587 | 601 | """ |
|
588 | 602 | if table is not None: |
|
589 | 603 | self.table = table |
|
590 | 604 | engine = self.table.bind |
|
591 | 605 | visitorcallable = get_engine_visitor(engine, 'columndropper') |
|
592 |
|
|
|
606 | _run_visitor(engine, visitorcallable, self, connection, **kwargs) | |
|
593 | 607 | self.remove_from_table(self.table, unset_table=False) |
|
594 | 608 | self.table = None |
|
595 | 609 | return self |
|
596 | 610 | |
|
597 | 611 | def add_to_table(self, table): |
|
598 | 612 | if table is not None and self.table is None: |
|
599 | 613 | if SQLA_07: |
|
600 | 614 | table.append_column(self) |
|
601 | 615 | else: |
|
602 | 616 | self._set_parent(table) |
|
603 | 617 | |
|
604 | 618 | def _col_name_in_constraint(self,cons,name): |
|
605 | 619 | return False |
|
606 | 620 | |
|
607 | 621 | def remove_from_table(self, table, unset_table=True): |
|
608 | 622 | # TODO: remove primary keys, constraints, etc |
|
609 | 623 | if unset_table: |
|
610 | 624 | self.table = None |
|
611 | 625 | |
|
612 | 626 | to_drop = set() |
|
613 | 627 | for index in table.indexes: |
|
614 | 628 | columns = [] |
|
615 | 629 | for col in index.columns: |
|
616 | 630 | if col.name!=self.name: |
|
617 | 631 | columns.append(col) |
|
618 | 632 | if columns: |
|
619 | 633 | index.columns = columns |
|
620 | 634 | if SQLA_08: |
|
621 | 635 | index.expressions = columns |
|
622 | 636 | else: |
|
623 | 637 | to_drop.add(index) |
|
624 | 638 | table.indexes = table.indexes - to_drop |
|
625 | 639 | |
|
626 | 640 | to_drop = set() |
|
627 | 641 | for cons in table.constraints: |
|
628 | 642 | # TODO: deal with other types of constraint |
|
629 | 643 | if isinstance(cons,(ForeignKeyConstraint, |
|
630 | 644 | UniqueConstraint)): |
|
631 | 645 | for col_name in cons.columns: |
|
632 | 646 | if not isinstance(col_name, str): |
|
633 | 647 | col_name = col_name.name |
|
634 | 648 | if self.name==col_name: |
|
635 | 649 | to_drop.add(cons) |
|
636 | 650 | table.constraints = table.constraints - to_drop |
|
637 | 651 | |
|
638 | 652 | if table.c.contains_column(self): |
|
639 | 653 | if SQLA_07: |
|
640 | 654 | table._columns.remove(self) |
|
641 | 655 | else: |
|
642 | 656 | table.c.remove(self) |
|
643 | 657 | |
|
644 | 658 | # TODO: this is fixed in 0.6 |
|
645 | 659 | def copy_fixed(self, **kw): |
|
646 | 660 | """Create a copy of this ``Column``, with all attributes.""" |
|
647 | 661 | q = util.safe_quote(self) |
|
648 | 662 | return sqlalchemy.Column(self.name, self.type, self.default, |
|
649 | 663 | key=self.key, |
|
650 | 664 | primary_key=self.primary_key, |
|
651 | 665 | nullable=self.nullable, |
|
652 | 666 | quote=q, |
|
653 | 667 | index=self.index, |
|
654 | 668 | unique=self.unique, |
|
655 | 669 | onupdate=self.onupdate, |
|
656 | 670 | autoincrement=self.autoincrement, |
|
657 | 671 | server_default=self.server_default, |
|
658 | 672 | server_onupdate=self.server_onupdate, |
|
659 | 673 | *[c.copy(**kw) for c in self.constraints]) |
|
660 | 674 | |
|
661 | 675 | def _check_sanity_constraints(self, name): |
|
662 | 676 | """Check if constraints names are correct""" |
|
663 | 677 | obj = getattr(self, name) |
|
664 | 678 | if (getattr(self, name[:-5]) and not obj): |
|
665 | 679 | raise InvalidConstraintError("Column.create() accepts index_name," |
|
666 | 680 | " primary_key_name and unique_name to generate constraints") |
|
667 | 681 | if not isinstance(obj, str) and obj is not None: |
|
668 | 682 | raise InvalidConstraintError( |
|
669 | 683 | "%s argument for column must be constraint name" % name) |
|
670 | 684 | |
|
671 | 685 | |
|
672 | 686 | class ChangesetIndex(object): |
|
673 | 687 | """Changeset extensions to SQLAlchemy Indexes.""" |
|
674 | 688 | |
|
675 | 689 | __visit_name__ = 'index' |
|
676 | 690 | |
|
677 | 691 | def rename(self, name, connection=None, **kwargs): |
|
678 | 692 | """Change the name of an index. |
|
679 | 693 | |
|
680 | 694 | :param name: New name of the Index. |
|
681 | 695 | :type name: string |
|
682 | 696 | :param connection: reuse connection istead of creating new one. |
|
683 | 697 | :type connection: :class:`sqlalchemy.engine.base.Connection` instance |
|
684 | 698 | """ |
|
685 | 699 | engine = self.table.bind |
|
686 | 700 | self.new_name = name |
|
687 | 701 | visitorcallable = get_engine_visitor(engine, 'schemachanger') |
|
688 | 702 | engine._run_visitor(visitorcallable, self, connection, **kwargs) |
|
689 | 703 | self.name = name |
|
690 | 704 | |
|
691 | 705 | |
|
692 | 706 | class ChangesetDefaultClause(object): |
|
693 | 707 | """Implements comparison between :class:`DefaultClause` instances""" |
|
694 | 708 | |
|
695 | 709 | def __eq__(self, other): |
|
696 | 710 | if isinstance(other, self.__class__): |
|
697 | 711 | if self.arg == other.arg: |
|
698 | 712 | return True |
|
699 | 713 | |
|
700 | 714 | def __ne__(self, other): |
|
701 | 715 | return not self.__eq__(other) |
General Comments 0
You need to be logged in to leave comments.
Login now