comparison rhodecode/lib/dbmigrate/migrate/changeset/ansisql.py @ 833:9753e0907827 beta

added dbmigrate package, added model changes moved out upgrade db command to that package
author Marcin Kuzminski <marcin@python-works.com>
date Sat, 11 Dec 2010 01:54:12 +0100
parents
children 08d2dcd71666
comparison
equal deleted inserted replaced
832:634596f81cfd 833:9753e0907827
1 """
2 Extensions to SQLAlchemy for altering existing tables.
3
4 At the moment, this isn't so much based off of ANSI as much as
5 things that just happen to work with multiple databases.
6 """
7 import StringIO
8
9 import sqlalchemy as sa
10 from sqlalchemy.schema import SchemaVisitor
11 from sqlalchemy.engine.default import DefaultDialect
12 from sqlalchemy.sql import ClauseElement
13 from sqlalchemy.schema import (ForeignKeyConstraint,
14 PrimaryKeyConstraint,
15 CheckConstraint,
16 UniqueConstraint,
17 Index)
18
19 from migrate import exceptions
20 from migrate.changeset import constraint, SQLA_06
21
22 if not SQLA_06:
23 from sqlalchemy.sql.compiler import SchemaGenerator, SchemaDropper
24 else:
25 from sqlalchemy.schema import AddConstraint, DropConstraint
26 from sqlalchemy.sql.compiler import DDLCompiler
27 SchemaGenerator = SchemaDropper = DDLCompiler
28
29
30 class AlterTableVisitor(SchemaVisitor):
31 """Common operations for ``ALTER TABLE`` statements."""
32
33 if SQLA_06:
34 # engine.Compiler looks for .statement
35 # when it spawns off a new compiler
36 statement = ClauseElement()
37
38 def append(self, s):
39 """Append content to the SchemaIterator's query buffer."""
40
41 self.buffer.write(s)
42
43 def execute(self):
44 """Execute the contents of the SchemaIterator's buffer."""
45 try:
46 return self.connection.execute(self.buffer.getvalue())
47 finally:
48 self.buffer.truncate(0)
49
50 def __init__(self, dialect, connection, **kw):
51 self.connection = connection
52 self.buffer = StringIO.StringIO()
53 self.preparer = dialect.identifier_preparer
54 self.dialect = dialect
55
56 def traverse_single(self, elem):
57 ret = super(AlterTableVisitor, self).traverse_single(elem)
58 if ret:
59 # adapt to 0.6 which uses a string-returning
60 # object
61 self.append(" %s" % ret)
62
63 def _to_table(self, param):
64 """Returns the table object for the given param object."""
65 if isinstance(param, (sa.Column, sa.Index, sa.schema.Constraint)):
66 ret = param.table
67 else:
68 ret = param
69 return ret
70
71 def start_alter_table(self, param):
72 """Returns the start of an ``ALTER TABLE`` SQL-Statement.
73
74 Use the param object to determine the table name and use it
75 for building the SQL statement.
76
77 :param param: object to determine the table from
78 :type param: :class:`sqlalchemy.Column`, :class:`sqlalchemy.Index`,
79 :class:`sqlalchemy.schema.Constraint`, :class:`sqlalchemy.Table`,
80 or string (table name)
81 """
82 table = self._to_table(param)
83 self.append('\nALTER TABLE %s ' % self.preparer.format_table(table))
84 return table
85
86
87 class ANSIColumnGenerator(AlterTableVisitor, SchemaGenerator):
88 """Extends ansisql generator for column creation (alter table add col)"""
89
90 def visit_column(self, column):
91 """Create a column (table already exists).
92
93 :param column: column object
94 :type column: :class:`sqlalchemy.Column` instance
95 """
96 if column.default is not None:
97 self.traverse_single(column.default)
98
99 table = self.start_alter_table(column)
100 self.append("ADD ")
101 self.append(self.get_column_specification(column))
102
103 for cons in column.constraints:
104 self.traverse_single(cons)
105 self.execute()
106
107 # ALTER TABLE STATEMENTS
108
109 # add indexes and unique constraints
110 if column.index_name:
111 Index(column.index_name,column).create()
112 elif column.unique_name:
113 constraint.UniqueConstraint(column,
114 name=column.unique_name).create()
115
116 # SA bounds FK constraints to table, add manually
117 for fk in column.foreign_keys:
118 self.add_foreignkey(fk.constraint)
119
120 # add primary key constraint if needed
121 if column.primary_key_name:
122 cons = constraint.PrimaryKeyConstraint(column,
123 name=column.primary_key_name)
124 cons.create()
125
126 if SQLA_06:
127 def add_foreignkey(self, fk):
128 self.connection.execute(AddConstraint(fk))
129
130 class ANSIColumnDropper(AlterTableVisitor, SchemaDropper):
131 """Extends ANSI SQL dropper for column dropping (``ALTER TABLE
132 DROP COLUMN``).
133 """
134
135 def visit_column(self, column):
136 """Drop a column from its table.
137
138 :param column: the column object
139 :type column: :class:`sqlalchemy.Column`
140 """
141 table = self.start_alter_table(column)
142 self.append('DROP COLUMN %s' % self.preparer.format_column(column))
143 self.execute()
144
145
146 class ANSISchemaChanger(AlterTableVisitor, SchemaGenerator):
147 """Manages changes to existing schema elements.
148
149 Note that columns are schema elements; ``ALTER TABLE ADD COLUMN``
150 is in SchemaGenerator.
151
152 All items may be renamed. Columns can also have many of their properties -
153 type, for example - changed.
154
155 Each function is passed a tuple, containing (object, name); where
156 object is a type of object you'd expect for that function
157 (ie. table for visit_table) and name is the object's new
158 name. NONE means the name is unchanged.
159 """
160
161 def visit_table(self, table):
162 """Rename a table. Other ops aren't supported."""
163 self.start_alter_table(table)
164 self.append("RENAME TO %s" % self.preparer.quote(table.new_name,
165 table.quote))
166 self.execute()
167
168 def visit_index(self, index):
169 """Rename an index"""
170 if hasattr(self, '_validate_identifier'):
171 # SA <= 0.6.3
172 self.append("ALTER INDEX %s RENAME TO %s" % (
173 self.preparer.quote(
174 self._validate_identifier(
175 index.name, True), index.quote),
176 self.preparer.quote(
177 self._validate_identifier(
178 index.new_name, True), index.quote)))
179 else:
180 # SA >= 0.6.5
181 self.append("ALTER INDEX %s RENAME TO %s" % (
182 self.preparer.quote(
183 self._index_identifier(
184 index.name), index.quote),
185 self.preparer.quote(
186 self._index_identifier(
187 index.new_name), index.quote)))
188 self.execute()
189
190 def visit_column(self, delta):
191 """Rename/change a column."""
192 # ALTER COLUMN is implemented as several ALTER statements
193 keys = delta.keys()
194 if 'type' in keys:
195 self._run_subvisit(delta, self._visit_column_type)
196 if 'nullable' in keys:
197 self._run_subvisit(delta, self._visit_column_nullable)
198 if 'server_default' in keys:
199 # Skip 'default': only handle server-side defaults, others
200 # are managed by the app, not the db.
201 self._run_subvisit(delta, self._visit_column_default)
202 if 'name' in keys:
203 self._run_subvisit(delta, self._visit_column_name, start_alter=False)
204
205 def _run_subvisit(self, delta, func, start_alter=True):
206 """Runs visit method based on what needs to be changed on column"""
207 table = self._to_table(delta.table)
208 col_name = delta.current_name
209 if start_alter:
210 self.start_alter_column(table, col_name)
211 ret = func(table, delta.result_column, delta)
212 self.execute()
213
214 def start_alter_column(self, table, col_name):
215 """Starts ALTER COLUMN"""
216 self.start_alter_table(table)
217 self.append("ALTER COLUMN %s " % self.preparer.quote(col_name, table.quote))
218
219 def _visit_column_nullable(self, table, column, delta):
220 nullable = delta['nullable']
221 if nullable:
222 self.append("DROP NOT NULL")
223 else:
224 self.append("SET NOT NULL")
225
226 def _visit_column_default(self, table, column, delta):
227 default_text = self.get_column_default_string(column)
228 if default_text is not None:
229 self.append("SET DEFAULT %s" % default_text)
230 else:
231 self.append("DROP DEFAULT")
232
233 def _visit_column_type(self, table, column, delta):
234 type_ = delta['type']
235 if SQLA_06:
236 type_text = str(type_.compile(dialect=self.dialect))
237 else:
238 type_text = type_.dialect_impl(self.dialect).get_col_spec()
239 self.append("TYPE %s" % type_text)
240
241 def _visit_column_name(self, table, column, delta):
242 self.start_alter_table(table)
243 col_name = self.preparer.quote(delta.current_name, table.quote)
244 new_name = self.preparer.format_column(delta.result_column)
245 self.append('RENAME COLUMN %s TO %s' % (col_name, new_name))
246
247
248 class ANSIConstraintCommon(AlterTableVisitor):
249 """
250 Migrate's constraints require a separate creation function from
251 SA's: Migrate's constraints are created independently of a table;
252 SA's are created at the same time as the table.
253 """
254
255 def get_constraint_name(self, cons):
256 """Gets a name for the given constraint.
257
258 If the name is already set it will be used otherwise the
259 constraint's :meth:`autoname <migrate.changeset.constraint.ConstraintChangeset.autoname>`
260 method is used.
261
262 :param cons: constraint object
263 """
264 if cons.name is not None:
265 ret = cons.name
266 else:
267 ret = cons.name = cons.autoname()
268 return self.preparer.quote(ret, cons.quote)
269
270 def visit_migrate_primary_key_constraint(self, *p, **k):
271 self._visit_constraint(*p, **k)
272
273 def visit_migrate_foreign_key_constraint(self, *p, **k):
274 self._visit_constraint(*p, **k)
275
276 def visit_migrate_check_constraint(self, *p, **k):
277 self._visit_constraint(*p, **k)
278
279 def visit_migrate_unique_constraint(self, *p, **k):
280 self._visit_constraint(*p, **k)
281
282 if SQLA_06:
283 class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
284 def _visit_constraint(self, constraint):
285 constraint.name = self.get_constraint_name(constraint)
286 self.append(self.process(AddConstraint(constraint)))
287 self.execute()
288
289 class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
290 def _visit_constraint(self, constraint):
291 constraint.name = self.get_constraint_name(constraint)
292 self.append(self.process(DropConstraint(constraint, cascade=constraint.cascade)))
293 self.execute()
294
295 else:
296 class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
297
298 def get_constraint_specification(self, cons, **kwargs):
299 """Constaint SQL generators.
300
301 We cannot use SA visitors because they append comma.
302 """
303
304 if isinstance(cons, PrimaryKeyConstraint):
305 if cons.name is not None:
306 self.append("CONSTRAINT %s " % self.preparer.format_constraint(cons))
307 self.append("PRIMARY KEY ")
308 self.append("(%s)" % ', '.join(self.preparer.quote(c.name, c.quote)
309 for c in cons))
310 self.define_constraint_deferrability(cons)
311 elif isinstance(cons, ForeignKeyConstraint):
312 self.define_foreign_key(cons)
313 elif isinstance(cons, CheckConstraint):
314 if cons.name is not None:
315 self.append("CONSTRAINT %s " %
316 self.preparer.format_constraint(cons))
317 self.append("CHECK (%s)" % cons.sqltext)
318 self.define_constraint_deferrability(cons)
319 elif isinstance(cons, UniqueConstraint):
320 if cons.name is not None:
321 self.append("CONSTRAINT %s " %
322 self.preparer.format_constraint(cons))
323 self.append("UNIQUE (%s)" % \
324 (', '.join(self.preparer.quote(c.name, c.quote) for c in cons)))
325 self.define_constraint_deferrability(cons)
326 else:
327 raise exceptions.InvalidConstraintError(cons)
328
329 def _visit_constraint(self, constraint):
330
331 table = self.start_alter_table(constraint)
332 constraint.name = self.get_constraint_name(constraint)
333 self.append("ADD ")
334 self.get_constraint_specification(constraint)
335 self.execute()
336
337
338 class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
339
340 def _visit_constraint(self, constraint):
341 self.start_alter_table(constraint)
342 self.append("DROP CONSTRAINT ")
343 constraint.name = self.get_constraint_name(constraint)
344 self.append(self.preparer.format_constraint(constraint))
345 if constraint.cascade:
346 self.cascade_constraint(constraint)
347 self.execute()
348
349 def cascade_constraint(self, constraint):
350 self.append(" CASCADE")
351
352
353 class ANSIDialect(DefaultDialect):
354 columngenerator = ANSIColumnGenerator
355 columndropper = ANSIColumnDropper
356 schemachanger = ANSISchemaChanger
357 constraintgenerator = ANSIConstraintGenerator
358 constraintdropper = ANSIConstraintDropper