##// END OF EJS Templates
added dbmigrate package, added model changes...
marcink -
r833:9753e090 beta
parent child Browse files
Show More
@@ -0,0 +1,59 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.dbmigrate.__init__
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Database migration modules
7
8 :created_on: Dec 11, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software; you can redistribute it and/or
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
27
28 from rhodecode.lib.utils import BasePasterCommand
29 from rhodecode.lib.utils import BasePasterCommand, Command, add_cache
30
31 from sqlalchemy import engine_from_config
32
33 class UpgradeDb(BasePasterCommand):
34 """Command used for paster to upgrade our database to newer version
35 """
36
37 max_args = 1
38 min_args = 1
39
40 usage = "CONFIG_FILE"
41 summary = "Upgrades current db to newer version given configuration file"
42 group_name = "RhodeCode"
43
44 parser = Command.standard_parser(verbose=True)
45
46 def command(self):
47 from pylons import config
48 add_cache(config)
49 engine = engine_from_config(config, 'sqlalchemy.db1.')
50 print engine
51 raise NotImplementedError('Not implemented yet')
52
53
54 def update_parser(self):
55 self.parser.add_option('--sql',
56 action='store_true',
57 dest='just_sql',
58 help="Prints upgrade sql for further investigation",
59 default=False)
@@ -0,0 +1,9 b''
1 """
2 SQLAlchemy migrate provides two APIs :mod:`migrate.versioning` for
3 database schema version and repository management and
4 :mod:`migrate.changeset` that allows to define database schema changes
5 using Python.
6 """
7
8 from migrate.versioning import *
9 from migrate.changeset import *
@@ -0,0 +1,28 b''
1 """
2 This module extends SQLAlchemy and provides additional DDL [#]_
3 support.
4
5 .. [#] SQL Data Definition Language
6 """
7 import re
8 import warnings
9
10 import sqlalchemy
11 from sqlalchemy import __version__ as _sa_version
12
13 warnings.simplefilter('always', DeprecationWarning)
14
15 _sa_version = tuple(int(re.match("\d+", x).group(0)) for x in _sa_version.split("."))
16 SQLA_06 = _sa_version >= (0, 6)
17
18 del re
19 del _sa_version
20
21 from migrate.changeset.schema import *
22 from migrate.changeset.constraint import *
23
24 sqlalchemy.schema.Table.__bases__ += (ChangesetTable, )
25 sqlalchemy.schema.Column.__bases__ += (ChangesetColumn, )
26 sqlalchemy.schema.Index.__bases__ += (ChangesetIndex, )
27
28 sqlalchemy.schema.DefaultClause.__bases__ += (ChangesetDefaultClause, )
@@ -0,0 +1,358 b''
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
@@ -0,0 +1,202 b''
1 """
2 This module defines standalone schema constraint classes.
3 """
4 from sqlalchemy import schema
5
6 from migrate.exceptions import *
7 from migrate.changeset import SQLA_06
8
9 class ConstraintChangeset(object):
10 """Base class for Constraint classes."""
11
12 def _normalize_columns(self, cols, table_name=False):
13 """Given: column objects or names; return col names and
14 (maybe) a table"""
15 colnames = []
16 table = None
17 for col in cols:
18 if isinstance(col, schema.Column):
19 if col.table is not None and table is None:
20 table = col.table
21 if table_name:
22 col = '.'.join((col.table.name, col.name))
23 else:
24 col = col.name
25 colnames.append(col)
26 return colnames, table
27
28 def __do_imports(self, visitor_name, *a, **kw):
29 engine = kw.pop('engine', self.table.bind)
30 from migrate.changeset.databases.visitor import (get_engine_visitor,
31 run_single_visitor)
32 visitorcallable = get_engine_visitor(engine, visitor_name)
33 run_single_visitor(engine, visitorcallable, self, *a, **kw)
34
35 def create(self, *a, **kw):
36 """Create the constraint in the database.
37
38 :param engine: the database engine to use. If this is \
39 :keyword:`None` the instance's engine will be used
40 :type engine: :class:`sqlalchemy.engine.base.Engine`
41 :param connection: reuse connection istead of creating new one.
42 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
43 """
44 # TODO: set the parent here instead of in __init__
45 self.__do_imports('constraintgenerator', *a, **kw)
46
47 def drop(self, *a, **kw):
48 """Drop the constraint from the database.
49
50 :param engine: the database engine to use. If this is
51 :keyword:`None` the instance's engine will be used
52 :param cascade: Issue CASCADE drop if database supports it
53 :type engine: :class:`sqlalchemy.engine.base.Engine`
54 :type cascade: bool
55 :param connection: reuse connection istead of creating new one.
56 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
57 :returns: Instance with cleared columns
58 """
59 self.cascade = kw.pop('cascade', False)
60 self.__do_imports('constraintdropper', *a, **kw)
61 # the spirit of Constraint objects is that they
62 # are immutable (just like in a DB. they're only ADDed
63 # or DROPped).
64 #self.columns.clear()
65 return self
66
67
68 class PrimaryKeyConstraint(ConstraintChangeset, schema.PrimaryKeyConstraint):
69 """Construct PrimaryKeyConstraint
70
71 Migrate's additional parameters:
72
73 :param cols: Columns in constraint.
74 :param table: If columns are passed as strings, this kw is required
75 :type table: Table instance
76 :type cols: strings or Column instances
77 """
78
79 __migrate_visit_name__ = 'migrate_primary_key_constraint'
80
81 def __init__(self, *cols, **kwargs):
82 colnames, table = self._normalize_columns(cols)
83 table = kwargs.pop('table', table)
84 super(PrimaryKeyConstraint, self).__init__(*colnames, **kwargs)
85 if table is not None:
86 self._set_parent(table)
87
88
89 def autoname(self):
90 """Mimic the database's automatic constraint names"""
91 return "%s_pkey" % self.table.name
92
93
94 class ForeignKeyConstraint(ConstraintChangeset, schema.ForeignKeyConstraint):
95 """Construct ForeignKeyConstraint
96
97 Migrate's additional parameters:
98
99 :param columns: Columns in constraint
100 :param refcolumns: Columns that this FK reffers to in another table.
101 :param table: If columns are passed as strings, this kw is required
102 :type table: Table instance
103 :type columns: list of strings or Column instances
104 :type refcolumns: list of strings or Column instances
105 """
106
107 __migrate_visit_name__ = 'migrate_foreign_key_constraint'
108
109 def __init__(self, columns, refcolumns, *args, **kwargs):
110 colnames, table = self._normalize_columns(columns)
111 table = kwargs.pop('table', table)
112 refcolnames, reftable = self._normalize_columns(refcolumns,
113 table_name=True)
114 super(ForeignKeyConstraint, self).__init__(colnames, refcolnames, *args,
115 **kwargs)
116 if table is not None:
117 self._set_parent(table)
118
119 @property
120 def referenced(self):
121 return [e.column for e in self.elements]
122
123 @property
124 def reftable(self):
125 return self.referenced[0].table
126
127 def autoname(self):
128 """Mimic the database's automatic constraint names"""
129 if hasattr(self.columns, 'keys'):
130 # SA <= 0.5
131 firstcol = self.columns[self.columns.keys()[0]]
132 ret = "%(table)s_%(firstcolumn)s_fkey" % dict(
133 table=firstcol.table.name,
134 firstcolumn=firstcol.name,)
135 else:
136 # SA >= 0.6
137 ret = "%(table)s_%(firstcolumn)s_fkey" % dict(
138 table=self.table.name,
139 firstcolumn=self.columns[0],)
140 return ret
141
142
143 class CheckConstraint(ConstraintChangeset, schema.CheckConstraint):
144 """Construct CheckConstraint
145
146 Migrate's additional parameters:
147
148 :param sqltext: Plain SQL text to check condition
149 :param columns: If not name is applied, you must supply this kw\
150 to autoname constraint
151 :param table: If columns are passed as strings, this kw is required
152 :type table: Table instance
153 :type columns: list of Columns instances
154 :type sqltext: string
155 """
156
157 __migrate_visit_name__ = 'migrate_check_constraint'
158
159 def __init__(self, sqltext, *args, **kwargs):
160 cols = kwargs.pop('columns', [])
161 if not cols and not kwargs.get('name', False):
162 raise InvalidConstraintError('You must either set "name"'
163 'parameter or "columns" to autogenarate it.')
164 colnames, table = self._normalize_columns(cols)
165 table = kwargs.pop('table', table)
166 schema.CheckConstraint.__init__(self, sqltext, *args, **kwargs)
167 if table is not None:
168 if not SQLA_06:
169 self.table = table
170 self._set_parent(table)
171 self.colnames = colnames
172
173 def autoname(self):
174 return "%(table)s_%(cols)s_check" % \
175 dict(table=self.table.name, cols="_".join(self.colnames))
176
177
178 class UniqueConstraint(ConstraintChangeset, schema.UniqueConstraint):
179 """Construct UniqueConstraint
180
181 Migrate's additional parameters:
182
183 :param cols: Columns in constraint.
184 :param table: If columns are passed as strings, this kw is required
185 :type table: Table instance
186 :type cols: strings or Column instances
187
188 .. versionadded:: 0.6.0
189 """
190
191 __migrate_visit_name__ = 'migrate_unique_constraint'
192
193 def __init__(self, *cols, **kwargs):
194 self.colnames, table = self._normalize_columns(cols)
195 table = kwargs.pop('table', table)
196 super(UniqueConstraint, self).__init__(*self.colnames, **kwargs)
197 if table is not None:
198 self._set_parent(table)
199
200 def autoname(self):
201 """Mimic the database's automatic constraint names"""
202 return "%s_%s_key" % (self.table.name, self.colnames[0])
@@ -0,0 +1,10 b''
1 """
2 This module contains database dialect specific changeset
3 implementations.
4 """
5 __all__ = [
6 'postgres',
7 'sqlite',
8 'mysql',
9 'oracle',
10 ]
@@ -0,0 +1,80 b''
1 """
2 Firebird database specific implementations of changeset classes.
3 """
4 from sqlalchemy.databases import firebird as sa_base
5
6 from migrate import exceptions
7 from migrate.changeset import ansisql, SQLA_06
8
9
10 if SQLA_06:
11 FBSchemaGenerator = sa_base.FBDDLCompiler
12 else:
13 FBSchemaGenerator = sa_base.FBSchemaGenerator
14
15 class FBColumnGenerator(FBSchemaGenerator, ansisql.ANSIColumnGenerator):
16 """Firebird column generator implementation."""
17
18
19 class FBColumnDropper(ansisql.ANSIColumnDropper):
20 """Firebird column dropper implementation."""
21
22 def visit_column(self, column):
23 """Firebird supports 'DROP col' instead of 'DROP COLUMN col' syntax
24
25 Drop primary key and unique constraints if dropped column is referencing it."""
26 if column.primary_key:
27 if column.table.primary_key.columns.contains_column(column):
28 column.table.primary_key.drop()
29 # TODO: recreate primary key if it references more than this column
30 if column.unique or getattr(column, 'unique_name', None):
31 for cons in column.table.constraints:
32 if cons.contains_column(column):
33 cons.drop()
34 # TODO: recreate unique constraint if it refenrences more than this column
35
36 table = self.start_alter_table(column)
37 self.append('DROP %s' % self.preparer.format_column(column))
38 self.execute()
39
40
41 class FBSchemaChanger(ansisql.ANSISchemaChanger):
42 """Firebird schema changer implementation."""
43
44 def visit_table(self, table):
45 """Rename table not supported"""
46 raise exceptions.NotSupportedError(
47 "Firebird does not support renaming tables.")
48
49 def _visit_column_name(self, table, column, delta):
50 self.start_alter_table(table)
51 col_name = self.preparer.quote(delta.current_name, table.quote)
52 new_name = self.preparer.format_column(delta.result_column)
53 self.append('ALTER COLUMN %s TO %s' % (col_name, new_name))
54
55 def _visit_column_nullable(self, table, column, delta):
56 """Changing NULL is not supported"""
57 # TODO: http://www.firebirdfaq.org/faq103/
58 raise exceptions.NotSupportedError(
59 "Firebird does not support altering NULL bevahior.")
60
61
62 class FBConstraintGenerator(ansisql.ANSIConstraintGenerator):
63 """Firebird constraint generator implementation."""
64
65
66 class FBConstraintDropper(ansisql.ANSIConstraintDropper):
67 """Firebird constaint dropper implementation."""
68
69 def cascade_constraint(self, constraint):
70 """Cascading constraints is not supported"""
71 raise exceptions.NotSupportedError(
72 "Firebird does not support cascading constraints")
73
74
75 class FBDialect(ansisql.ANSIDialect):
76 columngenerator = FBColumnGenerator
77 columndropper = FBColumnDropper
78 schemachanger = FBSchemaChanger
79 constraintgenerator = FBConstraintGenerator
80 constraintdropper = FBConstraintDropper
@@ -0,0 +1,94 b''
1 """
2 MySQL database specific implementations of changeset classes.
3 """
4
5 from sqlalchemy.databases import mysql as sa_base
6 from sqlalchemy import types as sqltypes
7
8 from migrate import exceptions
9 from migrate.changeset import ansisql, SQLA_06
10
11
12 if not SQLA_06:
13 MySQLSchemaGenerator = sa_base.MySQLSchemaGenerator
14 else:
15 MySQLSchemaGenerator = sa_base.MySQLDDLCompiler
16
17 class MySQLColumnGenerator(MySQLSchemaGenerator, ansisql.ANSIColumnGenerator):
18 pass
19
20
21 class MySQLColumnDropper(ansisql.ANSIColumnDropper):
22 pass
23
24
25 class MySQLSchemaChanger(MySQLSchemaGenerator, ansisql.ANSISchemaChanger):
26
27 def visit_column(self, delta):
28 table = delta.table
29 colspec = self.get_column_specification(delta.result_column)
30 if delta.result_column.autoincrement:
31 primary_keys = [c for c in table.primary_key.columns
32 if (c.autoincrement and
33 isinstance(c.type, sqltypes.Integer) and
34 not c.foreign_keys)]
35
36 if primary_keys:
37 first = primary_keys.pop(0)
38 if first.name == delta.current_name:
39 colspec += " AUTO_INCREMENT"
40 old_col_name = self.preparer.quote(delta.current_name, table.quote)
41
42 self.start_alter_table(table)
43
44 self.append("CHANGE COLUMN %s " % old_col_name)
45 self.append(colspec)
46 self.execute()
47
48 def visit_index(self, param):
49 # If MySQL can do this, I can't find how
50 raise exceptions.NotSupportedError("MySQL cannot rename indexes")
51
52
53 class MySQLConstraintGenerator(ansisql.ANSIConstraintGenerator):
54 pass
55
56 if SQLA_06:
57 class MySQLConstraintDropper(MySQLSchemaGenerator, ansisql.ANSIConstraintDropper):
58 def visit_migrate_check_constraint(self, *p, **k):
59 raise exceptions.NotSupportedError("MySQL does not support CHECK"
60 " constraints, use triggers instead.")
61
62 else:
63 class MySQLConstraintDropper(ansisql.ANSIConstraintDropper):
64
65 def visit_migrate_primary_key_constraint(self, constraint):
66 self.start_alter_table(constraint)
67 self.append("DROP PRIMARY KEY")
68 self.execute()
69
70 def visit_migrate_foreign_key_constraint(self, constraint):
71 self.start_alter_table(constraint)
72 self.append("DROP FOREIGN KEY ")
73 constraint.name = self.get_constraint_name(constraint)
74 self.append(self.preparer.format_constraint(constraint))
75 self.execute()
76
77 def visit_migrate_check_constraint(self, *p, **k):
78 raise exceptions.NotSupportedError("MySQL does not support CHECK"
79 " constraints, use triggers instead.")
80
81 def visit_migrate_unique_constraint(self, constraint, *p, **k):
82 self.start_alter_table(constraint)
83 self.append('DROP INDEX ')
84 constraint.name = self.get_constraint_name(constraint)
85 self.append(self.preparer.format_constraint(constraint))
86 self.execute()
87
88
89 class MySQLDialect(ansisql.ANSIDialect):
90 columngenerator = MySQLColumnGenerator
91 columndropper = MySQLColumnDropper
92 schemachanger = MySQLSchemaChanger
93 constraintgenerator = MySQLConstraintGenerator
94 constraintdropper = MySQLConstraintDropper
@@ -0,0 +1,111 b''
1 """
2 Oracle database specific implementations of changeset classes.
3 """
4 import sqlalchemy as sa
5 from sqlalchemy.databases import oracle as sa_base
6
7 from migrate import exceptions
8 from migrate.changeset import ansisql, SQLA_06
9
10
11 if not SQLA_06:
12 OracleSchemaGenerator = sa_base.OracleSchemaGenerator
13 else:
14 OracleSchemaGenerator = sa_base.OracleDDLCompiler
15
16
17 class OracleColumnGenerator(OracleSchemaGenerator, ansisql.ANSIColumnGenerator):
18 pass
19
20
21 class OracleColumnDropper(ansisql.ANSIColumnDropper):
22 pass
23
24
25 class OracleSchemaChanger(OracleSchemaGenerator, ansisql.ANSISchemaChanger):
26
27 def get_column_specification(self, column, **kwargs):
28 # Ignore the NOT NULL generated
29 override_nullable = kwargs.pop('override_nullable', None)
30 if override_nullable:
31 orig = column.nullable
32 column.nullable = True
33 ret = super(OracleSchemaChanger, self).get_column_specification(
34 column, **kwargs)
35 if override_nullable:
36 column.nullable = orig
37 return ret
38
39 def visit_column(self, delta):
40 keys = delta.keys()
41
42 if 'name' in keys:
43 self._run_subvisit(delta,
44 self._visit_column_name,
45 start_alter=False)
46
47 if len(set(('type', 'nullable', 'server_default')).intersection(keys)):
48 self._run_subvisit(delta,
49 self._visit_column_change,
50 start_alter=False)
51
52 def _visit_column_change(self, table, column, delta):
53 # Oracle cannot drop a default once created, but it can set it
54 # to null. We'll do that if default=None
55 # http://forums.oracle.com/forums/message.jspa?messageID=1273234#1273234
56 dropdefault_hack = (column.server_default is None \
57 and 'server_default' in delta.keys())
58 # Oracle apparently doesn't like it when we say "not null" if
59 # the column's already not null. Fudge it, so we don't need a
60 # new function
61 notnull_hack = ((not column.nullable) \
62 and ('nullable' not in delta.keys()))
63 # We need to specify NULL if we're removing a NOT NULL
64 # constraint
65 null_hack = (column.nullable and ('nullable' in delta.keys()))
66
67 if dropdefault_hack:
68 column.server_default = sa.PassiveDefault(sa.sql.null())
69 if notnull_hack:
70 column.nullable = True
71 colspec = self.get_column_specification(column,
72 override_nullable=null_hack)
73 if null_hack:
74 colspec += ' NULL'
75 if notnull_hack:
76 column.nullable = False
77 if dropdefault_hack:
78 column.server_default = None
79
80 self.start_alter_table(table)
81 self.append("MODIFY (")
82 self.append(colspec)
83 self.append(")")
84
85
86 class OracleConstraintCommon(object):
87
88 def get_constraint_name(self, cons):
89 # Oracle constraints can't guess their name like other DBs
90 if not cons.name:
91 raise exceptions.NotSupportedError(
92 "Oracle constraint names must be explicitly stated")
93 return cons.name
94
95
96 class OracleConstraintGenerator(OracleConstraintCommon,
97 ansisql.ANSIConstraintGenerator):
98 pass
99
100
101 class OracleConstraintDropper(OracleConstraintCommon,
102 ansisql.ANSIConstraintDropper):
103 pass
104
105
106 class OracleDialect(ansisql.ANSIDialect):
107 columngenerator = OracleColumnGenerator
108 columndropper = OracleColumnDropper
109 schemachanger = OracleSchemaChanger
110 constraintgenerator = OracleConstraintGenerator
111 constraintdropper = OracleConstraintDropper
@@ -0,0 +1,46 b''
1 """
2 `PostgreSQL`_ database specific implementations of changeset classes.
3
4 .. _`PostgreSQL`: http://www.postgresql.org/
5 """
6 from migrate.changeset import ansisql, SQLA_06
7
8 if not SQLA_06:
9 from sqlalchemy.databases import postgres as sa_base
10 PGSchemaGenerator = sa_base.PGSchemaGenerator
11 else:
12 from sqlalchemy.databases import postgresql as sa_base
13 PGSchemaGenerator = sa_base.PGDDLCompiler
14
15
16 class PGColumnGenerator(PGSchemaGenerator, ansisql.ANSIColumnGenerator):
17 """PostgreSQL column generator implementation."""
18 pass
19
20
21 class PGColumnDropper(ansisql.ANSIColumnDropper):
22 """PostgreSQL column dropper implementation."""
23 pass
24
25
26 class PGSchemaChanger(ansisql.ANSISchemaChanger):
27 """PostgreSQL schema changer implementation."""
28 pass
29
30
31 class PGConstraintGenerator(ansisql.ANSIConstraintGenerator):
32 """PostgreSQL constraint generator implementation."""
33 pass
34
35
36 class PGConstraintDropper(ansisql.ANSIConstraintDropper):
37 """PostgreSQL constaint dropper implementation."""
38 pass
39
40
41 class PGDialect(ansisql.ANSIDialect):
42 columngenerator = PGColumnGenerator
43 columndropper = PGColumnDropper
44 schemachanger = PGSchemaChanger
45 constraintgenerator = PGConstraintGenerator
46 constraintdropper = PGConstraintDropper
@@ -0,0 +1,148 b''
1 """
2 `SQLite`_ database specific implementations of changeset classes.
3
4 .. _`SQLite`: http://www.sqlite.org/
5 """
6 from UserDict import DictMixin
7 from copy import copy
8
9 from sqlalchemy.databases import sqlite as sa_base
10
11 from migrate import exceptions
12 from migrate.changeset import ansisql, SQLA_06
13
14
15 if not SQLA_06:
16 SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator
17 else:
18 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
19
20 class SQLiteCommon(object):
21
22 def _not_supported(self, op):
23 raise exceptions.NotSupportedError("SQLite does not support "
24 "%s; see http://www.sqlite.org/lang_altertable.html" % op)
25
26
27 class SQLiteHelper(SQLiteCommon):
28
29 def recreate_table(self,table,column=None,delta=None):
30 table_name = self.preparer.format_table(table)
31
32 # we remove all indexes so as not to have
33 # problems during copy and re-create
34 for index in table.indexes:
35 index.drop()
36
37 self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
38 self.execute()
39
40 insertion_string = self._modify_table(table, column, delta)
41
42 table.create()
43 self.append(insertion_string % {'table_name': table_name})
44 self.execute()
45 self.append('DROP TABLE migration_tmp')
46 self.execute()
47
48 def visit_column(self, delta):
49 if isinstance(delta, DictMixin):
50 column = delta.result_column
51 table = self._to_table(delta.table)
52 else:
53 column = delta
54 table = self._to_table(column.table)
55 self.recreate_table(table,column,delta)
56
57 class SQLiteColumnGenerator(SQLiteSchemaGenerator,
58 ansisql.ANSIColumnGenerator,
59 # at the end so we get the normal
60 # visit_column by default
61 SQLiteHelper,
62 SQLiteCommon
63 ):
64 """SQLite ColumnGenerator"""
65
66 def _modify_table(self, table, column, delta):
67 columns = ' ,'.join(map(
68 self.preparer.format_column,
69 [c for c in table.columns if c.name!=column.name]))
70 return ('INSERT INTO %%(table_name)s (%(cols)s) '
71 'SELECT %(cols)s from migration_tmp')%{'cols':columns}
72
73 def visit_column(self,column):
74 if column.foreign_keys:
75 SQLiteHelper.visit_column(self,column)
76 else:
77 super(SQLiteColumnGenerator,self).visit_column(column)
78
79 class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
80 """SQLite ColumnDropper"""
81
82 def _modify_table(self, table, column, delta):
83 columns = ' ,'.join(map(self.preparer.format_column, table.columns))
84 return 'INSERT INTO %(table_name)s SELECT ' + columns + \
85 ' from migration_tmp'
86
87
88 class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
89 """SQLite SchemaChanger"""
90
91 def _modify_table(self, table, column, delta):
92 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
93
94 def visit_index(self, index):
95 """Does not support ALTER INDEX"""
96 self._not_supported('ALTER INDEX')
97
98
99 class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon):
100
101 def visit_migrate_primary_key_constraint(self, constraint):
102 tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )"
103 cols = ', '.join(map(self.preparer.format_column, constraint.columns))
104 tname = self.preparer.format_table(constraint.table)
105 name = self.get_constraint_name(constraint)
106 msg = tmpl % (name, tname, cols)
107 self.append(msg)
108 self.execute()
109
110 def _modify_table(self, table, column, delta):
111 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
112
113 def visit_migrate_foreign_key_constraint(self, *p, **k):
114 self.recreate_table(p[0].table)
115
116 def visit_migrate_unique_constraint(self, *p, **k):
117 self.recreate_table(p[0].table)
118
119
120 class SQLiteConstraintDropper(ansisql.ANSIColumnDropper,
121 SQLiteCommon,
122 ansisql.ANSIConstraintCommon):
123
124 def visit_migrate_primary_key_constraint(self, constraint):
125 tmpl = "DROP INDEX %s "
126 name = self.get_constraint_name(constraint)
127 msg = tmpl % (name)
128 self.append(msg)
129 self.execute()
130
131 def visit_migrate_foreign_key_constraint(self, *p, **k):
132 self._not_supported('ALTER TABLE DROP CONSTRAINT')
133
134 def visit_migrate_check_constraint(self, *p, **k):
135 self._not_supported('ALTER TABLE DROP CONSTRAINT')
136
137 def visit_migrate_unique_constraint(self, *p, **k):
138 self._not_supported('ALTER TABLE DROP CONSTRAINT')
139
140
141 # TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index
142
143 class SQLiteDialect(ansisql.ANSIDialect):
144 columngenerator = SQLiteColumnGenerator
145 columndropper = SQLiteColumnDropper
146 schemachanger = SQLiteSchemaChanger
147 constraintgenerator = SQLiteConstraintGenerator
148 constraintdropper = SQLiteConstraintDropper
@@ -0,0 +1,78 b''
1 """
2 Module for visitor class mapping.
3 """
4 import sqlalchemy as sa
5
6 from migrate.changeset import ansisql
7 from migrate.changeset.databases import (sqlite,
8 postgres,
9 mysql,
10 oracle,
11 firebird)
12
13
14 # Map SA dialects to the corresponding Migrate extensions
15 DIALECTS = {
16 "default": ansisql.ANSIDialect,
17 "sqlite": sqlite.SQLiteDialect,
18 "postgres": postgres.PGDialect,
19 "postgresql": postgres.PGDialect,
20 "mysql": mysql.MySQLDialect,
21 "oracle": oracle.OracleDialect,
22 "firebird": firebird.FBDialect,
23 }
24
25
26 def get_engine_visitor(engine, name):
27 """
28 Get the visitor implementation for the given database engine.
29
30 :param engine: SQLAlchemy Engine
31 :param name: Name of the visitor
32 :type name: string
33 :type engine: Engine
34 :returns: visitor
35 """
36 # TODO: link to supported visitors
37 return get_dialect_visitor(engine.dialect, name)
38
39
40 def get_dialect_visitor(sa_dialect, name):
41 """
42 Get the visitor implementation for the given dialect.
43
44 Finds the visitor implementation based on the dialect class and
45 returns and instance initialized with the given name.
46
47 Binds dialect specific preparer to visitor.
48 """
49
50 # map sa dialect to migrate dialect and return visitor
51 sa_dialect_name = getattr(sa_dialect, 'name', 'default')
52 migrate_dialect_cls = DIALECTS[sa_dialect_name]
53 visitor = getattr(migrate_dialect_cls, name)
54
55 # bind preparer
56 visitor.preparer = sa_dialect.preparer(sa_dialect)
57
58 return visitor
59
60 def run_single_visitor(engine, visitorcallable, element,
61 connection=None, **kwargs):
62 """Taken from :meth:`sqlalchemy.engine.base.Engine._run_single_visitor`
63 with support for migrate visitors.
64 """
65 if connection is None:
66 conn = engine.contextual_connect(close_with_result=False)
67 else:
68 conn = connection
69 visitor = visitorcallable(engine.dialect, conn)
70 try:
71 if hasattr(element, '__migrate_visit_name__'):
72 fn = getattr(visitor, 'visit_' + element.__migrate_visit_name__)
73 else:
74 fn = getattr(visitor, 'visit_' + element.__visit_name__)
75 fn(element, **kwargs)
76 finally:
77 if connection is None:
78 conn.close()
This diff has been collapsed as it changes many lines, (669 lines changed) Show them Hide them
@@ -0,0 +1,669 b''
1 """
2 Schema module providing common schema operations.
3 """
4 import warnings
5
6 from UserDict import DictMixin
7
8 import sqlalchemy
9
10 from sqlalchemy.schema import ForeignKeyConstraint
11 from sqlalchemy.schema import UniqueConstraint
12
13 from migrate.exceptions import *
14 from migrate.changeset import SQLA_06
15 from migrate.changeset.databases.visitor import (get_engine_visitor,
16 run_single_visitor)
17
18
19 __all__ = [
20 'create_column',
21 'drop_column',
22 'alter_column',
23 'rename_table',
24 'rename_index',
25 'ChangesetTable',
26 'ChangesetColumn',
27 'ChangesetIndex',
28 'ChangesetDefaultClause',
29 'ColumnDelta',
30 ]
31
32 DEFAULT_ALTER_METADATA = True
33
34
35 def create_column(column, table=None, *p, **kw):
36 """Create a column, given the table.
37
38 API to :meth:`ChangesetColumn.create`.
39 """
40 if table is not None:
41 return table.create_column(column, *p, **kw)
42 return column.create(*p, **kw)
43
44
45 def drop_column(column, table=None, *p, **kw):
46 """Drop a column, given the table.
47
48 API to :meth:`ChangesetColumn.drop`.
49 """
50 if table is not None:
51 return table.drop_column(column, *p, **kw)
52 return column.drop(*p, **kw)
53
54
55 def rename_table(table, name, engine=None, **kw):
56 """Rename a table.
57
58 If Table instance is given, engine is not used.
59
60 API to :meth:`ChangesetTable.rename`.
61
62 :param table: Table to be renamed.
63 :param name: New name for Table.
64 :param engine: Engine instance.
65 :type table: string or Table instance
66 :type name: string
67 :type engine: obj
68 """
69 table = _to_table(table, engine)
70 table.rename(name, **kw)
71
72
73 def rename_index(index, name, table=None, engine=None, **kw):
74 """Rename an index.
75
76 If Index instance is given,
77 table and engine are not used.
78
79 API to :meth:`ChangesetIndex.rename`.
80
81 :param index: Index to be renamed.
82 :param name: New name for index.
83 :param table: Table to which Index is reffered.
84 :param engine: Engine instance.
85 :type index: string or Index instance
86 :type name: string
87 :type table: string or Table instance
88 :type engine: obj
89 """
90 index = _to_index(index, table, engine)
91 index.rename(name, **kw)
92
93
94 def alter_column(*p, **k):
95 """Alter a column.
96
97 This is a helper function that creates a :class:`ColumnDelta` and
98 runs it.
99
100 :argument column:
101 The name of the column to be altered or a
102 :class:`ChangesetColumn` column representing it.
103
104 :param table:
105 A :class:`~sqlalchemy.schema.Table` or table name to
106 for the table where the column will be changed.
107
108 :param engine:
109 The :class:`~sqlalchemy.engine.base.Engine` to use for table
110 reflection and schema alterations.
111
112 :param alter_metadata:
113 If `True`, which is the default, the
114 :class:`~sqlalchemy.schema.Column` will also modified.
115 If `False`, the :class:`~sqlalchemy.schema.Column` will be left
116 as it was.
117
118 :returns: A :class:`ColumnDelta` instance representing the change.
119
120
121 """
122
123 k.setdefault('alter_metadata', DEFAULT_ALTER_METADATA)
124
125 if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
126 k['table'] = p[0].table
127 if 'engine' not in k:
128 k['engine'] = k['table'].bind
129
130 # deprecation
131 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
132 warnings.warn(
133 "Passing a Column object to alter_column is deprecated."
134 " Just pass in keyword parameters instead.",
135 MigrateDeprecationWarning
136 )
137 engine = k['engine']
138 delta = ColumnDelta(*p, **k)
139
140 visitorcallable = get_engine_visitor(engine, 'schemachanger')
141 engine._run_visitor(visitorcallable, delta)
142
143 return delta
144
145
146 def _to_table(table, engine=None):
147 """Return if instance of Table, else construct new with metadata"""
148 if isinstance(table, sqlalchemy.Table):
149 return table
150
151 # Given: table name, maybe an engine
152 meta = sqlalchemy.MetaData()
153 if engine is not None:
154 meta.bind = engine
155 return sqlalchemy.Table(table, meta)
156
157
158 def _to_index(index, table=None, engine=None):
159 """Return if instance of Index, else construct new with metadata"""
160 if isinstance(index, sqlalchemy.Index):
161 return index
162
163 # Given: index name; table name required
164 table = _to_table(table, engine)
165 ret = sqlalchemy.Index(index)
166 ret.table = table
167 return ret
168
169
170 class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
171 """Extracts the differences between two columns/column-parameters
172
173 May receive parameters arranged in several different ways:
174
175 * **current_column, new_column, \*p, \*\*kw**
176 Additional parameters can be specified to override column
177 differences.
178
179 * **current_column, \*p, \*\*kw**
180 Additional parameters alter current_column. Table name is extracted
181 from current_column object.
182 Name is changed to current_column.name from current_name,
183 if current_name is specified.
184
185 * **current_col_name, \*p, \*\*kw**
186 Table kw must specified.
187
188 :param table: Table at which current Column should be bound to.\
189 If table name is given, reflection will be used.
190 :type table: string or Table instance
191 :param alter_metadata: If True, it will apply changes to metadata.
192 :type alter_metadata: bool
193 :param metadata: If `alter_metadata` is true, \
194 metadata is used to reflect table names into
195 :type metadata: :class:`MetaData` instance
196 :param engine: When reflecting tables, either engine or metadata must \
197 be specified to acquire engine object.
198 :type engine: :class:`Engine` instance
199 :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
200 `result_column` through :func:`dict` alike object.
201
202 * :class:`ColumnDelta`.result_column is altered column with new attributes
203
204 * :class:`ColumnDelta`.current_name is current name of column in db
205
206
207 """
208
209 # Column attributes that can be altered
210 diff_keys = ('name', 'type', 'primary_key', 'nullable',
211 'server_onupdate', 'server_default', 'autoincrement')
212 diffs = dict()
213 __visit_name__ = 'column'
214
215 def __init__(self, *p, **kw):
216 self.alter_metadata = kw.pop("alter_metadata", False)
217 self.meta = kw.pop("metadata", None)
218 self.engine = kw.pop("engine", None)
219
220 # Things are initialized differently depending on how many column
221 # parameters are given. Figure out how many and call the appropriate
222 # method.
223 if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
224 # At least one column specified
225 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
226 # Two columns specified
227 diffs = self.compare_2_columns(*p, **kw)
228 else:
229 # Exactly one column specified
230 diffs = self.compare_1_column(*p, **kw)
231 else:
232 # Zero columns specified
233 if not len(p) or not isinstance(p[0], basestring):
234 raise ValueError("First argument must be column name")
235 diffs = self.compare_parameters(*p, **kw)
236
237 self.apply_diffs(diffs)
238
239 def __repr__(self):
240 return '<ColumnDelta altermetadata=%r, %s>' % (self.alter_metadata,
241 super(ColumnDelta, self).__repr__())
242
243 def __getitem__(self, key):
244 if key not in self.keys():
245 raise KeyError("No such diff key, available: %s" % self.diffs)
246 return getattr(self.result_column, key)
247
248 def __setitem__(self, key, value):
249 if key not in self.keys():
250 raise KeyError("No such diff key, available: %s" % self.diffs)
251 setattr(self.result_column, key, value)
252
253 def __delitem__(self, key):
254 raise NotImplementedError
255
256 def keys(self):
257 return self.diffs.keys()
258
259 def compare_parameters(self, current_name, *p, **k):
260 """Compares Column objects with reflection"""
261 self.table = k.pop('table')
262 self.result_column = self._table.c.get(current_name)
263 if len(p):
264 k = self._extract_parameters(p, k, self.result_column)
265 return k
266
267 def compare_1_column(self, col, *p, **k):
268 """Compares one Column object"""
269 self.table = k.pop('table', None)
270 if self.table is None:
271 self.table = col.table
272 self.result_column = col
273 if len(p):
274 k = self._extract_parameters(p, k, self.result_column)
275 return k
276
277 def compare_2_columns(self, old_col, new_col, *p, **k):
278 """Compares two Column objects"""
279 self.process_column(new_col)
280 self.table = k.pop('table', None)
281 # we cannot use bool() on table in SA06
282 if self.table is None:
283 self.table = old_col.table
284 if self.table is None:
285 new_col.table
286 self.result_column = old_col
287
288 # set differences
289 # leave out some stuff for later comp
290 for key in (set(self.diff_keys) - set(('type',))):
291 val = getattr(new_col, key, None)
292 if getattr(self.result_column, key, None) != val:
293 k.setdefault(key, val)
294
295 # inspect types
296 if not self.are_column_types_eq(self.result_column.type, new_col.type):
297 k.setdefault('type', new_col.type)
298
299 if len(p):
300 k = self._extract_parameters(p, k, self.result_column)
301 return k
302
303 def apply_diffs(self, diffs):
304 """Populate dict and column object with new values"""
305 self.diffs = diffs
306 for key in self.diff_keys:
307 if key in diffs:
308 setattr(self.result_column, key, diffs[key])
309
310 self.process_column(self.result_column)
311
312 # create an instance of class type if not yet
313 if 'type' in diffs and callable(self.result_column.type):
314 self.result_column.type = self.result_column.type()
315
316 # add column to the table
317 if self.table is not None and self.alter_metadata:
318 self.result_column.add_to_table(self.table)
319
320 def are_column_types_eq(self, old_type, new_type):
321 """Compares two types to be equal"""
322 ret = old_type.__class__ == new_type.__class__
323
324 # String length is a special case
325 if ret and isinstance(new_type, sqlalchemy.types.String):
326 ret = (getattr(old_type, 'length', None) == \
327 getattr(new_type, 'length', None))
328 return ret
329
330 def _extract_parameters(self, p, k, column):
331 """Extracts data from p and modifies diffs"""
332 p = list(p)
333 while len(p):
334 if isinstance(p[0], basestring):
335 k.setdefault('name', p.pop(0))
336 elif isinstance(p[0], sqlalchemy.types.AbstractType):
337 k.setdefault('type', p.pop(0))
338 elif callable(p[0]):
339 p[0] = p[0]()
340 else:
341 break
342
343 if len(p):
344 new_col = column.copy_fixed()
345 new_col._init_items(*p)
346 k = self.compare_2_columns(column, new_col, **k)
347 return k
348
349 def process_column(self, column):
350 """Processes default values for column"""
351 # XXX: this is a snippet from SA processing of positional parameters
352 if not SQLA_06 and column.args:
353 toinit = list(column.args)
354 else:
355 toinit = list()
356
357 if column.server_default is not None:
358 if isinstance(column.server_default, sqlalchemy.FetchedValue):
359 toinit.append(column.server_default)
360 else:
361 toinit.append(sqlalchemy.DefaultClause(column.server_default))
362 if column.server_onupdate is not None:
363 if isinstance(column.server_onupdate, FetchedValue):
364 toinit.append(column.server_default)
365 else:
366 toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
367 for_update=True))
368 if toinit:
369 column._init_items(*toinit)
370
371 if not SQLA_06:
372 column.args = []
373
374 def _get_table(self):
375 return getattr(self, '_table', None)
376
377 def _set_table(self, table):
378 if isinstance(table, basestring):
379 if self.alter_metadata:
380 if not self.meta:
381 raise ValueError("metadata must be specified for table"
382 " reflection when using alter_metadata")
383 meta = self.meta
384 if self.engine:
385 meta.bind = self.engine
386 else:
387 if not self.engine and not self.meta:
388 raise ValueError("engine or metadata must be specified"
389 " to reflect tables")
390 if not self.engine:
391 self.engine = self.meta.bind
392 meta = sqlalchemy.MetaData(bind=self.engine)
393 self._table = sqlalchemy.Table(table, meta, autoload=True)
394 elif isinstance(table, sqlalchemy.Table):
395 self._table = table
396 if not self.alter_metadata:
397 self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
398
399 def _get_result_column(self):
400 return getattr(self, '_result_column', None)
401
402 def _set_result_column(self, column):
403 """Set Column to Table based on alter_metadata evaluation."""
404 self.process_column(column)
405 if not hasattr(self, 'current_name'):
406 self.current_name = column.name
407 if self.alter_metadata:
408 self._result_column = column
409 else:
410 self._result_column = column.copy_fixed()
411
412 table = property(_get_table, _set_table)
413 result_column = property(_get_result_column, _set_result_column)
414
415
416 class ChangesetTable(object):
417 """Changeset extensions to SQLAlchemy tables."""
418
419 def create_column(self, column, *p, **kw):
420 """Creates a column.
421
422 The column parameter may be a column definition or the name of
423 a column in this table.
424
425 API to :meth:`ChangesetColumn.create`
426
427 :param column: Column to be created
428 :type column: Column instance or string
429 """
430 if not isinstance(column, sqlalchemy.Column):
431 # It's a column name
432 column = getattr(self.c, str(column))
433 column.create(table=self, *p, **kw)
434
435 def drop_column(self, column, *p, **kw):
436 """Drop a column, given its name or definition.
437
438 API to :meth:`ChangesetColumn.drop`
439
440 :param column: Column to be droped
441 :type column: Column instance or string
442 """
443 if not isinstance(column, sqlalchemy.Column):
444 # It's a column name
445 try:
446 column = getattr(self.c, str(column))
447 except AttributeError:
448 # That column isn't part of the table. We don't need
449 # its entire definition to drop the column, just its
450 # name, so create a dummy column with the same name.
451 column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
452 column.drop(table=self, *p, **kw)
453
454 def rename(self, name, connection=None, **kwargs):
455 """Rename this table.
456
457 :param name: New name of the table.
458 :type name: string
459 :param alter_metadata: If True, table will be removed from metadata
460 :type alter_metadata: bool
461 :param connection: reuse connection istead of creating new one.
462 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
463 """
464 self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA)
465 engine = self.bind
466 self.new_name = name
467 visitorcallable = get_engine_visitor(engine, 'schemachanger')
468 run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
469
470 # Fix metadata registration
471 if self.alter_metadata:
472 self.name = name
473 self.deregister()
474 self._set_parent(self.metadata)
475
476 def _meta_key(self):
477 return sqlalchemy.schema._get_table_key(self.name, self.schema)
478
479 def deregister(self):
480 """Remove this table from its metadata"""
481 key = self._meta_key()
482 meta = self.metadata
483 if key in meta.tables:
484 del meta.tables[key]
485
486
487 class ChangesetColumn(object):
488 """Changeset extensions to SQLAlchemy columns."""
489
490 def alter(self, *p, **k):
491 """Makes a call to :func:`alter_column` for the column this
492 method is called on.
493 """
494 if 'table' not in k:
495 k['table'] = self.table
496 if 'engine' not in k:
497 k['engine'] = k['table'].bind
498 return alter_column(self, *p, **k)
499
500 def create(self, table=None, index_name=None, unique_name=None,
501 primary_key_name=None, populate_default=True, connection=None, **kwargs):
502 """Create this column in the database.
503
504 Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
505 for most databases.
506
507 :param table: Table instance to create on.
508 :param index_name: Creates :class:`ChangesetIndex` on this column.
509 :param unique_name: Creates :class:\
510 `~migrate.changeset.constraint.UniqueConstraint` on this column.
511 :param primary_key_name: Creates :class:\
512 `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column.
513 :param alter_metadata: If True, column will be added to table object.
514 :param populate_default: If True, created column will be \
515 populated with defaults
516 :param connection: reuse connection istead of creating new one.
517 :type table: Table instance
518 :type index_name: string
519 :type unique_name: string
520 :type primary_key_name: string
521 :type alter_metadata: bool
522 :type populate_default: bool
523 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
524
525 :returns: self
526 """
527 self.populate_default = populate_default
528 self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA)
529 self.index_name = index_name
530 self.unique_name = unique_name
531 self.primary_key_name = primary_key_name
532 for cons in ('index_name', 'unique_name', 'primary_key_name'):
533 self._check_sanity_constraints(cons)
534
535 if self.alter_metadata:
536 self.add_to_table(table)
537 engine = self.table.bind
538 visitorcallable = get_engine_visitor(engine, 'columngenerator')
539 engine._run_visitor(visitorcallable, self, connection, **kwargs)
540
541 # TODO: reuse existing connection
542 if self.populate_default and self.default is not None:
543 stmt = table.update().values({self: engine._execute_default(self.default)})
544 engine.execute(stmt)
545
546 return self
547
548 def drop(self, table=None, connection=None, **kwargs):
549 """Drop this column from the database, leaving its table intact.
550
551 ``ALTER TABLE DROP COLUMN``, for most databases.
552
553 :param alter_metadata: If True, column will be removed from table object.
554 :type alter_metadata: bool
555 :param connection: reuse connection istead of creating new one.
556 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
557 """
558 self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA)
559 if table is not None:
560 self.table = table
561 engine = self.table.bind
562 if self.alter_metadata:
563 self.remove_from_table(self.table, unset_table=False)
564 visitorcallable = get_engine_visitor(engine, 'columndropper')
565 engine._run_visitor(visitorcallable, self, connection, **kwargs)
566 if self.alter_metadata:
567 self.table = None
568 return self
569
570 def add_to_table(self, table):
571 if table is not None and self.table is None:
572 self._set_parent(table)
573
574 def _col_name_in_constraint(self, cons, name):
575 return False
576
577 def remove_from_table(self, table, unset_table=True):
578 # TODO: remove primary keys, constraints, etc
579 if unset_table:
580 self.table = None
581
582 to_drop = set()
583 for index in table.indexes:
584 columns = []
585 for col in index.columns:
586 if col.name != self.name:
587 columns.append(col)
588 if columns:
589 index.columns = columns
590 else:
591 to_drop.add(index)
592 table.indexes = table.indexes - to_drop
593
594 to_drop = set()
595 for cons in table.constraints:
596 # TODO: deal with other types of constraint
597 if isinstance(cons, (ForeignKeyConstraint,
598 UniqueConstraint)):
599 for col_name in cons.columns:
600 if not isinstance(col_name, basestring):
601 col_name = col_name.name
602 if self.name == col_name:
603 to_drop.add(cons)
604 table.constraints = table.constraints - to_drop
605
606 if table.c.contains_column(self):
607 table.c.remove(self)
608
609 # TODO: this is fixed in 0.6
610 def copy_fixed(self, **kw):
611 """Create a copy of this ``Column``, with all attributes."""
612 return sqlalchemy.Column(self.name, self.type, self.default,
613 key=self.key,
614 primary_key=self.primary_key,
615 nullable=self.nullable,
616 quote=self.quote,
617 index=self.index,
618 unique=self.unique,
619 onupdate=self.onupdate,
620 autoincrement=self.autoincrement,
621 server_default=self.server_default,
622 server_onupdate=self.server_onupdate,
623 *[c.copy(**kw) for c in self.constraints])
624
625 def _check_sanity_constraints(self, name):
626 """Check if constraints names are correct"""
627 obj = getattr(self, name)
628 if (getattr(self, name[:-5]) and not obj):
629 raise InvalidConstraintError("Column.create() accepts index_name,"
630 " primary_key_name and unique_name to generate constraints")
631 if not isinstance(obj, basestring) and obj is not None:
632 raise InvalidConstraintError(
633 "%s argument for column must be constraint name" % name)
634
635
636 class ChangesetIndex(object):
637 """Changeset extensions to SQLAlchemy Indexes."""
638
639 __visit_name__ = 'index'
640
641 def rename(self, name, connection=None, **kwargs):
642 """Change the name of an index.
643
644 :param name: New name of the Index.
645 :type name: string
646 :param alter_metadata: If True, Index object will be altered.
647 :type alter_metadata: bool
648 :param connection: reuse connection istead of creating new one.
649 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
650 """
651 self.alter_metadata = kwargs.pop('alter_metadata', DEFAULT_ALTER_METADATA)
652 engine = self.table.bind
653 self.new_name = name
654 visitorcallable = get_engine_visitor(engine, 'schemachanger')
655 engine._run_visitor(visitorcallable, self, connection, **kwargs)
656 if self.alter_metadata:
657 self.name = name
658
659
660 class ChangesetDefaultClause(object):
661 """Implements comparison between :class:`DefaultClause` instances"""
662
663 def __eq__(self, other):
664 if isinstance(other, self.__class__):
665 if self.arg == other.arg:
666 return True
667
668 def __ne__(self, other):
669 return not self.__eq__(other)
@@ -0,0 +1,87 b''
1 """
2 Provide exception classes for :mod:`migrate`
3 """
4
5
6 class Error(Exception):
7 """Error base class."""
8
9
10 class ApiError(Error):
11 """Base class for API errors."""
12
13
14 class KnownError(ApiError):
15 """A known error condition."""
16
17
18 class UsageError(ApiError):
19 """A known error condition where help should be displayed."""
20
21
22 class ControlledSchemaError(Error):
23 """Base class for controlled schema errors."""
24
25
26 class InvalidVersionError(ControlledSchemaError):
27 """Invalid version number."""
28
29
30 class DatabaseNotControlledError(ControlledSchemaError):
31 """Database should be under version control, but it's not."""
32
33
34 class DatabaseAlreadyControlledError(ControlledSchemaError):
35 """Database shouldn't be under version control, but it is"""
36
37
38 class WrongRepositoryError(ControlledSchemaError):
39 """This database is under version control by another repository."""
40
41
42 class NoSuchTableError(ControlledSchemaError):
43 """The table does not exist."""
44
45
46 class PathError(Error):
47 """Base class for path errors."""
48
49
50 class PathNotFoundError(PathError):
51 """A path with no file was required; found a file."""
52
53
54 class PathFoundError(PathError):
55 """A path with a file was required; found no file."""
56
57
58 class RepositoryError(Error):
59 """Base class for repository errors."""
60
61
62 class InvalidRepositoryError(RepositoryError):
63 """Invalid repository error."""
64
65
66 class ScriptError(Error):
67 """Base class for script errors."""
68
69
70 class InvalidScriptError(ScriptError):
71 """Invalid script error."""
72
73
74 class InvalidVersionError(Error):
75 """Invalid version error."""
76
77 # migrate.changeset
78
79 class NotSupportedError(Error):
80 """Not supported error"""
81
82
83 class InvalidConstraintError(Error):
84 """Invalid constraint error"""
85
86 class MigrateDeprecationWarning(DeprecationWarning):
87 """Warning for deprecated features in Migrate"""
@@ -0,0 +1,5 b''
1 """
2 This package provides functionality to create and manage
3 repositories of database schema changesets and to apply these
4 changesets to databases.
5 """
@@ -0,0 +1,383 b''
1 """
2 This module provides an external API to the versioning system.
3
4 .. versionchanged:: 0.6.0
5 :func:`migrate.versioning.api.test` and schema diff functions
6 changed order of positional arguments so all accept `url` and `repository`
7 as first arguments.
8
9 .. versionchanged:: 0.5.4
10 ``--preview_sql`` displays source file when using SQL scripts.
11 If Python script is used, it runs the action with mocked engine and
12 returns captured SQL statements.
13
14 .. versionchanged:: 0.5.4
15 Deprecated ``--echo`` parameter in favour of new
16 :func:`migrate.versioning.util.construct_engine` behavior.
17 """
18
19 # Dear migrate developers,
20 #
21 # please do not comment this module using sphinx syntax because its
22 # docstrings are presented as user help and most users cannot
23 # interpret sphinx annotated ReStructuredText.
24 #
25 # Thanks,
26 # Jan Dittberner
27
28 import sys
29 import inspect
30 import logging
31
32 from migrate import exceptions
33 from migrate.versioning import (repository, schema, version,
34 script as script_) # command name conflict
35 from migrate.versioning.util import catch_known_errors, with_engine
36
37
38 log = logging.getLogger(__name__)
39 command_desc = {
40 'help': 'displays help on a given command',
41 'create': 'create an empty repository at the specified path',
42 'script': 'create an empty change Python script',
43 'script_sql': 'create empty change SQL scripts for given database',
44 'version': 'display the latest version available in a repository',
45 'db_version': 'show the current version of the repository under version control',
46 'source': 'display the Python code for a particular version in this repository',
47 'version_control': 'mark a database as under this repository\'s version control',
48 'upgrade': 'upgrade a database to a later version',
49 'downgrade': 'downgrade a database to an earlier version',
50 'drop_version_control': 'removes version control from a database',
51 'manage': 'creates a Python script that runs Migrate with a set of default values',
52 'test': 'performs the upgrade and downgrade command on the given database',
53 'compare_model_to_db': 'compare MetaData against the current database state',
54 'create_model': 'dump the current database as a Python model to stdout',
55 'make_update_script_for_model': 'create a script changing the old MetaData to the new (current) MetaData',
56 'update_db_from_model': 'modify the database to match the structure of the current MetaData',
57 }
58 __all__ = command_desc.keys()
59
60 Repository = repository.Repository
61 ControlledSchema = schema.ControlledSchema
62 VerNum = version.VerNum
63 PythonScript = script_.PythonScript
64 SqlScript = script_.SqlScript
65
66
67 # deprecated
68 def help(cmd=None, **opts):
69 """%prog help COMMAND
70
71 Displays help on a given command.
72 """
73 if cmd is None:
74 raise exceptions.UsageError(None)
75 try:
76 func = globals()[cmd]
77 except:
78 raise exceptions.UsageError(
79 "'%s' isn't a valid command. Try 'help COMMAND'" % cmd)
80 ret = func.__doc__
81 if sys.argv[0]:
82 ret = ret.replace('%prog', sys.argv[0])
83 return ret
84
85 @catch_known_errors
86 def create(repository, name, **opts):
87 """%prog create REPOSITORY_PATH NAME [--table=TABLE]
88
89 Create an empty repository at the specified path.
90
91 You can specify the version_table to be used; by default, it is
92 'migrate_version'. This table is created in all version-controlled
93 databases.
94 """
95 repo_path = Repository.create(repository, name, **opts)
96
97
98 @catch_known_errors
99 def script(description, repository, **opts):
100 """%prog script DESCRIPTION REPOSITORY_PATH
101
102 Create an empty change script using the next unused version number
103 appended with the given description.
104
105 For instance, manage.py script "Add initial tables" creates:
106 repository/versions/001_Add_initial_tables.py
107 """
108 repo = Repository(repository)
109 repo.create_script(description, **opts)
110
111
112 @catch_known_errors
113 def script_sql(database, repository, **opts):
114 """%prog script_sql DATABASE REPOSITORY_PATH
115
116 Create empty change SQL scripts for given DATABASE, where DATABASE
117 is either specific ('postgres', 'mysql', 'oracle', 'sqlite', etc.)
118 or generic ('default').
119
120 For instance, manage.py script_sql postgres creates:
121 repository/versions/001_postgres_upgrade.sql and
122 repository/versions/001_postgres_postgres.sql
123 """
124 repo = Repository(repository)
125 repo.create_script_sql(database, **opts)
126
127
128 def version(repository, **opts):
129 """%prog version REPOSITORY_PATH
130
131 Display the latest version available in a repository.
132 """
133 repo = Repository(repository)
134 return repo.latest
135
136
137 @with_engine
138 def db_version(url, repository, **opts):
139 """%prog db_version URL REPOSITORY_PATH
140
141 Show the current version of the repository with the given
142 connection string, under version control of the specified
143 repository.
144
145 The url should be any valid SQLAlchemy connection string.
146 """
147 engine = opts.pop('engine')
148 schema = ControlledSchema(engine, repository)
149 return schema.version
150
151
152 def source(version, dest=None, repository=None, **opts):
153 """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH
154
155 Display the Python code for a particular version in this
156 repository. Save it to the file at DESTINATION or, if omitted,
157 send to stdout.
158 """
159 if repository is None:
160 raise exceptions.UsageError("A repository must be specified")
161 repo = Repository(repository)
162 ret = repo.version(version).script().source()
163 if dest is not None:
164 dest = open(dest, 'w')
165 dest.write(ret)
166 dest.close()
167 ret = None
168 return ret
169
170
171 def upgrade(url, repository, version=None, **opts):
172 """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql]
173
174 Upgrade a database to a later version.
175
176 This runs the upgrade() function defined in your change scripts.
177
178 By default, the database is updated to the latest available
179 version. You may specify a version instead, if you wish.
180
181 You may preview the Python or SQL code to be executed, rather than
182 actually executing it, using the appropriate 'preview' option.
183 """
184 err = "Cannot upgrade a database of version %s to version %s. "\
185 "Try 'downgrade' instead."
186 return _migrate(url, repository, version, upgrade=True, err=err, **opts)
187
188
189 def downgrade(url, repository, version, **opts):
190 """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql]
191
192 Downgrade a database to an earlier version.
193
194 This is the reverse of upgrade; this runs the downgrade() function
195 defined in your change scripts.
196
197 You may preview the Python or SQL code to be executed, rather than
198 actually executing it, using the appropriate 'preview' option.
199 """
200 err = "Cannot downgrade a database of version %s to version %s. "\
201 "Try 'upgrade' instead."
202 return _migrate(url, repository, version, upgrade=False, err=err, **opts)
203
204 @with_engine
205 def test(url, repository, **opts):
206 """%prog test URL REPOSITORY_PATH [VERSION]
207
208 Performs the upgrade and downgrade option on the given
209 database. This is not a real test and may leave the database in a
210 bad state. You should therefore better run the test on a copy of
211 your database.
212 """
213 engine = opts.pop('engine')
214 repos = Repository(repository)
215 script = repos.version(None).script()
216
217 # Upgrade
218 log.info("Upgrading...")
219 script.run(engine, 1)
220 log.info("done")
221
222 log.info("Downgrading...")
223 script.run(engine, -1)
224 log.info("done")
225 log.info("Success")
226
227
228 @with_engine
229 def version_control(url, repository, version=None, **opts):
230 """%prog version_control URL REPOSITORY_PATH [VERSION]
231
232 Mark a database as under this repository's version control.
233
234 Once a database is under version control, schema changes should
235 only be done via change scripts in this repository.
236
237 This creates the table version_table in the database.
238
239 The url should be any valid SQLAlchemy connection string.
240
241 By default, the database begins at version 0 and is assumed to be
242 empty. If the database is not empty, you may specify a version at
243 which to begin instead. No attempt is made to verify this
244 version's correctness - the database schema is expected to be
245 identical to what it would be if the database were created from
246 scratch.
247 """
248 engine = opts.pop('engine')
249 ControlledSchema.create(engine, repository, version)
250
251
252 @with_engine
253 def drop_version_control(url, repository, **opts):
254 """%prog drop_version_control URL REPOSITORY_PATH
255
256 Removes version control from a database.
257 """
258 engine = opts.pop('engine')
259 schema = ControlledSchema(engine, repository)
260 schema.drop()
261
262
263 def manage(file, **opts):
264 """%prog manage FILENAME [VARIABLES...]
265
266 Creates a script that runs Migrate with a set of default values.
267
268 For example::
269
270 %prog manage manage.py --repository=/path/to/repository \
271 --url=sqlite:///project.db
272
273 would create the script manage.py. The following two commands
274 would then have exactly the same results::
275
276 python manage.py version
277 %prog version --repository=/path/to/repository
278 """
279 Repository.create_manage_file(file, **opts)
280
281
282 @with_engine
283 def compare_model_to_db(url, repository, model, **opts):
284 """%prog compare_model_to_db URL REPOSITORY_PATH MODEL
285
286 Compare the current model (assumed to be a module level variable
287 of type sqlalchemy.MetaData) against the current database.
288
289 NOTE: This is EXPERIMENTAL.
290 """ # TODO: get rid of EXPERIMENTAL label
291 engine = opts.pop('engine')
292 return ControlledSchema.compare_model_to_db(engine, model, repository)
293
294
295 @with_engine
296 def create_model(url, repository, **opts):
297 """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True]
298
299 Dump the current database as a Python model to stdout.
300
301 NOTE: This is EXPERIMENTAL.
302 """ # TODO: get rid of EXPERIMENTAL label
303 engine = opts.pop('engine')
304 declarative = opts.get('declarative', False)
305 return ControlledSchema.create_model(engine, repository, declarative)
306
307
308 @catch_known_errors
309 @with_engine
310 def make_update_script_for_model(url, repository, oldmodel, model, **opts):
311 """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
312
313 Create a script changing the old Python model to the new (current)
314 Python model, sending to stdout.
315
316 NOTE: This is EXPERIMENTAL.
317 """ # TODO: get rid of EXPERIMENTAL label
318 engine = opts.pop('engine')
319 return PythonScript.make_update_script_for_model(
320 engine, oldmodel, model, repository, **opts)
321
322
323 @with_engine
324 def update_db_from_model(url, repository, model, **opts):
325 """%prog update_db_from_model URL REPOSITORY_PATH MODEL
326
327 Modify the database to match the structure of the current Python
328 model. This also sets the db_version number to the latest in the
329 repository.
330
331 NOTE: This is EXPERIMENTAL.
332 """ # TODO: get rid of EXPERIMENTAL label
333 engine = opts.pop('engine')
334 schema = ControlledSchema(engine, repository)
335 schema.update_db_from_model(model)
336
337 @with_engine
338 def _migrate(url, repository, version, upgrade, err, **opts):
339 engine = opts.pop('engine')
340 url = str(engine.url)
341 schema = ControlledSchema(engine, repository)
342 version = _migrate_version(schema, version, upgrade, err)
343
344 changeset = schema.changeset(version)
345 for ver, change in changeset:
346 nextver = ver + changeset.step
347 log.info('%s -> %s... ', ver, nextver)
348
349 if opts.get('preview_sql'):
350 if isinstance(change, PythonScript):
351 log.info(change.preview_sql(url, changeset.step, **opts))
352 elif isinstance(change, SqlScript):
353 log.info(change.source())
354
355 elif opts.get('preview_py'):
356 if not isinstance(change, PythonScript):
357 raise exceptions.UsageError("Python source can be only displayed"
358 " for python migration files")
359 source_ver = max(ver, nextver)
360 module = schema.repository.version(source_ver).script().module
361 funcname = upgrade and "upgrade" or "downgrade"
362 func = getattr(module, funcname)
363 log.info(inspect.getsource(func))
364 else:
365 schema.runchange(ver, change, changeset.step)
366 log.info('done')
367
368
369 def _migrate_version(schema, version, upgrade, err):
370 if version is None:
371 return version
372 # Version is specified: ensure we're upgrading in the right direction
373 # (current version < target version for upgrading; reverse for down)
374 version = VerNum(version)
375 cur = schema.version
376 if upgrade is not None:
377 if upgrade:
378 direction = cur <= version
379 else:
380 direction = cur >= version
381 if not direction:
382 raise exceptions.KnownError(err % (cur, version))
383 return version
@@ -0,0 +1,27 b''
1 """
2 Configuration parser module.
3 """
4
5 from ConfigParser import ConfigParser
6
7 from migrate.versioning.config import *
8 from migrate.versioning import pathed
9
10
11 class Parser(ConfigParser):
12 """A project configuration file."""
13
14 def to_dict(self, sections=None):
15 """It's easier to access config values like dictionaries"""
16 return self._sections
17
18
19 class Config(pathed.Pathed, Parser):
20 """Configuration class."""
21
22 def __init__(self, path, *p, **k):
23 """Confirm the config file exists; read it."""
24 self.require_found(path)
25 pathed.Pathed.__init__(self, path)
26 Parser.__init__(self, *p, **k)
27 self.read(path)
@@ -0,0 +1,14 b''
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 from sqlalchemy.util import OrderedDict
5
6
7 __all__ = ['databases', 'operations']
8
9 databases = ('sqlite', 'postgres', 'mysql', 'oracle', 'mssql', 'firebird')
10
11 # Map operation names to function names
12 operations = OrderedDict()
13 operations['upgrade'] = 'upgrade'
14 operations['downgrade'] = 'downgrade'
@@ -0,0 +1,254 b''
1 """
2 Code to generate a Python model from a database or differences
3 between a model and database.
4
5 Some of this is borrowed heavily from the AutoCode project at:
6 http://code.google.com/p/sqlautocode/
7 """
8
9 import sys
10 import logging
11
12 import sqlalchemy
13
14 import migrate
15 import migrate.changeset
16
17
18 log = logging.getLogger(__name__)
19 HEADER = """
20 ## File autogenerated by genmodel.py
21
22 from sqlalchemy import *
23 meta = MetaData()
24 """
25
26 DECLARATIVE_HEADER = """
27 ## File autogenerated by genmodel.py
28
29 from sqlalchemy import *
30 from sqlalchemy.ext import declarative
31
32 Base = declarative.declarative_base()
33 """
34
35
36 class ModelGenerator(object):
37
38 def __init__(self, diff, engine, declarative=False):
39 self.diff = diff
40 self.engine = engine
41 self.declarative = declarative
42
43 def column_repr(self, col):
44 kwarg = []
45 if col.key != col.name:
46 kwarg.append('key')
47 if col.primary_key:
48 col.primary_key = True # otherwise it dumps it as 1
49 kwarg.append('primary_key')
50 if not col.nullable:
51 kwarg.append('nullable')
52 if col.onupdate:
53 kwarg.append('onupdate')
54 if col.default:
55 if col.primary_key:
56 # I found that PostgreSQL automatically creates a
57 # default value for the sequence, but let's not show
58 # that.
59 pass
60 else:
61 kwarg.append('default')
62 ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg)
63
64 # crs: not sure if this is good idea, but it gets rid of extra
65 # u''
66 name = col.name.encode('utf8')
67
68 type_ = col.type
69 for cls in col.type.__class__.__mro__:
70 if cls.__module__ == 'sqlalchemy.types' and \
71 not cls.__name__.isupper():
72 if cls is not type_.__class__:
73 type_ = cls()
74 break
75
76 data = {
77 'name': name,
78 'type': type_,
79 'constraints': ', '.join([repr(cn) for cn in col.constraints]),
80 'args': ks and ks or ''}
81
82 if data['constraints']:
83 if data['args']:
84 data['args'] = ',' + data['args']
85
86 if data['constraints'] or data['args']:
87 data['maybeComma'] = ','
88 else:
89 data['maybeComma'] = ''
90
91 commonStuff = """ %(maybeComma)s %(constraints)s %(args)s)""" % data
92 commonStuff = commonStuff.strip()
93 data['commonStuff'] = commonStuff
94 if self.declarative:
95 return """%(name)s = Column(%(type)r%(commonStuff)s""" % data
96 else:
97 return """Column(%(name)r, %(type)r%(commonStuff)s""" % data
98
99 def getTableDefn(self, table):
100 out = []
101 tableName = table.name
102 if self.declarative:
103 out.append("class %(table)s(Base):" % {'table': tableName})
104 out.append(" __tablename__ = '%(table)s'" % {'table': tableName})
105 for col in table.columns:
106 out.append(" %s" % self.column_repr(col))
107 else:
108 out.append("%(table)s = Table('%(table)s', meta," % \
109 {'table': tableName})
110 for col in table.columns:
111 out.append(" %s," % self.column_repr(col))
112 out.append(")")
113 return out
114
115 def _get_tables(self,missingA=False,missingB=False,modified=False):
116 to_process = []
117 for bool_,names,metadata in (
118 (missingA,self.diff.tables_missing_from_A,self.diff.metadataB),
119 (missingB,self.diff.tables_missing_from_B,self.diff.metadataA),
120 (modified,self.diff.tables_different,self.diff.metadataA),
121 ):
122 if bool_:
123 for name in names:
124 yield metadata.tables.get(name)
125
126 def toPython(self):
127 """Assume database is current and model is empty."""
128 out = []
129 if self.declarative:
130 out.append(DECLARATIVE_HEADER)
131 else:
132 out.append(HEADER)
133 out.append("")
134 for table in self._get_tables(missingA=True):
135 out.extend(self.getTableDefn(table))
136 out.append("")
137 return '\n'.join(out)
138
139 def toUpgradeDowngradePython(self, indent=' '):
140 ''' Assume model is most current and database is out-of-date. '''
141 decls = ['from migrate.changeset import schema',
142 'meta = MetaData()']
143 for table in self._get_tables(
144 missingA=True,missingB=True,modified=True
145 ):
146 decls.extend(self.getTableDefn(table))
147
148 upgradeCommands, downgradeCommands = [], []
149 for tableName in self.diff.tables_missing_from_A:
150 upgradeCommands.append("%(table)s.drop()" % {'table': tableName})
151 downgradeCommands.append("%(table)s.create()" % \
152 {'table': tableName})
153 for tableName in self.diff.tables_missing_from_B:
154 upgradeCommands.append("%(table)s.create()" % {'table': tableName})
155 downgradeCommands.append("%(table)s.drop()" % {'table': tableName})
156
157 for tableName in self.diff.tables_different:
158 dbTable = self.diff.metadataB.tables[tableName]
159 missingInDatabase, missingInModel, diffDecl = \
160 self.diff.colDiffs[tableName]
161 for col in missingInDatabase:
162 upgradeCommands.append('%s.columns[%r].create()' % (
163 modelTable, col.name))
164 downgradeCommands.append('%s.columns[%r].drop()' % (
165 modelTable, col.name))
166 for col in missingInModel:
167 upgradeCommands.append('%s.columns[%r].drop()' % (
168 modelTable, col.name))
169 downgradeCommands.append('%s.columns[%r].create()' % (
170 modelTable, col.name))
171 for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl:
172 upgradeCommands.append(
173 'assert False, "Can\'t alter columns: %s:%s=>%s"',
174 modelTable, modelCol.name, databaseCol.name)
175 downgradeCommands.append(
176 'assert False, "Can\'t alter columns: %s:%s=>%s"',
177 modelTable, modelCol.name, databaseCol.name)
178 pre_command = ' meta.bind = migrate_engine'
179
180 return (
181 '\n'.join(decls),
182 '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in upgradeCommands]),
183 '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in downgradeCommands]))
184
185 def _db_can_handle_this_change(self,td):
186 if (td.columns_missing_from_B
187 and not td.columns_missing_from_A
188 and not td.columns_different):
189 # Even sqlite can handle this.
190 return True
191 else:
192 return not self.engine.url.drivername.startswith('sqlite')
193
194 def applyModel(self):
195 """Apply model to current database."""
196
197 meta = sqlalchemy.MetaData(self.engine)
198
199 for table in self._get_tables(missingA=True):
200 table = table.tometadata(meta)
201 table.drop()
202 for table in self._get_tables(missingB=True):
203 table = table.tometadata(meta)
204 table.create()
205 for modelTable in self._get_tables(modified=True):
206 tableName = modelTable.name
207 modelTable = modelTable.tometadata(meta)
208 dbTable = self.diff.metadataB.tables[tableName]
209
210 td = self.diff.tables_different[tableName]
211
212 if self._db_can_handle_this_change(td):
213
214 for col in td.columns_missing_from_B:
215 modelTable.columns[col].create()
216 for col in td.columns_missing_from_A:
217 dbTable.columns[col].drop()
218 # XXX handle column changes here.
219 else:
220 # Sqlite doesn't support drop column, so you have to
221 # do more: create temp table, copy data to it, drop
222 # old table, create new table, copy data back.
223 #
224 # I wonder if this is guaranteed to be unique?
225 tempName = '_temp_%s' % modelTable.name
226
227 def getCopyStatement():
228 preparer = self.engine.dialect.preparer
229 commonCols = []
230 for modelCol in modelTable.columns:
231 if modelCol.name in dbTable.columns:
232 commonCols.append(modelCol.name)
233 commonColsStr = ', '.join(commonCols)
234 return 'INSERT INTO %s (%s) SELECT %s FROM %s' % \
235 (tableName, commonColsStr, commonColsStr, tempName)
236
237 # Move the data in one transaction, so that we don't
238 # leave the database in a nasty state.
239 connection = self.engine.connect()
240 trans = connection.begin()
241 try:
242 connection.execute(
243 'CREATE TEMPORARY TABLE %s as SELECT * from %s' % \
244 (tempName, modelTable.name))
245 # make sure the drop takes place inside our
246 # transaction with the bind parameter
247 modelTable.drop(bind=connection)
248 modelTable.create(bind=connection)
249 connection.execute(getCopyStatement())
250 connection.execute('DROP TABLE %s' % tempName)
251 trans.commit()
252 except:
253 trans.rollback()
254 raise
@@ -0,0 +1,100 b''
1 """
2 Script to migrate repository from sqlalchemy <= 0.4.4 to the new
3 repository schema. This shouldn't use any other migrate modules, so
4 that it can work in any version.
5 """
6
7 import os
8 import sys
9 import logging
10
11 log = logging.getLogger(__name__)
12
13
14 def usage():
15 """Gives usage information."""
16 print """Usage: %(prog)s repository-to-migrate
17
18 Upgrade your repository to the new flat format.
19
20 NOTE: You should probably make a backup before running this.
21 """ % {'prog': sys.argv[0]}
22
23 sys.exit(1)
24
25
26 def delete_file(filepath):
27 """Deletes a file and prints a message."""
28 log.info('Deleting file: %s' % filepath)
29 os.remove(filepath)
30
31
32 def move_file(src, tgt):
33 """Moves a file and prints a message."""
34 log.info('Moving file %s to %s' % (src, tgt))
35 if os.path.exists(tgt):
36 raise Exception(
37 'Cannot move file %s because target %s already exists' % \
38 (src, tgt))
39 os.rename(src, tgt)
40
41
42 def delete_directory(dirpath):
43 """Delete a directory and print a message."""
44 log.info('Deleting directory: %s' % dirpath)
45 os.rmdir(dirpath)
46
47
48 def migrate_repository(repos):
49 """Does the actual migration to the new repository format."""
50 log.info('Migrating repository at: %s to new format' % repos)
51 versions = '%s/versions' % repos
52 dirs = os.listdir(versions)
53 # Only use int's in list.
54 numdirs = [int(dirname) for dirname in dirs if dirname.isdigit()]
55 numdirs.sort() # Sort list.
56 for dirname in numdirs:
57 origdir = '%s/%s' % (versions, dirname)
58 log.info('Working on directory: %s' % origdir)
59 files = os.listdir(origdir)
60 files.sort()
61 for filename in files:
62 # Delete compiled Python files.
63 if filename.endswith('.pyc') or filename.endswith('.pyo'):
64 delete_file('%s/%s' % (origdir, filename))
65
66 # Delete empty __init__.py files.
67 origfile = '%s/__init__.py' % origdir
68 if os.path.exists(origfile) and len(open(origfile).read()) == 0:
69 delete_file(origfile)
70
71 # Move sql upgrade scripts.
72 if filename.endswith('.sql'):
73 version, dbms, operation = filename.split('.', 3)[0:3]
74 origfile = '%s/%s' % (origdir, filename)
75 # For instance: 2.postgres.upgrade.sql ->
76 # 002_postgres_upgrade.sql
77 tgtfile = '%s/%03d_%s_%s.sql' % (
78 versions, int(version), dbms, operation)
79 move_file(origfile, tgtfile)
80
81 # Move Python upgrade script.
82 pyfile = '%s.py' % dirname
83 pyfilepath = '%s/%s' % (origdir, pyfile)
84 if os.path.exists(pyfilepath):
85 tgtfile = '%s/%03d.py' % (versions, int(dirname))
86 move_file(pyfilepath, tgtfile)
87
88 # Try to remove directory. Will fail if it's not empty.
89 delete_directory(origdir)
90
91
92 def main():
93 """Main function to be called when using this script."""
94 if len(sys.argv) != 2:
95 usage()
96 migrate_repository(sys.argv[1])
97
98
99 if __name__ == '__main__':
100 main()
@@ -0,0 +1,75 b''
1 """
2 A path/directory class.
3 """
4
5 import os
6 import shutil
7 import logging
8
9 from migrate import exceptions
10 from migrate.versioning.config import *
11 from migrate.versioning.util import KeyedInstance
12
13
14 log = logging.getLogger(__name__)
15
16 class Pathed(KeyedInstance):
17 """
18 A class associated with a path/directory tree.
19
20 Only one instance of this class may exist for a particular file;
21 __new__ will return an existing instance if possible
22 """
23 parent = None
24
25 @classmethod
26 def _key(cls, path):
27 return str(path)
28
29 def __init__(self, path):
30 self.path = path
31 if self.__class__.parent is not None:
32 self._init_parent(path)
33
34 def _init_parent(self, path):
35 """Try to initialize this object's parent, if it has one"""
36 parent_path = self.__class__._parent_path(path)
37 self.parent = self.__class__.parent(parent_path)
38 log.debug("Getting parent %r:%r" % (self.__class__.parent, parent_path))
39 self.parent._init_child(path, self)
40
41 def _init_child(self, child, path):
42 """Run when a child of this object is initialized.
43
44 Parameters: the child object; the path to this object (its
45 parent)
46 """
47
48 @classmethod
49 def _parent_path(cls, path):
50 """
51 Fetch the path of this object's parent from this object's path.
52 """
53 # os.path.dirname(), but strip directories like files (like
54 # unix basename)
55 #
56 # Treat directories like files...
57 if path[-1] == '/':
58 path = path[:-1]
59 ret = os.path.dirname(path)
60 return ret
61
62 @classmethod
63 def require_notfound(cls, path):
64 """Ensures a given path does not already exist"""
65 if os.path.exists(path):
66 raise exceptions.PathFoundError(path)
67
68 @classmethod
69 def require_found(cls, path):
70 """Ensures a given path already exists"""
71 if not os.path.exists(path):
72 raise exceptions.PathNotFoundError(path)
73
74 def __str__(self):
75 return self.path
@@ -0,0 +1,231 b''
1 """
2 SQLAlchemy migrate repository management.
3 """
4 import os
5 import shutil
6 import string
7 import logging
8
9 from pkg_resources import resource_filename
10 from tempita import Template as TempitaTemplate
11
12 from migrate import exceptions
13 from migrate.versioning import version, pathed, cfgparse
14 from migrate.versioning.template import Template
15 from migrate.versioning.config import *
16
17
18 log = logging.getLogger(__name__)
19
20 class Changeset(dict):
21 """A collection of changes to be applied to a database.
22
23 Changesets are bound to a repository and manage a set of
24 scripts from that repository.
25
26 Behaves like a dict, for the most part. Keys are ordered based on step value.
27 """
28
29 def __init__(self, start, *changes, **k):
30 """
31 Give a start version; step must be explicitly stated.
32 """
33 self.step = k.pop('step', 1)
34 self.start = version.VerNum(start)
35 self.end = self.start
36 for change in changes:
37 self.add(change)
38
39 def __iter__(self):
40 return iter(self.items())
41
42 def keys(self):
43 """
44 In a series of upgrades x -> y, keys are version x. Sorted.
45 """
46 ret = super(Changeset, self).keys()
47 # Reverse order if downgrading
48 ret.sort(reverse=(self.step < 1))
49 return ret
50
51 def values(self):
52 return [self[k] for k in self.keys()]
53
54 def items(self):
55 return zip(self.keys(), self.values())
56
57 def add(self, change):
58 """Add new change to changeset"""
59 key = self.end
60 self.end += self.step
61 self[key] = change
62
63 def run(self, *p, **k):
64 """Run the changeset scripts"""
65 for version, script in self:
66 script.run(*p, **k)
67
68
69 class Repository(pathed.Pathed):
70 """A project's change script repository"""
71
72 _config = 'migrate.cfg'
73 _versions = 'versions'
74
75 def __init__(self, path):
76 log.debug('Loading repository %s...' % path)
77 self.verify(path)
78 super(Repository, self).__init__(path)
79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
80 self.versions = version.Collection(os.path.join(self.path,
81 self._versions))
82 log.debug('Repository %s loaded successfully' % path)
83 log.debug('Config: %r' % self.config.to_dict())
84
85 @classmethod
86 def verify(cls, path):
87 """
88 Ensure the target path is a valid repository.
89
90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
91 """
92 # Ensure the existence of required files
93 try:
94 cls.require_found(path)
95 cls.require_found(os.path.join(path, cls._config))
96 cls.require_found(os.path.join(path, cls._versions))
97 except exceptions.PathNotFoundError, e:
98 raise exceptions.InvalidRepositoryError(path)
99
100 @classmethod
101 def prepare_config(cls, tmpl_dir, name, options=None):
102 """
103 Prepare a project configuration file for a new project.
104
105 :param tmpl_dir: Path to Repository template
106 :param config_file: Name of the config file in Repository template
107 :param name: Repository name
108 :type tmpl_dir: string
109 :type config_file: string
110 :type name: string
111 :returns: Populated config file
112 """
113 if options is None:
114 options = {}
115 options.setdefault('version_table', 'migrate_version')
116 options.setdefault('repository_id', name)
117 options.setdefault('required_dbs', [])
118
119 tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
120 ret = TempitaTemplate(tmpl).substitute(options)
121
122 # cleanup
123 del options['__template_name__']
124
125 return ret
126
127 @classmethod
128 def create(cls, path, name, **opts):
129 """Create a repository at a specified path"""
130 cls.require_notfound(path)
131 theme = opts.pop('templates_theme', None)
132 t_path = opts.pop('templates_path', None)
133
134 # Create repository
135 tmpl_dir = Template(t_path).get_repository(theme=theme)
136 shutil.copytree(tmpl_dir, path)
137
138 # Edit config defaults
139 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
140 fd = open(os.path.join(path, cls._config), 'w')
141 fd.write(config_text)
142 fd.close()
143
144 opts['repository_name'] = name
145
146 # Create a management script
147 manager = os.path.join(path, 'manage.py')
148 Repository.create_manage_file(manager, templates_theme=theme,
149 templates_path=t_path, **opts)
150
151 return cls(path)
152
153 def create_script(self, description, **k):
154 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
155 self.versions.create_new_python_version(description, **k)
156
157 def create_script_sql(self, database, **k):
158 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
159 self.versions.create_new_sql_version(database, **k)
160
161 @property
162 def latest(self):
163 """API to :attr:`migrate.versioning.version.Collection.latest`"""
164 return self.versions.latest
165
166 @property
167 def version_table(self):
168 """Returns version_table name specified in config"""
169 return self.config.get('db_settings', 'version_table')
170
171 @property
172 def id(self):
173 """Returns repository id specified in config"""
174 return self.config.get('db_settings', 'repository_id')
175
176 def version(self, *p, **k):
177 """API to :attr:`migrate.versioning.version.Collection.version`"""
178 return self.versions.version(*p, **k)
179
180 @classmethod
181 def clear(cls):
182 # TODO: deletes repo
183 super(Repository, cls).clear()
184 version.Collection.clear()
185
186 def changeset(self, database, start, end=None):
187 """Create a changeset to migrate this database from ver. start to end/latest.
188
189 :param database: name of database to generate changeset
190 :param start: version to start at
191 :param end: version to end at (latest if None given)
192 :type database: string
193 :type start: int
194 :type end: int
195 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
196 """
197 start = version.VerNum(start)
198
199 if end is None:
200 end = self.latest
201 else:
202 end = version.VerNum(end)
203
204 if start <= end:
205 step = 1
206 range_mod = 1
207 op = 'upgrade'
208 else:
209 step = -1
210 range_mod = 0
211 op = 'downgrade'
212
213 versions = range(start + range_mod, end + range_mod, step)
214 changes = [self.version(v).script(database, op) for v in versions]
215 ret = Changeset(start, step=step, *changes)
216 return ret
217
218 @classmethod
219 def create_manage_file(cls, file_, **opts):
220 """Create a project management script (manage.py)
221
222 :param file_: Destination file to be written
223 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
224 """
225 mng_file = Template(opts.pop('templates_path', None))\
226 .get_manage(theme=opts.pop('templates_theme', None))
227
228 tmpl = open(mng_file).read()
229 fd = open(file_, 'w')
230 fd.write(TempitaTemplate(tmpl).substitute(opts))
231 fd.close()
@@ -0,0 +1,213 b''
1 """
2 Database schema version management.
3 """
4 import sys
5 import logging
6
7 from sqlalchemy import (Table, Column, MetaData, String, Text, Integer,
8 create_engine)
9 from sqlalchemy.sql import and_
10 from sqlalchemy import exceptions as sa_exceptions
11 from sqlalchemy.sql import bindparam
12
13 from migrate import exceptions
14 from migrate.versioning import genmodel, schemadiff
15 from migrate.versioning.repository import Repository
16 from migrate.versioning.util import load_model
17 from migrate.versioning.version import VerNum
18
19
20 log = logging.getLogger(__name__)
21
22 class ControlledSchema(object):
23 """A database under version control"""
24
25 def __init__(self, engine, repository):
26 if isinstance(repository, basestring):
27 repository = Repository(repository)
28 self.engine = engine
29 self.repository = repository
30 self.meta = MetaData(engine)
31 self.load()
32
33 def __eq__(self, other):
34 """Compare two schemas by repositories and versions"""
35 return (self.repository is other.repository \
36 and self.version == other.version)
37
38 def load(self):
39 """Load controlled schema version info from DB"""
40 tname = self.repository.version_table
41 try:
42 if not hasattr(self, 'table') or self.table is None:
43 self.table = Table(tname, self.meta, autoload=True)
44
45 result = self.engine.execute(self.table.select(
46 self.table.c.repository_id == str(self.repository.id)))
47
48 data = list(result)[0]
49 except:
50 cls, exc, tb = sys.exc_info()
51 raise exceptions.DatabaseNotControlledError, exc.__str__(), tb
52
53 self.version = data['version']
54 return data
55
56 def drop(self):
57 """
58 Remove version control from a database.
59 """
60 try:
61 self.table.drop()
62 except (sa_exceptions.SQLError):
63 raise exceptions.DatabaseNotControlledError(str(self.table))
64
65 def changeset(self, version=None):
66 """API to Changeset creation.
67
68 Uses self.version for start version and engine.name
69 to get database name.
70 """
71 database = self.engine.name
72 start_ver = self.version
73 changeset = self.repository.changeset(database, start_ver, version)
74 return changeset
75
76 def runchange(self, ver, change, step):
77 startver = ver
78 endver = ver + step
79 # Current database version must be correct! Don't run if corrupt!
80 if self.version != startver:
81 raise exceptions.InvalidVersionError("%s is not %s" % \
82 (self.version, startver))
83 # Run the change
84 change.run(self.engine, step)
85
86 # Update/refresh database version
87 self.update_repository_table(startver, endver)
88 self.load()
89
90 def update_repository_table(self, startver, endver):
91 """Update version_table with new information"""
92 update = self.table.update(and_(self.table.c.version == int(startver),
93 self.table.c.repository_id == str(self.repository.id)))
94 self.engine.execute(update, version=int(endver))
95
96 def upgrade(self, version=None):
97 """
98 Upgrade (or downgrade) to a specified version, or latest version.
99 """
100 changeset = self.changeset(version)
101 for ver, change in changeset:
102 self.runchange(ver, change, changeset.step)
103
104 def update_db_from_model(self, model):
105 """
106 Modify the database to match the structure of the current Python model.
107 """
108 model = load_model(model)
109
110 diff = schemadiff.getDiffOfModelAgainstDatabase(
111 model, self.engine, excludeTables=[self.repository.version_table]
112 )
113 genmodel.ModelGenerator(diff,self.engine).applyModel()
114
115 self.update_repository_table(self.version, int(self.repository.latest))
116
117 self.load()
118
119 @classmethod
120 def create(cls, engine, repository, version=None):
121 """
122 Declare a database to be under a repository's version control.
123
124 :raises: :exc:`DatabaseAlreadyControlledError`
125 :returns: :class:`ControlledSchema`
126 """
127 # Confirm that the version # is valid: positive, integer,
128 # exists in repos
129 if isinstance(repository, basestring):
130 repository = Repository(repository)
131 version = cls._validate_version(repository, version)
132 table = cls._create_table_version(engine, repository, version)
133 # TODO: history table
134 # Load repository information and return
135 return cls(engine, repository)
136
137 @classmethod
138 def _validate_version(cls, repository, version):
139 """
140 Ensures this is a valid version number for this repository.
141
142 :raises: :exc:`InvalidVersionError` if invalid
143 :return: valid version number
144 """
145 if version is None:
146 version = 0
147 try:
148 version = VerNum(version) # raises valueerror
149 if version < 0 or version > repository.latest:
150 raise ValueError()
151 except ValueError:
152 raise exceptions.InvalidVersionError(version)
153 return version
154
155 @classmethod
156 def _create_table_version(cls, engine, repository, version):
157 """
158 Creates the versioning table in a database.
159
160 :raises: :exc:`DatabaseAlreadyControlledError`
161 """
162 # Create tables
163 tname = repository.version_table
164 meta = MetaData(engine)
165
166 table = Table(
167 tname, meta,
168 Column('repository_id', String(250), primary_key=True),
169 Column('repository_path', Text),
170 Column('version', Integer), )
171
172 # there can be multiple repositories/schemas in the same db
173 if not table.exists():
174 table.create()
175
176 # test for existing repository_id
177 s = table.select(table.c.repository_id == bindparam("repository_id"))
178 result = engine.execute(s, repository_id=repository.id)
179 if result.fetchone():
180 raise exceptions.DatabaseAlreadyControlledError
181
182 # Insert data
183 engine.execute(table.insert().values(
184 repository_id=repository.id,
185 repository_path=repository.path,
186 version=int(version)))
187 return table
188
189 @classmethod
190 def compare_model_to_db(cls, engine, model, repository):
191 """
192 Compare the current model against the current database.
193 """
194 if isinstance(repository, basestring):
195 repository = Repository(repository)
196 model = load_model(model)
197
198 diff = schemadiff.getDiffOfModelAgainstDatabase(
199 model, engine, excludeTables=[repository.version_table])
200 return diff
201
202 @classmethod
203 def create_model(cls, engine, repository, declarative=False):
204 """
205 Dump the current database as a Python model.
206 """
207 if isinstance(repository, basestring):
208 repository = Repository(repository)
209
210 diff = schemadiff.getDiffOfModelAgainstDatabase(
211 MetaData(), engine, excludeTables=[repository.version_table]
212 )
213 return genmodel.ModelGenerator(diff, engine, declarative).toPython()
@@ -0,0 +1,285 b''
1 """
2 Schema differencing support.
3 """
4
5 import logging
6 import sqlalchemy
7
8 from migrate.changeset import SQLA_06
9 from sqlalchemy.types import Float
10
11 log = logging.getLogger(__name__)
12
13 def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None):
14 """
15 Return differences of model against database.
16
17 :return: object which will evaluate to :keyword:`True` if there \
18 are differences else :keyword:`False`.
19 """
20 return SchemaDiff(metadata,
21 sqlalchemy.MetaData(engine, reflect=True),
22 labelA='model',
23 labelB='database',
24 excludeTables=excludeTables)
25
26
27 def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None):
28 """
29 Return differences of model against another model.
30
31 :return: object which will evaluate to :keyword:`True` if there \
32 are differences else :keyword:`False`.
33 """
34 return SchemaDiff(metadataA, metadataB, excludeTables)
35
36
37 class ColDiff(object):
38 """
39 Container for differences in one :class:`~sqlalchemy.schema.Column`
40 between two :class:`~sqlalchemy.schema.Table` instances, ``A``
41 and ``B``.
42
43 .. attribute:: col_A
44
45 The :class:`~sqlalchemy.schema.Column` object for A.
46
47 .. attribute:: col_B
48
49 The :class:`~sqlalchemy.schema.Column` object for B.
50
51 .. attribute:: type_A
52
53 The most generic type of the :class:`~sqlalchemy.schema.Column`
54 object in A.
55
56 .. attribute:: type_B
57
58 The most generic type of the :class:`~sqlalchemy.schema.Column`
59 object in A.
60
61 """
62
63 diff = False
64
65 def __init__(self,col_A,col_B):
66 self.col_A = col_A
67 self.col_B = col_B
68
69 self.type_A = col_A.type
70 self.type_B = col_B.type
71
72 self.affinity_A = self.type_A._type_affinity
73 self.affinity_B = self.type_B._type_affinity
74
75 if self.affinity_A is not self.affinity_B:
76 self.diff = True
77 return
78
79 if isinstance(self.type_A,Float) or isinstance(self.type_B,Float):
80 if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)):
81 self.diff=True
82 return
83
84 for attr in ('precision','scale','length'):
85 A = getattr(self.type_A,attr,None)
86 B = getattr(self.type_B,attr,None)
87 if not (A is None or B is None) and A!=B:
88 self.diff=True
89 return
90
91 def __nonzero__(self):
92 return self.diff
93
94 class TableDiff(object):
95 """
96 Container for differences in one :class:`~sqlalchemy.schema.Table`
97 between two :class:`~sqlalchemy.schema.MetaData` instances, ``A``
98 and ``B``.
99
100 .. attribute:: columns_missing_from_A
101
102 A sequence of column names that were found in B but weren't in
103 A.
104
105 .. attribute:: columns_missing_from_B
106
107 A sequence of column names that were found in A but weren't in
108 B.
109
110 .. attribute:: columns_different
111
112 A dictionary containing information about columns that were
113 found to be different.
114 It maps column names to a :class:`ColDiff` objects describing the
115 differences found.
116 """
117 __slots__ = (
118 'columns_missing_from_A',
119 'columns_missing_from_B',
120 'columns_different',
121 )
122
123 def __nonzero__(self):
124 return bool(
125 self.columns_missing_from_A or
126 self.columns_missing_from_B or
127 self.columns_different
128 )
129
130 class SchemaDiff(object):
131 """
132 Compute the difference between two :class:`~sqlalchemy.schema.MetaData`
133 objects.
134
135 The string representation of a :class:`SchemaDiff` will summarise
136 the changes found between the two
137 :class:`~sqlalchemy.schema.MetaData` objects.
138
139 The length of a :class:`SchemaDiff` will give the number of
140 changes found, enabling it to be used much like a boolean in
141 expressions.
142
143 :param metadataA:
144 First :class:`~sqlalchemy.schema.MetaData` to compare.
145
146 :param metadataB:
147 Second :class:`~sqlalchemy.schema.MetaData` to compare.
148
149 :param labelA:
150 The label to use in messages about the first
151 :class:`~sqlalchemy.schema.MetaData`.
152
153 :param labelB:
154 The label to use in messages about the second
155 :class:`~sqlalchemy.schema.MetaData`.
156
157 :param excludeTables:
158 A sequence of table names to exclude.
159
160 .. attribute:: tables_missing_from_A
161
162 A sequence of table names that were found in B but weren't in
163 A.
164
165 .. attribute:: tables_missing_from_B
166
167 A sequence of table names that were found in A but weren't in
168 B.
169
170 .. attribute:: tables_different
171
172 A dictionary containing information about tables that were found
173 to be different.
174 It maps table names to a :class:`TableDiff` objects describing the
175 differences found.
176 """
177
178 def __init__(self,
179 metadataA, metadataB,
180 labelA='metadataA',
181 labelB='metadataB',
182 excludeTables=None):
183
184 self.metadataA, self.metadataB = metadataA, metadataB
185 self.labelA, self.labelB = labelA, labelB
186 self.label_width = max(len(labelA),len(labelB))
187 excludeTables = set(excludeTables or [])
188
189 A_table_names = set(metadataA.tables.keys())
190 B_table_names = set(metadataB.tables.keys())
191
192 self.tables_missing_from_A = sorted(
193 B_table_names - A_table_names - excludeTables
194 )
195 self.tables_missing_from_B = sorted(
196 A_table_names - B_table_names - excludeTables
197 )
198
199 self.tables_different = {}
200 for table_name in A_table_names.intersection(B_table_names):
201
202 td = TableDiff()
203
204 A_table = metadataA.tables[table_name]
205 B_table = metadataB.tables[table_name]
206
207 A_column_names = set(A_table.columns.keys())
208 B_column_names = set(B_table.columns.keys())
209
210 td.columns_missing_from_A = sorted(
211 B_column_names - A_column_names
212 )
213
214 td.columns_missing_from_B = sorted(
215 A_column_names - B_column_names
216 )
217
218 td.columns_different = {}
219
220 for col_name in A_column_names.intersection(B_column_names):
221
222 cd = ColDiff(
223 A_table.columns.get(col_name),
224 B_table.columns.get(col_name)
225 )
226
227 if cd:
228 td.columns_different[col_name]=cd
229
230 # XXX - index and constraint differences should
231 # be checked for here
232
233 if td:
234 self.tables_different[table_name]=td
235
236 def __str__(self):
237 ''' Summarize differences. '''
238 out = []
239 column_template =' %%%is: %%r' % self.label_width
240
241 for names,label in (
242 (self.tables_missing_from_A,self.labelA),
243 (self.tables_missing_from_B,self.labelB),
244 ):
245 if names:
246 out.append(
247 ' tables missing from %s: %s' % (
248 label,', '.join(sorted(names))
249 )
250 )
251
252 for name,td in sorted(self.tables_different.items()):
253 out.append(
254 ' table with differences: %s' % name
255 )
256 for names,label in (
257 (td.columns_missing_from_A,self.labelA),
258 (td.columns_missing_from_B,self.labelB),
259 ):
260 if names:
261 out.append(
262 ' %s missing these columns: %s' % (
263 label,', '.join(sorted(names))
264 )
265 )
266 for name,cd in td.columns_different.items():
267 out.append(' column with differences: %s' % name)
268 out.append(column_template % (self.labelA,cd.col_A))
269 out.append(column_template % (self.labelB,cd.col_B))
270
271 if out:
272 out.insert(0, 'Schema diffs:')
273 return '\n'.join(out)
274 else:
275 return 'No schema diffs'
276
277 def __len__(self):
278 """
279 Used in bool evaluation, return of 0 means no diffs.
280 """
281 return (
282 len(self.tables_missing_from_A) +
283 len(self.tables_missing_from_B) +
284 len(self.tables_different)
285 )
@@ -0,0 +1,6 b''
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from migrate.versioning.script.base import BaseScript
5 from migrate.versioning.script.py import PythonScript
6 from migrate.versioning.script.sql import SqlScript
@@ -0,0 +1,57 b''
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 import logging
4
5 from migrate import exceptions
6 from migrate.versioning.config import operations
7 from migrate.versioning import pathed
8
9
10 log = logging.getLogger(__name__)
11
12 class BaseScript(pathed.Pathed):
13 """Base class for other types of scripts.
14 All scripts have the following properties:
15
16 source (script.source())
17 The source code of the script
18 version (script.version())
19 The version number of the script
20 operations (script.operations())
21 The operations defined by the script: upgrade(), downgrade() or both.
22 Returns a tuple of operations.
23 Can also check for an operation with ex. script.operation(Script.ops.up)
24 """ # TODO: sphinxfy this and implement it correctly
25
26 def __init__(self, path):
27 log.debug('Loading script %s...' % path)
28 self.verify(path)
29 super(BaseScript, self).__init__(path)
30 log.debug('Script %s loaded successfully' % path)
31
32 @classmethod
33 def verify(cls, path):
34 """Ensure this is a valid script
35 This version simply ensures the script file's existence
36
37 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
38 """
39 try:
40 cls.require_found(path)
41 except:
42 raise exceptions.InvalidScriptError(path)
43
44 def source(self):
45 """:returns: source code of the script.
46 :rtype: string
47 """
48 fd = open(self.path)
49 ret = fd.read()
50 fd.close()
51 return ret
52
53 def run(self, engine):
54 """Core of each BaseScript subclass.
55 This method executes the script.
56 """
57 raise NotImplementedError()
@@ -0,0 +1,159 b''
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import shutil
5 import warnings
6 import logging
7 from StringIO import StringIO
8
9 import migrate
10 from migrate.versioning import genmodel, schemadiff
11 from migrate.versioning.config import operations
12 from migrate.versioning.template import Template
13 from migrate.versioning.script import base
14 from migrate.versioning.util import import_path, load_model, with_engine
15 from migrate.exceptions import MigrateDeprecationWarning, InvalidScriptError, ScriptError
16
17 log = logging.getLogger(__name__)
18 __all__ = ['PythonScript']
19
20
21 class PythonScript(base.BaseScript):
22 """Base for Python scripts"""
23
24 @classmethod
25 def create(cls, path, **opts):
26 """Create an empty migration script at specified path
27
28 :returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
29 cls.require_notfound(path)
30
31 src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None))
32 shutil.copy(src, path)
33
34 return cls(path)
35
36 @classmethod
37 def make_update_script_for_model(cls, engine, oldmodel,
38 model, repository, **opts):
39 """Create a migration script based on difference between two SA models.
40
41 :param repository: path to migrate repository
42 :param oldmodel: dotted.module.name:SAClass or SAClass object
43 :param model: dotted.module.name:SAClass or SAClass object
44 :param engine: SQLAlchemy engine
45 :type repository: string or :class:`Repository instance <migrate.versioning.repository.Repository>`
46 :type oldmodel: string or Class
47 :type model: string or Class
48 :type engine: Engine instance
49 :returns: Upgrade / Downgrade script
50 :rtype: string
51 """
52
53 if isinstance(repository, basestring):
54 # oh dear, an import cycle!
55 from migrate.versioning.repository import Repository
56 repository = Repository(repository)
57
58 oldmodel = load_model(oldmodel)
59 model = load_model(model)
60
61 # Compute differences.
62 diff = schemadiff.getDiffOfModelAgainstModel(
63 oldmodel,
64 model,
65 excludeTables=[repository.version_table])
66 # TODO: diff can be False (there is no difference?)
67 decls, upgradeCommands, downgradeCommands = \
68 genmodel.ModelGenerator(diff,engine).toUpgradeDowngradePython()
69
70 # Store differences into file.
71 src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))
72 f = open(src)
73 contents = f.read()
74 f.close()
75
76 # generate source
77 search = 'def upgrade(migrate_engine):'
78 contents = contents.replace(search, '\n\n'.join((decls, search)), 1)
79 if upgradeCommands:
80 contents = contents.replace(' pass', upgradeCommands, 1)
81 if downgradeCommands:
82 contents = contents.replace(' pass', downgradeCommands, 1)
83 return contents
84
85 @classmethod
86 def verify_module(cls, path):
87 """Ensure path is a valid script
88
89 :param path: Script location
90 :type path: string
91 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
92 :returns: Python module
93 """
94 # Try to import and get the upgrade() func
95 module = import_path(path)
96 try:
97 assert callable(module.upgrade)
98 except Exception, e:
99 raise InvalidScriptError(path + ': %s' % str(e))
100 return module
101
102 def preview_sql(self, url, step, **args):
103 """Mocks SQLAlchemy Engine to store all executed calls in a string
104 and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>`
105
106 :returns: SQL file
107 """
108 buf = StringIO()
109 args['engine_arg_strategy'] = 'mock'
110 args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p)
111
112 @with_engine
113 def go(url, step, **kw):
114 engine = kw.pop('engine')
115 self.run(engine, step)
116 return buf.getvalue()
117
118 return go(url, step, **args)
119
120 def run(self, engine, step):
121 """Core method of Script file.
122 Exectues :func:`update` or :func:`downgrade` functions
123
124 :param engine: SQLAlchemy Engine
125 :param step: Operation to run
126 :type engine: string
127 :type step: int
128 """
129 if step > 0:
130 op = 'upgrade'
131 elif step < 0:
132 op = 'downgrade'
133 else:
134 raise ScriptError("%d is not a valid step" % step)
135
136 funcname = base.operations[op]
137 script_func = self._func(funcname)
138
139 try:
140 script_func(engine)
141 except TypeError:
142 warnings.warn("upgrade/downgrade functions must accept engine"
143 " parameter (since version > 0.5.4)", MigrateDeprecationWarning)
144 raise
145
146 @property
147 def module(self):
148 """Calls :meth:`migrate.versioning.script.py.verify_module`
149 and returns it.
150 """
151 if not hasattr(self, '_module'):
152 self._module = self.verify_module(self.path)
153 return self._module
154
155 def _func(self, funcname):
156 if not hasattr(self.module, funcname):
157 msg = "Function '%s' is not defined in this script"
158 raise ScriptError(msg % funcname)
159 return getattr(self.module, funcname)
@@ -0,0 +1,49 b''
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 import logging
4 import shutil
5
6 from migrate.versioning.script import base
7 from migrate.versioning.template import Template
8
9
10 log = logging.getLogger(__name__)
11
12 class SqlScript(base.BaseScript):
13 """A file containing plain SQL statements."""
14
15 @classmethod
16 def create(cls, path, **opts):
17 """Create an empty migration script at specified path
18
19 :returns: :class:`SqlScript instance <migrate.versioning.script.sql.SqlScript>`"""
20 cls.require_notfound(path)
21
22 src = Template(opts.pop('templates_path', None)).get_sql_script(theme=opts.pop('templates_theme', None))
23 shutil.copy(src, path)
24 return cls(path)
25
26 # TODO: why is step parameter even here?
27 def run(self, engine, step=None, executemany=True):
28 """Runs SQL script through raw dbapi execute call"""
29 text = self.source()
30 # Don't rely on SA's autocommit here
31 # (SA uses .startswith to check if a commit is needed. What if script
32 # starts with a comment?)
33 conn = engine.connect()
34 try:
35 trans = conn.begin()
36 try:
37 # HACK: SQLite doesn't allow multiple statements through
38 # its execute() method, but it provides executescript() instead
39 dbapi = conn.engine.raw_connection()
40 if executemany and getattr(dbapi, 'executescript', None):
41 dbapi.executescript(text)
42 else:
43 conn.execute(text)
44 trans.commit()
45 except:
46 trans.rollback()
47 raise
48 finally:
49 conn.close()
@@ -0,0 +1,215 b''
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """The migrate command-line tool."""
5
6 import sys
7 import inspect
8 import logging
9 from optparse import OptionParser, BadOptionError
10
11 from migrate import exceptions
12 from migrate.versioning import api
13 from migrate.versioning.config import *
14 from migrate.versioning.util import asbool
15
16
17 alias = dict(
18 s=api.script,
19 vc=api.version_control,
20 dbv=api.db_version,
21 v=api.version,
22 )
23
24 def alias_setup():
25 global alias
26 for key, val in alias.iteritems():
27 setattr(api, key, val)
28 alias_setup()
29
30
31 class PassiveOptionParser(OptionParser):
32
33 def _process_args(self, largs, rargs, values):
34 """little hack to support all --some_option=value parameters"""
35
36 while rargs:
37 arg = rargs[0]
38 if arg == "--":
39 del rargs[0]
40 return
41 elif arg[0:2] == "--":
42 # if parser does not know about the option
43 # pass it along (make it anonymous)
44 try:
45 opt = arg.split('=', 1)[0]
46 self._match_long_opt(opt)
47 except BadOptionError:
48 largs.append(arg)
49 del rargs[0]
50 else:
51 self._process_long_opt(rargs, values)
52 elif arg[:1] == "-" and len(arg) > 1:
53 self._process_short_opts(rargs, values)
54 elif self.allow_interspersed_args:
55 largs.append(arg)
56 del rargs[0]
57
58 def main(argv=None, **kwargs):
59 """Shell interface to :mod:`migrate.versioning.api`.
60
61 kwargs are default options that can be overriden with passing
62 --some_option as command line option
63
64 :param disable_logging: Let migrate configure logging
65 :type disable_logging: bool
66 """
67 if argv is not None:
68 argv = argv
69 else:
70 argv = list(sys.argv[1:])
71 commands = list(api.__all__)
72 commands.sort()
73
74 usage = """%%prog COMMAND ...
75
76 Available commands:
77 %s
78
79 Enter "%%prog help COMMAND" for information on a particular command.
80 """ % '\n\t'.join(["%s - %s" % (command.ljust(28),
81 api.command_desc.get(command)) for command in commands])
82
83 parser = PassiveOptionParser(usage=usage)
84 parser.add_option("-d", "--debug",
85 action="store_true",
86 dest="debug",
87 default=False,
88 help="Shortcut to turn on DEBUG mode for logging")
89 parser.add_option("-q", "--disable_logging",
90 action="store_true",
91 dest="disable_logging",
92 default=False,
93 help="Use this option to disable logging configuration")
94 help_commands = ['help', '-h', '--help']
95 HELP = False
96
97 try:
98 command = argv.pop(0)
99 if command in help_commands:
100 HELP = True
101 command = argv.pop(0)
102 except IndexError:
103 parser.print_help()
104 return
105
106 command_func = getattr(api, command, None)
107 if command_func is None or command.startswith('_'):
108 parser.error("Invalid command %s" % command)
109
110 parser.set_usage(inspect.getdoc(command_func))
111 f_args, f_varargs, f_kwargs, f_defaults = inspect.getargspec(command_func)
112 for arg in f_args:
113 parser.add_option(
114 "--%s" % arg,
115 dest=arg,
116 action='store',
117 type="string")
118
119 # display help of the current command
120 if HELP:
121 parser.print_help()
122 return
123
124 options, args = parser.parse_args(argv)
125
126 # override kwargs with anonymous parameters
127 override_kwargs = dict()
128 for arg in list(args):
129 if arg.startswith('--'):
130 args.remove(arg)
131 if '=' in arg:
132 opt, value = arg[2:].split('=', 1)
133 else:
134 opt = arg[2:]
135 value = True
136 override_kwargs[opt] = value
137
138 # override kwargs with options if user is overwriting
139 for key, value in options.__dict__.iteritems():
140 if value is not None:
141 override_kwargs[key] = value
142
143 # arguments that function accepts without passed kwargs
144 f_required = list(f_args)
145 candidates = dict(kwargs)
146 candidates.update(override_kwargs)
147 for key, value in candidates.iteritems():
148 if key in f_args:
149 f_required.remove(key)
150
151 # map function arguments to parsed arguments
152 for arg in args:
153 try:
154 kw = f_required.pop(0)
155 except IndexError:
156 parser.error("Too many arguments for command %s: %s" % (command,
157 arg))
158 kwargs[kw] = arg
159
160 # apply overrides
161 kwargs.update(override_kwargs)
162
163 # configure options
164 for key, value in options.__dict__.iteritems():
165 kwargs.setdefault(key, value)
166
167 # configure logging
168 if not asbool(kwargs.pop('disable_logging', False)):
169 # filter to log =< INFO into stdout and rest to stderr
170 class SingleLevelFilter(logging.Filter):
171 def __init__(self, min=None, max=None):
172 self.min = min or 0
173 self.max = max or 100
174
175 def filter(self, record):
176 return self.min <= record.levelno <= self.max
177
178 logger = logging.getLogger()
179 h1 = logging.StreamHandler(sys.stdout)
180 f1 = SingleLevelFilter(max=logging.INFO)
181 h1.addFilter(f1)
182 h2 = logging.StreamHandler(sys.stderr)
183 f2 = SingleLevelFilter(min=logging.WARN)
184 h2.addFilter(f2)
185 logger.addHandler(h1)
186 logger.addHandler(h2)
187
188 if options.debug:
189 logger.setLevel(logging.DEBUG)
190 else:
191 logger.setLevel(logging.INFO)
192
193 log = logging.getLogger(__name__)
194
195 # check if all args are given
196 try:
197 num_defaults = len(f_defaults)
198 except TypeError:
199 num_defaults = 0
200 f_args_default = f_args[len(f_args) - num_defaults:]
201 required = list(set(f_required) - set(f_args_default))
202 if required:
203 parser.error("Not enough arguments for command %s: %s not specified" \
204 % (command, ', '.join(required)))
205
206 # handle command
207 try:
208 ret = command_func(**kwargs)
209 if ret is not None:
210 log.info(ret)
211 except (exceptions.UsageError, exceptions.KnownError), e:
212 parser.error(e.args[0])
213
214 if __name__ == "__main__":
215 main()
@@ -0,0 +1,94 b''
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import os
5 import shutil
6 import sys
7
8 from pkg_resources import resource_filename
9
10 from migrate.versioning.config import *
11 from migrate.versioning import pathed
12
13
14 class Collection(pathed.Pathed):
15 """A collection of templates of a specific type"""
16 _mask = None
17
18 def get_path(self, file):
19 return os.path.join(self.path, str(file))
20
21
22 class RepositoryCollection(Collection):
23 _mask = '%s'
24
25 class ScriptCollection(Collection):
26 _mask = '%s.py_tmpl'
27
28 class ManageCollection(Collection):
29 _mask = '%s.py_tmpl'
30
31 class SQLScriptCollection(Collection):
32 _mask = '%s.py_tmpl'
33
34 class Template(pathed.Pathed):
35 """Finds the paths/packages of various Migrate templates.
36
37 :param path: Templates are loaded from migrate package
38 if `path` is not provided.
39 """
40 pkg = 'migrate.versioning.templates'
41 _manage = 'manage.py_tmpl'
42
43 def __new__(cls, path=None):
44 if path is None:
45 path = cls._find_path(cls.pkg)
46 return super(Template, cls).__new__(cls, path)
47
48 def __init__(self, path=None):
49 if path is None:
50 path = Template._find_path(self.pkg)
51 super(Template, self).__init__(path)
52 self.repository = RepositoryCollection(os.path.join(path, 'repository'))
53 self.script = ScriptCollection(os.path.join(path, 'script'))
54 self.manage = ManageCollection(os.path.join(path, 'manage'))
55 self.sql_script = SQLScriptCollection(os.path.join(path, 'sql_script'))
56
57 @classmethod
58 def _find_path(cls, pkg):
59 """Returns absolute path to dotted python package."""
60 tmp_pkg = pkg.rsplit('.', 1)
61
62 if len(tmp_pkg) != 1:
63 return resource_filename(tmp_pkg[0], tmp_pkg[1])
64 else:
65 return resource_filename(tmp_pkg[0], '')
66
67 def _get_item(self, collection, theme=None):
68 """Locates and returns collection.
69
70 :param collection: name of collection to locate
71 :param type_: type of subfolder in collection (defaults to "_default")
72 :returns: (package, source)
73 :rtype: str, str
74 """
75 item = getattr(self, collection)
76 theme_mask = getattr(item, '_mask')
77 theme = theme_mask % (theme or 'default')
78 return item.get_path(theme)
79
80 def get_repository(self, *a, **kw):
81 """Calls self._get_item('repository', *a, **kw)"""
82 return self._get_item('repository', *a, **kw)
83
84 def get_script(self, *a, **kw):
85 """Calls self._get_item('script', *a, **kw)"""
86 return self._get_item('script', *a, **kw)
87
88 def get_sql_script(self, *a, **kw):
89 """Calls self._get_item('sql_script', *a, **kw)"""
90 return self._get_item('sql_script', *a, **kw)
91
92 def get_manage(self, *a, **kw):
93 """Calls self._get_item('manage', *a, **kw)"""
94 return self._get_item('manage', *a, **kw)
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,5 b''
1 #!/usr/bin/env python
2 from migrate.versioning.shell import main
3
4 if __name__ == '__main__':
5 main(%(defaults)s)
@@ -0,0 +1,10 b''
1 #!/usr/bin/env python
2 from migrate.versioning.shell import main
3
4 {{py:
5 _vars = locals().copy()
6 del _vars['__template_name__']
7 _vars.pop('repository_name', None)
8 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
9 }}
10 main({{ defaults }})
@@ -0,0 +1,29 b''
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 import sys
4
5 from sqlalchemy import engine_from_config
6 from paste.deploy.loadwsgi import ConfigLoader
7
8 from migrate.versioning.shell import main
9 from {{ locals().pop('repository_name') }}.model import migrations
10
11
12 if '-c' in sys.argv:
13 pos = sys.argv.index('-c')
14 conf_path = sys.argv[pos + 1]
15 del sys.argv[pos:pos + 2]
16 else:
17 conf_path = 'development.ini'
18
19 {{py:
20 _vars = locals().copy()
21 del _vars['__template_name__']
22 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
23 }}
24
25 conf_dict = ConfigLoader(conf_path).parser._sections['app:main']
26
27 # migrate supports passing url as an existing Engine instance (since 0.6.0)
28 # usage: migrate -c path/to/config.ini COMMANDS
29 main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,4 b''
1 This is a database migration repository.
2
3 More information at
4 http://code.google.com/p/sqlalchemy-migrate/
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,20 b''
1 [db_settings]
2 # Used to identify which repository this database is versioned under.
3 # You can use the name of your project.
4 repository_id={{ locals().pop('repository_id') }}
5
6 # The name of the database table used to track the schema version.
7 # This name shouldn't already be used by your project.
8 # If this is changed once a database is under version control, you'll need to
9 # change the table name in each database too.
10 version_table={{ locals().pop('version_table') }}
11
12 # When committing a change script, Migrate will attempt to generate the
13 # sql for all supported databases; normally, if one of them fails - probably
14 # because you don't have that database installed - it is ignored and the
15 # commit continues, perhaps ending successfully.
16 # Databases in this list MUST compile successfully during a commit, or the
17 # entire commit will fail. List the databases your application will actually
18 # be using to ensure your updates to that database work properly.
19 # This must be a list; example: ['postgres','sqlite']
20 required_dbs={{ locals().pop('required_dbs') }}
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,4 b''
1 This is a database migration repository.
2
3 More information at
4 http://code.google.com/p/sqlalchemy-migrate/
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,20 b''
1 [db_settings]
2 # Used to identify which repository this database is versioned under.
3 # You can use the name of your project.
4 repository_id={{ locals().pop('repository_id') }}
5
6 # The name of the database table used to track the schema version.
7 # This name shouldn't already be used by your project.
8 # If this is changed once a database is under version control, you'll need to
9 # change the table name in each database too.
10 version_table={{ locals().pop('version_table') }}
11
12 # When committing a change script, Migrate will attempt to generate the
13 # sql for all supported databases; normally, if one of them fails - probably
14 # because you don't have that database installed - it is ignored and the
15 # commit continues, perhaps ending successfully.
16 # Databases in this list MUST compile successfully during a commit, or the
17 # entire commit will fail. List the databases your application will actually
18 # be using to ensure your updates to that database work properly.
19 # This must be a list; example: ['postgres','sqlite']
20 required_dbs={{ locals().pop('required_dbs') }}
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,11 b''
1 from sqlalchemy import *
2 from migrate import *
3
4 def upgrade(migrate_engine):
5 # Upgrade operations go here. Don't create your own engine; bind migrate_engine
6 # to your metadata
7 pass
8
9 def downgrade(migrate_engine):
10 # Operations to reverse the above upgrade go here.
11 pass
@@ -0,0 +1,11 b''
1 from sqlalchemy import *
2 from migrate import *
3
4 def upgrade(migrate_engine):
5 # Upgrade operations go here. Don't create your own engine; bind migrate_engine
6 # to your metadata
7 pass
8
9 def downgrade(migrate_engine):
10 # Operations to reverse the above upgrade go here.
11 pass
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,179 b''
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """.. currentmodule:: migrate.versioning.util"""
4
5 import warnings
6 import logging
7 from decorator import decorator
8 from pkg_resources import EntryPoint
9
10 from sqlalchemy import create_engine
11 from sqlalchemy.engine import Engine
12 from sqlalchemy.pool import StaticPool
13
14 from migrate import exceptions
15 from migrate.versioning.util.keyedinstance import KeyedInstance
16 from migrate.versioning.util.importpath import import_path
17
18
19 log = logging.getLogger(__name__)
20
21 def load_model(dotted_name):
22 """Import module and use module-level variable".
23
24 :param dotted_name: path to model in form of string: ``some.python.module:Class``
25
26 .. versionchanged:: 0.5.4
27
28 """
29 if isinstance(dotted_name, basestring):
30 if ':' not in dotted_name:
31 # backwards compatibility
32 warnings.warn('model should be in form of module.model:User '
33 'and not module.model.User', exceptions.MigrateDeprecationWarning)
34 dotted_name = ':'.join(dotted_name.rsplit('.', 1))
35 return EntryPoint.parse('x=%s' % dotted_name).load(False)
36 else:
37 # Assume it's already loaded.
38 return dotted_name
39
40 def asbool(obj):
41 """Do everything to use object as bool"""
42 if isinstance(obj, basestring):
43 obj = obj.strip().lower()
44 if obj in ['true', 'yes', 'on', 'y', 't', '1']:
45 return True
46 elif obj in ['false', 'no', 'off', 'n', 'f', '0']:
47 return False
48 else:
49 raise ValueError("String is not true/false: %r" % obj)
50 if obj in (True, False):
51 return bool(obj)
52 else:
53 raise ValueError("String is not true/false: %r" % obj)
54
55 def guess_obj_type(obj):
56 """Do everything to guess object type from string
57
58 Tries to convert to `int`, `bool` and finally returns if not succeded.
59
60 .. versionadded: 0.5.4
61 """
62
63 result = None
64
65 try:
66 result = int(obj)
67 except:
68 pass
69
70 if result is None:
71 try:
72 result = asbool(obj)
73 except:
74 pass
75
76 if result is not None:
77 return result
78 else:
79 return obj
80
81 @decorator
82 def catch_known_errors(f, *a, **kw):
83 """Decorator that catches known api errors
84
85 .. versionadded: 0.5.4
86 """
87
88 try:
89 return f(*a, **kw)
90 except exceptions.PathFoundError, e:
91 raise exceptions.KnownError("The path %s already exists" % e.args[0])
92
93 def construct_engine(engine, **opts):
94 """.. versionadded:: 0.5.4
95
96 Constructs and returns SQLAlchemy engine.
97
98 Currently, there are 2 ways to pass create_engine options to :mod:`migrate.versioning.api` functions:
99
100 :param engine: connection string or a existing engine
101 :param engine_dict: python dictionary of options to pass to `create_engine`
102 :param engine_arg_*: keyword parameters to pass to `create_engine` (evaluated with :func:`migrate.versioning.util.guess_obj_type`)
103 :type engine_dict: dict
104 :type engine: string or Engine instance
105 :type engine_arg_*: string
106 :returns: SQLAlchemy Engine
107
108 .. note::
109
110 keyword parameters override ``engine_dict`` values.
111
112 """
113 if isinstance(engine, Engine):
114 return engine
115 elif not isinstance(engine, basestring):
116 raise ValueError("you need to pass either an existing engine or a database uri")
117
118 # get options for create_engine
119 if opts.get('engine_dict') and isinstance(opts['engine_dict'], dict):
120 kwargs = opts['engine_dict']
121 else:
122 kwargs = dict()
123
124 # DEPRECATED: handle echo the old way
125 echo = asbool(opts.get('echo', False))
126 if echo:
127 warnings.warn('echo=True parameter is deprecated, pass '
128 'engine_arg_echo=True or engine_dict={"echo": True}',
129 exceptions.MigrateDeprecationWarning)
130 kwargs['echo'] = echo
131
132 # parse keyword arguments
133 for key, value in opts.iteritems():
134 if key.startswith('engine_arg_'):
135 kwargs[key[11:]] = guess_obj_type(value)
136
137 log.debug('Constructing engine')
138 # TODO: return create_engine(engine, poolclass=StaticPool, **kwargs)
139 # seems like 0.5.x branch does not work with engine.dispose and staticpool
140 return create_engine(engine, **kwargs)
141
142 @decorator
143 def with_engine(f, *a, **kw):
144 """Decorator for :mod:`migrate.versioning.api` functions
145 to safely close resources after function usage.
146
147 Passes engine parameters to :func:`construct_engine` and
148 resulting parameter is available as kw['engine'].
149
150 Engine is disposed after wrapped function is executed.
151
152 .. versionadded: 0.6.0
153 """
154 url = a[0]
155 engine = construct_engine(url, **kw)
156
157 try:
158 kw['engine'] = engine
159 return f(*a, **kw)
160 finally:
161 if isinstance(engine, Engine):
162 log.debug('Disposing SQLAlchemy engine %s', engine)
163 engine.dispose()
164
165
166 class Memoize:
167 """Memoize(fn) - an instance which acts like fn but memoizes its arguments
168 Will only work on functions with non-mutable arguments
169
170 ActiveState Code 52201
171 """
172 def __init__(self, fn):
173 self.fn = fn
174 self.memo = {}
175
176 def __call__(self, *args):
177 if not self.memo.has_key(args):
178 self.memo[args] = self.fn(*args)
179 return self.memo[args]
@@ -0,0 +1,16 b''
1 import os
2 import sys
3
4 def import_path(fullpath):
5 """ Import a file with full path specification. Allows one to
6 import from anywhere, something __import__ does not do.
7 """
8 # http://zephyrfalcon.org/weblog/arch_d7_2002_08_31.html
9 path, filename = os.path.split(fullpath)
10 filename, ext = os.path.splitext(filename)
11 sys.path.append(path)
12 module = __import__(filename)
13 reload(module) # Might be out of date during tests
14 del sys.path[-1]
15 return module
16
@@ -0,0 +1,36 b''
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 class KeyedInstance(object):
5 """A class whose instances have a unique identifier of some sort
6 No two instances with the same unique ID should exist - if we try to create
7 a second instance, the first should be returned.
8 """
9
10 _instances = dict()
11
12 def __new__(cls, *p, **k):
13 instances = cls._instances
14 clskey = str(cls)
15 if clskey not in instances:
16 instances[clskey] = dict()
17 instances = instances[clskey]
18
19 key = cls._key(*p, **k)
20 if key not in instances:
21 instances[key] = super(KeyedInstance, cls).__new__(cls)
22 return instances[key]
23
24 @classmethod
25 def _key(cls, *p, **k):
26 """Given a unique identifier, return a dictionary key
27 This should be overridden by child classes, to specify which parameters
28 should determine an object's uniqueness
29 """
30 raise NotImplementedError()
31
32 @classmethod
33 def clear(cls):
34 # Allow cls.clear() as well as uniqueInstance.clear(cls)
35 if str(cls) in cls._instances:
36 del cls._instances[str(cls)]
@@ -0,0 +1,215 b''
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import os
5 import re
6 import shutil
7 import logging
8
9 from migrate import exceptions
10 from migrate.versioning import pathed, script
11
12
13 log = logging.getLogger(__name__)
14
15 class VerNum(object):
16 """A version number that behaves like a string and int at the same time"""
17
18 _instances = dict()
19
20 def __new__(cls, value):
21 val = str(value)
22 if val not in cls._instances:
23 cls._instances[val] = super(VerNum, cls).__new__(cls)
24 ret = cls._instances[val]
25 return ret
26
27 def __init__(self,value):
28 self.value = str(int(value))
29 if self < 0:
30 raise ValueError("Version number cannot be negative")
31
32 def __add__(self, value):
33 ret = int(self) + int(value)
34 return VerNum(ret)
35
36 def __sub__(self, value):
37 return self + (int(value) * -1)
38
39 def __cmp__(self, value):
40 return int(self) - int(value)
41
42 def __repr__(self):
43 return "<VerNum(%s)>" % self.value
44
45 def __str__(self):
46 return str(self.value)
47
48 def __int__(self):
49 return int(self.value)
50
51
52 class Collection(pathed.Pathed):
53 """A collection of versioning scripts in a repository"""
54
55 FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*')
56
57 def __init__(self, path):
58 """Collect current version scripts in repository
59 and store them in self.versions
60 """
61 super(Collection, self).__init__(path)
62
63 # Create temporary list of files, allowing skipped version numbers.
64 files = os.listdir(path)
65 if '1' in files:
66 # deprecation
67 raise Exception('It looks like you have a repository in the old '
68 'format (with directories for each version). '
69 'Please convert repository before proceeding.')
70
71 tempVersions = dict()
72 for filename in files:
73 match = self.FILENAME_WITH_VERSION.match(filename)
74 if match:
75 num = int(match.group(1))
76 tempVersions.setdefault(num, []).append(filename)
77 else:
78 pass # Must be a helper file or something, let's ignore it.
79
80 # Create the versions member where the keys
81 # are VerNum's and the values are Version's.
82 self.versions = dict()
83 for num, files in tempVersions.items():
84 self.versions[VerNum(num)] = Version(num, path, files)
85
86 @property
87 def latest(self):
88 """:returns: Latest version in Collection"""
89 return max([VerNum(0)] + self.versions.keys())
90
91 def create_new_python_version(self, description, **k):
92 """Create Python files for new version"""
93 ver = self.latest + 1
94 extra = str_to_filename(description)
95
96 if extra:
97 if extra == '_':
98 extra = ''
99 elif not extra.startswith('_'):
100 extra = '_%s' % extra
101
102 filename = '%03d%s.py' % (ver, extra)
103 filepath = self._version_path(filename)
104
105 script.PythonScript.create(filepath, **k)
106 self.versions[ver] = Version(ver, self.path, [filename])
107
108 def create_new_sql_version(self, database, **k):
109 """Create SQL files for new version"""
110 ver = self.latest + 1
111 self.versions[ver] = Version(ver, self.path, [])
112
113 # Create new files.
114 for op in ('upgrade', 'downgrade'):
115 filename = '%03d_%s_%s.sql' % (ver, database, op)
116 filepath = self._version_path(filename)
117 script.SqlScript.create(filepath, **k)
118 self.versions[ver].add_script(filepath)
119
120 def version(self, vernum=None):
121 """Returns latest Version if vernum is not given.
122 Otherwise, returns wanted version"""
123 if vernum is None:
124 vernum = self.latest
125 return self.versions[VerNum(vernum)]
126
127 @classmethod
128 def clear(cls):
129 super(Collection, cls).clear()
130
131 def _version_path(self, ver):
132 """Returns path of file in versions repository"""
133 return os.path.join(self.path, str(ver))
134
135
136 class Version(object):
137 """A single version in a collection
138 :param vernum: Version Number
139 :param path: Path to script files
140 :param filelist: List of scripts
141 :type vernum: int, VerNum
142 :type path: string
143 :type filelist: list
144 """
145
146 def __init__(self, vernum, path, filelist):
147 self.version = VerNum(vernum)
148
149 # Collect scripts in this folder
150 self.sql = dict()
151 self.python = None
152
153 for script in filelist:
154 self.add_script(os.path.join(path, script))
155
156 def script(self, database=None, operation=None):
157 """Returns SQL or Python Script"""
158 for db in (database, 'default'):
159 # Try to return a .sql script first
160 try:
161 return self.sql[db][operation]
162 except KeyError:
163 continue # No .sql script exists
164
165 # TODO: maybe add force Python parameter?
166 ret = self.python
167
168 assert ret is not None, \
169 "There is no script for %d version" % self.version
170 return ret
171
172 def add_script(self, path):
173 """Add script to Collection/Version"""
174 if path.endswith(Extensions.py):
175 self._add_script_py(path)
176 elif path.endswith(Extensions.sql):
177 self._add_script_sql(path)
178
179 SQL_FILENAME = re.compile(r'^(\d+)_([^_]+)_([^_]+).sql')
180
181 def _add_script_sql(self, path):
182 basename = os.path.basename(path)
183 match = self.SQL_FILENAME.match(basename)
184
185 if match:
186 version, dbms, op = match.group(1), match.group(2), match.group(3)
187 else:
188 raise exceptions.ScriptError(
189 "Invalid SQL script name %s " % basename + \
190 "(needs to be ###_database_operation.sql)")
191
192 # File the script into a dictionary
193 self.sql.setdefault(dbms, {})[op] = script.SqlScript(path)
194
195 def _add_script_py(self, path):
196 if self.python is not None:
197 raise exceptions.ScriptError('You can only have one Python script '
198 'per version, but you have: %s and %s' % (self.python, path))
199 self.python = script.PythonScript(path)
200
201
202 class Extensions:
203 """A namespace for file extensions"""
204 py = 'py'
205 sql = 'sql'
206
207 def str_to_filename(s):
208 """Replaces spaces, (double and single) quotes
209 and double underscores to underscores
210 """
211
212 s = s.replace(' ', '_').replace('"', '_').replace("'", '_').replace(".", "_")
213 while '__' in s:
214 s = s.replace('__', '_')
215 return s
@@ -0,0 +1,238 b''
1 from migrate import *
2
3 #==============================================================================
4 # DB INITIAL MODEL
5 #==============================================================================
6 import logging
7 import datetime
8
9 from sqlalchemy import *
10 from sqlalchemy.exc import DatabaseError
11 from sqlalchemy.orm import relation, backref, class_mapper
12 from sqlalchemy.orm.session import Session
13
14 from rhodecode.model.meta import Base
15
16 log = logging.getLogger(__name__)
17
18 class BaseModel(object):
19
20 @classmethod
21 def _get_keys(cls):
22 """return column names for this model """
23 return class_mapper(cls).c.keys()
24
25 def get_dict(self):
26 """return dict with keys and values corresponding
27 to this model data """
28
29 d = {}
30 for k in self._get_keys():
31 d[k] = getattr(self, k)
32 return d
33
34 def get_appstruct(self):
35 """return list with keys and values tupples corresponding
36 to this model data """
37
38 l = []
39 for k in self._get_keys():
40 l.append((k, getattr(self, k),))
41 return l
42
43 def populate_obj(self, populate_dict):
44 """populate model with data from given populate_dict"""
45
46 for k in self._get_keys():
47 if k in populate_dict:
48 setattr(self, k, populate_dict[k])
49
50 class RhodeCodeSettings(Base, BaseModel):
51 __tablename__ = 'rhodecode_settings'
52 __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True})
53 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
54 app_settings_name = Column("app_settings_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
55 app_settings_value = Column("app_settings_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
56
57 def __init__(self, k, v):
58 self.app_settings_name = k
59 self.app_settings_value = v
60
61 def __repr__(self):
62 return "<RhodeCodeSetting('%s:%s')>" % (self.app_settings_name,
63 self.app_settings_value)
64
65 class RhodeCodeUi(Base, BaseModel):
66 __tablename__ = 'rhodecode_ui'
67 __table_args__ = {'useexisting':True}
68 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
69 ui_section = Column("ui_section", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
70 ui_key = Column("ui_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
71 ui_value = Column("ui_value", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
72 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
73
74
75 class User(Base, BaseModel):
76 __tablename__ = 'users'
77 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'useexisting':True})
78 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
79 username = Column("username", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
80 password = Column("password", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
81 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
82 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
83 name = Column("name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
84 lastname = Column("lastname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
85 email = Column("email", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
86 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
87 is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
88
89 user_log = relation('UserLog', cascade='all')
90 user_perms = relation('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
91
92 repositories = relation('Repository')
93 user_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
94
95 @property
96 def full_contact(self):
97 return '%s %s <%s>' % (self.name, self.lastname, self.email)
98
99 def __repr__(self):
100 return "<User('id:%s:%s')>" % (self.user_id, self.username)
101
102 def update_lastlogin(self):
103 """Update user lastlogin"""
104
105 try:
106 session = Session.object_session(self)
107 self.last_login = datetime.datetime.now()
108 session.add(self)
109 session.commit()
110 log.debug('updated user %s lastlogin', self.username)
111 except (DatabaseError,):
112 session.rollback()
113
114
115 class UserLog(Base, BaseModel):
116 __tablename__ = 'user_logs'
117 __table_args__ = {'useexisting':True}
118 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
119 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
120 repository_id = Column("repository_id", Integer(length=None, convert_unicode=False, assert_unicode=None), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
121 repository_name = Column("repository_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
122 user_ip = Column("user_ip", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
123 action = Column("action", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
124 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
125
126 user = relation('User')
127 repository = relation('Repository')
128
129 class Repository(Base, BaseModel):
130 __tablename__ = 'repositories'
131 __table_args__ = (UniqueConstraint('repo_name'), {'useexisting':True},)
132 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
133 repo_name = Column("repo_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
134 repo_type = Column("repo_type", String(length=None, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
135 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=False, default=None)
136 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
137 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
138 description = Column("description", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
139 fork_id = Column("fork_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=False, default=None)
140
141 user = relation('User')
142 fork = relation('Repository', remote_side=repo_id)
143 repo_to_perm = relation('RepoToPerm', cascade='all')
144 stats = relation('Statistics', cascade='all', uselist=False)
145
146 repo_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
147
148
149 def __repr__(self):
150 return "<Repository('%s:%s')>" % (self.repo_id, self.repo_name)
151
152 class Permission(Base, BaseModel):
153 __tablename__ = 'permissions'
154 __table_args__ = {'useexisting':True}
155 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
156 permission_name = Column("permission_name", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 permission_longname = Column("permission_longname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
158
159 def __repr__(self):
160 return "<Permission('%s:%s')>" % (self.permission_id, self.permission_name)
161
162 class RepoToPerm(Base, BaseModel):
163 __tablename__ = 'repo_to_perm'
164 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True})
165 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
166 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
167 permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
168 repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=None, default=None)
169
170 user = relation('User')
171 permission = relation('Permission')
172 repository = relation('Repository')
173
174 class UserToPerm(Base, BaseModel):
175 __tablename__ = 'user_to_perm'
176 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'useexisting':True})
177 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
178 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
179 permission_id = Column("permission_id", Integer(), ForeignKey(u'permissions.permission_id'), nullable=False, unique=None, default=None)
180
181 user = relation('User')
182 permission = relation('Permission')
183
184 class Statistics(Base, BaseModel):
185 __tablename__ = 'statistics'
186 __table_args__ = (UniqueConstraint('repository_id'), {'useexisting':True})
187 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
188 repository_id = Column("repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=False, unique=True, default=None)
189 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
190 commit_activity = Column("commit_activity", LargeBinary(), nullable=False)#JSON data
191 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
192 languages = Column("languages", LargeBinary(), nullable=False)#JSON data
193
194 repository = relation('Repository', single_parent=True)
195
196 class UserFollowing(Base, BaseModel):
197 __tablename__ = 'user_followings'
198 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
199 UniqueConstraint('user_id', 'follows_user_id')
200 , {'useexisting':True})
201
202 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
203 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
204 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None)
205 follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None)
206
207 user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id')
208
209 follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
210 follows_repository = relation('Repository')
211
212
213 class CacheInvalidation(Base, BaseModel):
214 __tablename__ = 'cache_invalidation'
215 __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
216 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
217 cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
218 cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
219 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
220
221
222 def __init__(self, cache_key, cache_args=''):
223 self.cache_key = cache_key
224 self.cache_args = cache_args
225 self.cache_active = False
226
227 def __repr__(self):
228 return "<CacheInvalidation('%s:%s')>" % (self.cache_id, self.cache_key)
229
230
231 def upgrade(migrate_engine):
232 # Upgrade operations go here. Don't create your own engine; bind migrate_engine
233 # to your metadata
234 Base.metadata.create_all(bind=migrate_engine, checkfirst=False)
235
236 def downgrade(migrate_engine):
237 # Operations to reverse the above upgrade go here.
238 Base.metadata.drop_all(bind=migrate_engine, checkfirst=False)
@@ -0,0 +1,118 b''
1 from sqlalchemy import *
2 from sqlalchemy.orm import relation
3
4 from migrate import *
5 from migrate.changeset import *
6 from rhodecode.model.meta import Base, BaseModel
7
8 def upgrade(migrate_engine):
9 """ Upgrade operations go here.
10 Don't create your own engine; bind migrate_engine to your metadata
11 """
12
13 #==========================================================================
14 # Upgrade of `users` table
15 #==========================================================================
16 tblname = 'users'
17 tbl = Table(tblname, MetaData(bind=migrate_engine), autoload=True,
18 autoload_with=migrate_engine)
19
20 #ADD is_ldap column
21 is_ldap = Column("is_ldap", Boolean(), nullable=False,
22 unique=None, default=False)
23 is_ldap.create(tbl)
24
25
26 #==========================================================================
27 # Upgrade of `user_logs` table
28 #==========================================================================
29
30 tblname = 'users'
31 tbl = Table(tblname, MetaData(bind=migrate_engine), autoload=True,
32 autoload_with=migrate_engine)
33
34 #ADD revision column
35 revision = Column('revision', TEXT(length=None, convert_unicode=False,
36 assert_unicode=None),
37 nullable=True, unique=None, default=None)
38 revision.create(tbl)
39
40
41
42 #==========================================================================
43 # Upgrade of `repositories` table
44 #==========================================================================
45 tblname = 'users'
46 tbl = Table(tblname, MetaData(bind=migrate_engine), autoload=True,
47 autoload_with=migrate_engine)
48
49 #ADD repo_type column
50 repo_type = Column("repo_type", String(length=None, convert_unicode=False,
51 assert_unicode=None),
52 nullable=False, unique=False, default=None)
53 repo_type.create(tbl)
54
55
56 #ADD statistics column
57 enable_statistics = Column("statistics", Boolean(), nullable=True,
58 unique=None, default=True)
59 enable_statistics.create(tbl)
60
61
62
63 #==========================================================================
64 # Add table `user_followings`
65 #==========================================================================
66 tblname = 'user_followings'
67 class UserFollowing(Base, BaseModel):
68 __tablename__ = 'user_followings'
69 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
70 UniqueConstraint('user_id', 'follows_user_id')
71 , {'useexisting':True})
72
73 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
74 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
75 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None)
76 follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None)
77
78 user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id')
79
80 follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
81 follows_repository = relation('Repository')
82
83 Base.metadata.tables[tblname].create(migrate_engine)
84
85 #==========================================================================
86 # Add table `cache_invalidation`
87 #==========================================================================
88 class CacheInvalidation(Base, BaseModel):
89 __tablename__ = 'cache_invalidation'
90 __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
91 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
92 cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
93 cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
94 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
95
96
97 def __init__(self, cache_key, cache_args=''):
98 self.cache_key = cache_key
99 self.cache_args = cache_args
100 self.cache_active = False
101
102 def __repr__(self):
103 return "<CacheInvalidation('%s:%s')>" % (self.cache_id, self.cache_key)
104
105 Base.metadata.tables[tblname].create(migrate_engine)
106
107 return
108
109
110
111
112
113
114 def downgrade(migrate_engine):
115 meta = MetaData()
116 meta.bind = migrate_engine
117
118
@@ -0,0 +1,26 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.dbmigrate.versions.__init__
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Package containing new versions of database models
7
8 :created_on: Dec 11, 2010
9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software; you can redistribute it and/or
14 # modify it under the terms of the GNU General Public License
15 # as published by the Free Software Foundation; version 2
16 # of the License or (at your opinion) any later version of the license.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 # MA 02110-1301, USA.
@@ -1,7 +1,7 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 package.rhodecode.lib.utils
3 rhodecode.lib.utils
4 ~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Utilities library for RhodeCode
6 Utilities library for RhodeCode
7
7
@@ -599,30 +599,3 b' class BasePasterCommand(Command):'
599 path_to_ini_file = os.path.realpath(conf)
599 path_to_ini_file = os.path.realpath(conf)
600 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
600 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
601 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
601 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
602
603
604
605 class UpgradeDb(BasePasterCommand):
606 """Command used for paster to upgrade our database to newer version
607 """
608
609 max_args = 1
610 min_args = 1
611
612 usage = "CONFIG_FILE"
613 summary = "Upgrades current db to newer version given configuration file"
614 group_name = "RhodeCode"
615
616 parser = Command.standard_parser(verbose=True)
617
618 def command(self):
619 from pylons import config
620 raise NotImplementedError('Not implemented yet')
621
622
623 def update_parser(self):
624 self.parser.add_option('--sql',
625 action='store_true',
626 dest='just_sql',
627 help="Prints upgrade sql for further investigation",
628 default=False)
@@ -93,7 +93,7 b' setup('
93
93
94 [paste.global_paster_command]
94 [paste.global_paster_command]
95 make-index = rhodecode.lib.indexers:MakeIndex
95 make-index = rhodecode.lib.indexers:MakeIndex
96 upgrade-db = rhodecode.lib.utils:UpgradeDb
96 upgrade-db = rhodecode.lib.dbmigrate:UpgradeDb
97 celeryd=rhodecode.lib.celerypylons.commands:CeleryDaemonCommand
97 celeryd=rhodecode.lib.celerypylons.commands:CeleryDaemonCommand
98 celerybeat=rhodecode.lib.celerypylons.commands:CeleryBeatCommand
98 celerybeat=rhodecode.lib.celerypylons.commands:CeleryBeatCommand
99 camqadm=rhodecode.lib.celerypylons.commands:CAMQPAdminCommand
99 camqadm=rhodecode.lib.celerypylons.commands:CAMQPAdminCommand
General Comments 0
You need to be logged in to leave comments. Login now