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