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