1 | """ |
---|
2 | Schema module providing common schema operations. |
---|
3 | """ |
---|
4 | import re |
---|
5 | import sqlalchemy |
---|
6 | from migrate.changeset.databases.visitor import get_engine_visitor |
---|
7 | |
---|
8 | __all__ = [ |
---|
9 | 'create_column', |
---|
10 | 'drop_column', |
---|
11 | 'alter_column', |
---|
12 | 'rename_table', |
---|
13 | 'rename_index', |
---|
14 | ] |
---|
15 | |
---|
16 | |
---|
17 | def create_column(column, table=None, *p, **k): |
---|
18 | if table is not None: |
---|
19 | return table.create_column(column, *p, **k) |
---|
20 | return column.create(*p, **k) |
---|
21 | |
---|
22 | |
---|
23 | def drop_column(column, table=None, *p, **k): |
---|
24 | if table is not None: |
---|
25 | return table.drop_column(column, *p, **k) |
---|
26 | return column.drop(*p, **k) |
---|
27 | |
---|
28 | |
---|
29 | def _to_table(table, engine=None): |
---|
30 | if isinstance(table, sqlalchemy.Table): |
---|
31 | return table |
---|
32 | # Given: table name, maybe an engine |
---|
33 | meta = sqlalchemy.MetaData() |
---|
34 | if engine is not None: |
---|
35 | meta.bind = engine |
---|
36 | return sqlalchemy.Table(table, meta) |
---|
37 | |
---|
38 | |
---|
39 | def _to_index(index, table=None, engine=None): |
---|
40 | if isinstance(index, sqlalchemy.Index): |
---|
41 | return index |
---|
42 | # Given: index name; table name required |
---|
43 | table = _to_table(table, engine) |
---|
44 | ret = sqlalchemy.Index(index) |
---|
45 | ret.table = table |
---|
46 | return ret |
---|
47 | |
---|
48 | |
---|
49 | def rename_table(table, name, engine=None): |
---|
50 | """Rename a table, given the table's current name and the new |
---|
51 | name.""" |
---|
52 | table = _to_table(table, engine) |
---|
53 | table.rename(name) |
---|
54 | |
---|
55 | |
---|
56 | def rename_index(index, name, table=None, engine=None): |
---|
57 | """Rename an index. |
---|
58 | |
---|
59 | Takes an index name/object, a table name/object, and an |
---|
60 | engine. Engine and table aren't required if an index object is |
---|
61 | given. |
---|
62 | """ |
---|
63 | index = _to_index(index, table, engine) |
---|
64 | index.rename(name) |
---|
65 | |
---|
66 | |
---|
67 | def _engine_run_visitor(engine, visitorcallable, element, **kwargs): |
---|
68 | conn = engine.connect() |
---|
69 | try: |
---|
70 | element.accept_schema_visitor(visitorcallable(engine.dialect, |
---|
71 | connection=conn)) |
---|
72 | finally: |
---|
73 | conn.close() |
---|
74 | |
---|
75 | |
---|
76 | def alter_column(*p, **k): |
---|
77 | """Alter a column. |
---|
78 | |
---|
79 | Parameters: column name, table name, an engine, and the properties |
---|
80 | of that column to change |
---|
81 | """ |
---|
82 | if len(p) and isinstance(p[0], sqlalchemy.Column): |
---|
83 | col = p[0] |
---|
84 | else: |
---|
85 | col = None |
---|
86 | if 'table' not in k: |
---|
87 | k['table'] = col.table |
---|
88 | if 'engine' not in k: |
---|
89 | k['engine'] = k['table'].bind |
---|
90 | engine = k['engine'] |
---|
91 | delta = _ColumnDelta(*p, **k) |
---|
92 | visitorcallable = get_engine_visitor(engine, 'schemachanger') |
---|
93 | _engine_run_visitor(engine, visitorcallable, delta) |
---|
94 | |
---|
95 | # Update column |
---|
96 | if col is not None: |
---|
97 | # Special case: change column key on rename, if key not |
---|
98 | # explicit |
---|
99 | # |
---|
100 | # Used by SA : table.c.[key] |
---|
101 | # |
---|
102 | # This fails if the key was explit AND equal to the column |
---|
103 | # name. (It changes the key name when it shouldn't.) |
---|
104 | # |
---|
105 | # Not much we can do about it. |
---|
106 | if 'name' in delta.keys(): |
---|
107 | if (col.name == col.key): |
---|
108 | newname = delta['name'] |
---|
109 | del col.table.c[col.key] |
---|
110 | setattr(col, 'key', newname) |
---|
111 | col.table.c[col.key] = col |
---|
112 | # Change all other attrs |
---|
113 | for key, val in delta.iteritems(): |
---|
114 | setattr(col, key, val) |
---|
115 | |
---|
116 | |
---|
117 | def _normalize_table(column, table): |
---|
118 | if table is not None: |
---|
119 | if table is not column.table: |
---|
120 | # This is a bit of a hack: we end up with dupe PK columns here |
---|
121 | pk_names = map(lambda c: c.name, table.primary_key) |
---|
122 | if column.primary_key and pk_names.count(column.name): |
---|
123 | index = pk_names.index(column_name) |
---|
124 | del table.primary_key[index] |
---|
125 | table.append_column(column) |
---|
126 | return column.table |
---|
127 | |
---|
128 | |
---|
129 | class _WrapRename(object): |
---|
130 | |
---|
131 | def __init__(self, item, name): |
---|
132 | self.item = item |
---|
133 | self.name = name |
---|
134 | |
---|
135 | def accept_schema_visitor(self, visitor): |
---|
136 | if isinstance(self.item, sqlalchemy.Table): |
---|
137 | suffix = 'table' |
---|
138 | elif isinstance(self.item, sqlalchemy.Column): |
---|
139 | suffix = 'column' |
---|
140 | elif isinstance(self.item, sqlalchemy.Index): |
---|
141 | suffix = 'index' |
---|
142 | funcname = 'visit_%s' % suffix |
---|
143 | func = getattr(visitor, funcname) |
---|
144 | param = self.item, self.name |
---|
145 | return func(param) |
---|
146 | |
---|
147 | |
---|
148 | class _ColumnDelta(dict): |
---|
149 | """Extracts the differences between two columns/column-parameters""" |
---|
150 | |
---|
151 | def __init__(self, *p, **k): |
---|
152 | """Extract ALTER-able differences from two columns. |
---|
153 | |
---|
154 | May receive parameters arranged in several different ways: |
---|
155 | * old_column_object,new_column_object,*parameters Identifies |
---|
156 | attributes that differ between the two columns. |
---|
157 | Parameters specified outside of either column are always |
---|
158 | executed and override column differences. |
---|
159 | * column_object,[current_name,]*parameters Parameters |
---|
160 | specified are changed; table name is extracted from column |
---|
161 | object. Name is changed to column_object.name from |
---|
162 | current_name, if current_name is specified. If not |
---|
163 | specified, name is unchanged. |
---|
164 | * current_name,table,*parameters 'table' may be either an |
---|
165 | object or a name |
---|
166 | """ |
---|
167 | # Things are initialized differently depending on how many column |
---|
168 | # parameters are given. Figure out how many and call the appropriate |
---|
169 | # method. |
---|
170 | if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column): |
---|
171 | # At least one column specified |
---|
172 | if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column): |
---|
173 | # Two columns specified |
---|
174 | func = self._init_2col |
---|
175 | else: |
---|
176 | # Exactly one column specified |
---|
177 | func = self._init_1col |
---|
178 | else: |
---|
179 | # Zero columns specified |
---|
180 | func = self._init_0col |
---|
181 | diffs = func(*p, **k) |
---|
182 | self._set_diffs(diffs) |
---|
183 | # Column attributes that can be altered |
---|
184 | diff_keys = ('name', 'type', 'nullable', 'default', 'server_default', |
---|
185 | 'primary_key', 'foreign_key') |
---|
186 | |
---|
187 | def _get_table_name(self): |
---|
188 | if isinstance(self._table, basestring): |
---|
189 | ret = self._table |
---|
190 | else: |
---|
191 | ret = self._table.name |
---|
192 | return ret |
---|
193 | table_name = property(_get_table_name) |
---|
194 | |
---|
195 | def _get_table(self): |
---|
196 | if isinstance(self._table, basestring): |
---|
197 | ret = None |
---|
198 | else: |
---|
199 | ret = self._table |
---|
200 | return ret |
---|
201 | table = property(_get_table) |
---|
202 | |
---|
203 | def _init_0col(self, current_name, *p, **k): |
---|
204 | p, k = self._init_normalize_params(p, k) |
---|
205 | table = k.pop('table') |
---|
206 | self.current_name = current_name |
---|
207 | self._table = table |
---|
208 | return k |
---|
209 | |
---|
210 | def _init_1col(self, col, *p, **k): |
---|
211 | p, k = self._init_normalize_params(p, k) |
---|
212 | self._table = k.pop('table', None) or col.table |
---|
213 | self.result_column = col.copy() |
---|
214 | if 'current_name' in k: |
---|
215 | # Renamed |
---|
216 | self.current_name = k.pop('current_name') |
---|
217 | k.setdefault('name', col.name) |
---|
218 | else: |
---|
219 | self.current_name = col.name |
---|
220 | return k |
---|
221 | |
---|
222 | def _init_2col(self, start_col, end_col, *p, **k): |
---|
223 | p, k = self._init_normalize_params(p, k) |
---|
224 | self.result_column = start_col.copy() |
---|
225 | self._table = k.pop('table', None) or start_col.table \ |
---|
226 | or end_col.table |
---|
227 | self.current_name = start_col.name |
---|
228 | for key in ('name', 'nullable', 'default', 'server_default', |
---|
229 | 'primary_key', 'foreign_key'): |
---|
230 | val = getattr(end_col, key, None) |
---|
231 | if getattr(start_col, key, None) != val: |
---|
232 | k.setdefault(key, val) |
---|
233 | if not self.column_types_eq(start_col.type, end_col.type): |
---|
234 | k.setdefault('type', end_col.type) |
---|
235 | return k |
---|
236 | |
---|
237 | def _init_normalize_params(self, p, k): |
---|
238 | p = list(p) |
---|
239 | if len(p): |
---|
240 | k.setdefault('name', p.pop(0)) |
---|
241 | if len(p): |
---|
242 | k.setdefault('type', p.pop(0)) |
---|
243 | # TODO: sequences? FKs? |
---|
244 | return p, k |
---|
245 | |
---|
246 | def _set_diffs(self, diffs): |
---|
247 | for key in self.diff_keys: |
---|
248 | if key in diffs: |
---|
249 | self[key] = diffs[key] |
---|
250 | if getattr(self, 'result_column', None) is not None: |
---|
251 | setattr(self.result_column, key, diffs[key]) |
---|
252 | |
---|
253 | def column_types_eq(self, this, that): |
---|
254 | ret = isinstance(this, that.__class__) |
---|
255 | ret = ret or isinstance(that, this.__class__) |
---|
256 | # String length is a special case |
---|
257 | if ret and isinstance(that, sqlalchemy.types.String): |
---|
258 | ret = (getattr(this, 'length', None) == \ |
---|
259 | getattr(that, 'length', None)) |
---|
260 | return ret |
---|
261 | |
---|
262 | def accept_schema_visitor(self, visitor): |
---|
263 | return visitor.visit_column(self) |
---|
264 | |
---|
265 | |
---|
266 | class ChangesetTable(object): |
---|
267 | """Changeset extensions to SQLAlchemy tables.""" |
---|
268 | |
---|
269 | def create_column(self, column): |
---|
270 | """Creates a column. |
---|
271 | |
---|
272 | The column parameter may be a column definition or the name of |
---|
273 | a column in this table. |
---|
274 | """ |
---|
275 | if not isinstance(column, sqlalchemy.Column): |
---|
276 | # It's a column name |
---|
277 | column = getattr(self.c, str(column)) |
---|
278 | column.create(table=self) |
---|
279 | |
---|
280 | def drop_column(self, column): |
---|
281 | """Drop a column, given its name or definition.""" |
---|
282 | if not isinstance(column, sqlalchemy.Column): |
---|
283 | # It's a column name |
---|
284 | try: |
---|
285 | column = getattr(self.c, str(column), None) |
---|
286 | except AttributeError: |
---|
287 | # That column isn't part of the table. We don't need |
---|
288 | # its entire definition to drop the column, just its |
---|
289 | # name, so create a dummy column with the same name. |
---|
290 | column = sqlalchemy.Column(str(column)) |
---|
291 | column.drop(table=self) |
---|
292 | |
---|
293 | def _meta_key(self): |
---|
294 | return sqlalchemy.schema._get_table_key(self.name, self.schema) |
---|
295 | |
---|
296 | def deregister(self): |
---|
297 | """Remove this table from its metadata""" |
---|
298 | key = self._meta_key() |
---|
299 | meta = self.metadata |
---|
300 | if key in meta.tables: |
---|
301 | del meta.tables[key] |
---|
302 | |
---|
303 | def rename(self, name, *args, **kwargs): |
---|
304 | """Rename this table. |
---|
305 | |
---|
306 | This changes both the database name and the name of this |
---|
307 | Python object |
---|
308 | """ |
---|
309 | engine = self.bind |
---|
310 | visitorcallable = get_engine_visitor(engine, 'schemachanger') |
---|
311 | param = _WrapRename(self, name) |
---|
312 | _engine_run_visitor(engine, visitorcallable, param, *args, **kwargs) |
---|
313 | |
---|
314 | # Fix metadata registration |
---|
315 | meta = self.metadata |
---|
316 | self.deregister() |
---|
317 | self.name = name |
---|
318 | self._set_parent(meta) |
---|
319 | |
---|
320 | def _get_fullname(self): |
---|
321 | """Fullname should always be up to date""" |
---|
322 | # Copied from Table constructor |
---|
323 | if self.schema is not None: |
---|
324 | ret = "%s.%s"%(self.schema, self.name) |
---|
325 | else: |
---|
326 | ret = self.name |
---|
327 | return ret |
---|
328 | |
---|
329 | fullname = property(_get_fullname, (lambda self, val: None)) |
---|
330 | |
---|
331 | |
---|
332 | class ChangesetColumn(object): |
---|
333 | """Changeset extensions to SQLAlchemy columns""" |
---|
334 | |
---|
335 | def alter(self, *p, **k): |
---|
336 | """Alter a column's definition: ``ALTER TABLE ALTER COLUMN``. |
---|
337 | |
---|
338 | May supply a new column object, or a list of properties to |
---|
339 | change. |
---|
340 | |
---|
341 | For example; the following are equivalent: |
---|
342 | col.alter(Column('myint', Integer, nullable=False)) |
---|
343 | col.alter('myint', Integer, nullable=False) |
---|
344 | col.alter(name='myint', type=Integer, nullable=False) |
---|
345 | |
---|
346 | Column name, type, default, and nullable may be changed |
---|
347 | here. Note that for column defaults, only PassiveDefaults are |
---|
348 | managed by the database - changing others doesn't make sense. |
---|
349 | """ |
---|
350 | if 'table' not in k: |
---|
351 | k['table'] = self.table |
---|
352 | if 'engine' not in k: |
---|
353 | k['engine'] = k['table'].bind |
---|
354 | return alter_column(self, *p, **k) |
---|
355 | |
---|
356 | def create(self, table=None, *args, **kwargs): |
---|
357 | """Create this column in the database. |
---|
358 | |
---|
359 | Assumes the given table exists. ``ALTER TABLE ADD COLUMN``, |
---|
360 | for most databases. |
---|
361 | """ |
---|
362 | table = _normalize_table(self, table) |
---|
363 | engine = table.bind |
---|
364 | visitorcallable = get_engine_visitor(engine, 'columngenerator') |
---|
365 | engine._run_visitor(visitorcallable, self, *args, **kwargs) |
---|
366 | |
---|
367 | #add in foreign keys |
---|
368 | if self.foreign_keys: |
---|
369 | for fk in self.foreign_keys: |
---|
370 | visitorcallable = get_engine_visitor(engine, |
---|
371 | 'columnfkgenerator') |
---|
372 | engine._run_visitor(visitorcallable, self, fk=fk) |
---|
373 | return self |
---|
374 | |
---|
375 | def drop(self, table=None, *args, **kwargs): |
---|
376 | """Drop this column from the database, leaving its table intact. |
---|
377 | |
---|
378 | ``ALTER TABLE DROP COLUMN``, for most databases. |
---|
379 | """ |
---|
380 | table = _normalize_table(self, table) |
---|
381 | engine = table.bind |
---|
382 | visitorcallable = get_engine_visitor(engine, 'columndropper') |
---|
383 | engine._run_visitor(lambda dialect, conn: visitorcallable(conn), |
---|
384 | self, *args, **kwargs) |
---|
385 | return self |
---|
386 | |
---|
387 | |
---|
388 | class ChangesetIndex(object): |
---|
389 | """Changeset extensions to SQLAlchemy Indexes.""" |
---|
390 | |
---|
391 | def rename(self, name, *args, **kwargs): |
---|
392 | """Change the name of an index. |
---|
393 | |
---|
394 | This changes both the Python object name and the database |
---|
395 | name. |
---|
396 | """ |
---|
397 | engine = self.table.bind |
---|
398 | visitorcallable = get_engine_visitor(engine, 'schemachanger') |
---|
399 | param = _WrapRename(self, name) |
---|
400 | _engine_run_visitor(engine, visitorcallable, param, *args, **kwargs) |
---|
401 | self.name = name |
---|
402 | |
---|
403 | |
---|
404 | def _patch(): |
---|
405 | """All the 'ugly' operations that patch SQLAlchemy's internals.""" |
---|
406 | sqlalchemy.schema.Table.__bases__ += (ChangesetTable, ) |
---|
407 | sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, ) |
---|
408 | sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, ) |
---|
409 | _patch() |
---|