##// END OF EJS Templates
update migrations for 1.2
marcink -
r1442:7f31de15 beta
parent child Browse files
Show More
@@ -1,9 +1,11 b''
1 """
1 """
2 SQLAlchemy migrate provides two APIs :mod:`migrate.versioning` for
2 SQLAlchemy migrate provides two APIs :mod:`migrate.versioning` for
3 database schema version and repository management and
3 database schema version and repository management and
4 :mod:`migrate.changeset` that allows to define database schema changes
4 :mod:`migrate.changeset` that allows to define database schema changes
5 using Python.
5 using Python.
6 """
6 """
7
7
8 from rhodecode.lib.dbmigrate.migrate.versioning import *
8 from rhodecode.lib.dbmigrate.migrate.versioning import *
9 from rhodecode.lib.dbmigrate.migrate.changeset import *
9 from rhodecode.lib.dbmigrate.migrate.changeset import *
10
11 __version__ = '0.7.2.dev' No newline at end of file
@@ -1,29 +1,30 b''
1 """
1 """
2 This module extends SQLAlchemy and provides additional DDL [#]_
2 This module extends SQLAlchemy and provides additional DDL [#]_
3 support.
3 support.
4
4
5 .. [#] SQL Data Definition Language
5 .. [#] SQL Data Definition Language
6 """
6 """
7 import re
7 import re
8 import warnings
8 import warnings
9
9
10 import sqlalchemy
10 import sqlalchemy
11 from sqlalchemy import __version__ as _sa_version
11 from sqlalchemy import __version__ as _sa_version
12
12
13 warnings.simplefilter('always', DeprecationWarning)
13 warnings.simplefilter('always', DeprecationWarning)
14
14
15 _sa_version = tuple(int(re.match("\d+", x).group(0))
15 _sa_version = tuple(int(re.match("\d+", x).group(0))
16 for x in _sa_version.split("."))
16 for x in _sa_version.split("."))
17 SQLA_06 = _sa_version >= (0, 6)
17 SQLA_06 = _sa_version >= (0, 6)
18 SQLA_07 = _sa_version >= (0, 7)
18
19
19 del re
20 del re
20 del _sa_version
21 del _sa_version
21
22
22 from rhodecode.lib.dbmigrate.migrate.changeset.schema import *
23 from rhodecode.lib.dbmigrate.migrate.changeset.schema import *
23 from rhodecode.lib.dbmigrate.migrate.changeset.constraint import *
24 from rhodecode.lib.dbmigrate.migrate.changeset.constraint import *
24
25
25 sqlalchemy.schema.Table.__bases__ += (ChangesetTable,)
26 sqlalchemy.schema.Table.__bases__ += (ChangesetTable,)
26 sqlalchemy.schema.Column.__bases__ += (ChangesetColumn,)
27 sqlalchemy.schema.Column.__bases__ += (ChangesetColumn,)
27 sqlalchemy.schema.Index.__bases__ += (ChangesetIndex,)
28 sqlalchemy.schema.Index.__bases__ += (ChangesetIndex,)
28
29
29 sqlalchemy.schema.DefaultClause.__bases__ += (ChangesetDefaultClause,)
30 sqlalchemy.schema.DefaultClause.__bases__ += (ChangesetDefaultClause,)
@@ -1,651 +1,657 b''
1 """
1 """
2 Schema module providing common schema operations.
2 Schema module providing common schema operations.
3 """
3 """
4 import warnings
4 import warnings
5
5
6 from UserDict import DictMixin
6 from UserDict import DictMixin
7
7
8 import sqlalchemy
8 import sqlalchemy
9
9
10 from sqlalchemy.schema import ForeignKeyConstraint
10 from sqlalchemy.schema import ForeignKeyConstraint
11 from sqlalchemy.schema import UniqueConstraint
11 from sqlalchemy.schema import UniqueConstraint
12
12
13 from rhodecode.lib.dbmigrate.migrate.exceptions import *
13 from rhodecode.lib.dbmigrate.migrate.exceptions import *
14 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06
14 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06, SQLA_07
15 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (get_engine_visitor,
15 from rhodecode.lib.dbmigrate.migrate.changeset.databases.visitor import (get_engine_visitor,
16 run_single_visitor)
16 run_single_visitor)
17
17
18
18
19 __all__ = [
19 __all__ = [
20 'create_column',
20 'create_column',
21 'drop_column',
21 'drop_column',
22 'alter_column',
22 'alter_column',
23 'rename_table',
23 'rename_table',
24 'rename_index',
24 'rename_index',
25 'ChangesetTable',
25 'ChangesetTable',
26 'ChangesetColumn',
26 'ChangesetColumn',
27 'ChangesetIndex',
27 'ChangesetIndex',
28 'ChangesetDefaultClause',
28 'ChangesetDefaultClause',
29 'ColumnDelta',
29 'ColumnDelta',
30 ]
30 ]
31
31
32 def create_column(column, table=None, *p, **kw):
32 def create_column(column, table=None, *p, **kw):
33 """Create a column, given the table.
33 """Create a column, given the table.
34
34
35 API to :meth:`ChangesetColumn.create`.
35 API to :meth:`ChangesetColumn.create`.
36 """
36 """
37 if table is not None:
37 if table is not None:
38 return table.create_column(column, *p, **kw)
38 return table.create_column(column, *p, **kw)
39 return column.create(*p, **kw)
39 return column.create(*p, **kw)
40
40
41
41
42 def drop_column(column, table=None, *p, **kw):
42 def drop_column(column, table=None, *p, **kw):
43 """Drop a column, given the table.
43 """Drop a column, given the table.
44
44
45 API to :meth:`ChangesetColumn.drop`.
45 API to :meth:`ChangesetColumn.drop`.
46 """
46 """
47 if table is not None:
47 if table is not None:
48 return table.drop_column(column, *p, **kw)
48 return table.drop_column(column, *p, **kw)
49 return column.drop(*p, **kw)
49 return column.drop(*p, **kw)
50
50
51
51
52 def rename_table(table, name, engine=None, **kw):
52 def rename_table(table, name, engine=None, **kw):
53 """Rename a table.
53 """Rename a table.
54
54
55 If Table instance is given, engine is not used.
55 If Table instance is given, engine is not used.
56
56
57 API to :meth:`ChangesetTable.rename`.
57 API to :meth:`ChangesetTable.rename`.
58
58
59 :param table: Table to be renamed.
59 :param table: Table to be renamed.
60 :param name: New name for Table.
60 :param name: New name for Table.
61 :param engine: Engine instance.
61 :param engine: Engine instance.
62 :type table: string or Table instance
62 :type table: string or Table instance
63 :type name: string
63 :type name: string
64 :type engine: obj
64 :type engine: obj
65 """
65 """
66 table = _to_table(table, engine)
66 table = _to_table(table, engine)
67 table.rename(name, **kw)
67 table.rename(name, **kw)
68
68
69
69
70 def rename_index(index, name, table=None, engine=None, **kw):
70 def rename_index(index, name, table=None, engine=None, **kw):
71 """Rename an index.
71 """Rename an index.
72
72
73 If Index instance is given,
73 If Index instance is given,
74 table and engine are not used.
74 table and engine are not used.
75
75
76 API to :meth:`ChangesetIndex.rename`.
76 API to :meth:`ChangesetIndex.rename`.
77
77
78 :param index: Index to be renamed.
78 :param index: Index to be renamed.
79 :param name: New name for index.
79 :param name: New name for index.
80 :param table: Table to which Index is reffered.
80 :param table: Table to which Index is reffered.
81 :param engine: Engine instance.
81 :param engine: Engine instance.
82 :type index: string or Index instance
82 :type index: string or Index instance
83 :type name: string
83 :type name: string
84 :type table: string or Table instance
84 :type table: string or Table instance
85 :type engine: obj
85 :type engine: obj
86 """
86 """
87 index = _to_index(index, table, engine)
87 index = _to_index(index, table, engine)
88 index.rename(name, **kw)
88 index.rename(name, **kw)
89
89
90
90
91 def alter_column(*p, **k):
91 def alter_column(*p, **k):
92 """Alter a column.
92 """Alter a column.
93
93
94 This is a helper function that creates a :class:`ColumnDelta` and
94 This is a helper function that creates a :class:`ColumnDelta` and
95 runs it.
95 runs it.
96
96
97 :argument column:
97 :argument column:
98 The name of the column to be altered or a
98 The name of the column to be altered or a
99 :class:`ChangesetColumn` column representing it.
99 :class:`ChangesetColumn` column representing it.
100
100
101 :param table:
101 :param table:
102 A :class:`~sqlalchemy.schema.Table` or table name to
102 A :class:`~sqlalchemy.schema.Table` or table name to
103 for the table where the column will be changed.
103 for the table where the column will be changed.
104
104
105 :param engine:
105 :param engine:
106 The :class:`~sqlalchemy.engine.base.Engine` to use for table
106 The :class:`~sqlalchemy.engine.base.Engine` to use for table
107 reflection and schema alterations.
107 reflection and schema alterations.
108
108
109 :returns: A :class:`ColumnDelta` instance representing the change.
109 :returns: A :class:`ColumnDelta` instance representing the change.
110
110
111
111
112 """
112 """
113
113
114 if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
114 if 'table' not in k and isinstance(p[0], sqlalchemy.Column):
115 k['table'] = p[0].table
115 k['table'] = p[0].table
116 if 'engine' not in k:
116 if 'engine' not in k:
117 k['engine'] = k['table'].bind
117 k['engine'] = k['table'].bind
118
118
119 # deprecation
119 # deprecation
120 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
120 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
121 warnings.warn(
121 warnings.warn(
122 "Passing a Column object to alter_column is deprecated."
122 "Passing a Column object to alter_column is deprecated."
123 " Just pass in keyword parameters instead.",
123 " Just pass in keyword parameters instead.",
124 MigrateDeprecationWarning
124 MigrateDeprecationWarning
125 )
125 )
126 engine = k['engine']
126 engine = k['engine']
127
127
128 # enough tests seem to break when metadata is always altered
128 # enough tests seem to break when metadata is always altered
129 # that this crutch has to be left in until they can be sorted
129 # that this crutch has to be left in until they can be sorted
130 # out
130 # out
131 k['alter_metadata']=True
131 k['alter_metadata']=True
132
132
133 delta = ColumnDelta(*p, **k)
133 delta = ColumnDelta(*p, **k)
134
134
135 visitorcallable = get_engine_visitor(engine, 'schemachanger')
135 visitorcallable = get_engine_visitor(engine, 'schemachanger')
136 engine._run_visitor(visitorcallable, delta)
136 engine._run_visitor(visitorcallable, delta)
137
137
138 return delta
138 return delta
139
139
140
140
141 def _to_table(table, engine=None):
141 def _to_table(table, engine=None):
142 """Return if instance of Table, else construct new with metadata"""
142 """Return if instance of Table, else construct new with metadata"""
143 if isinstance(table, sqlalchemy.Table):
143 if isinstance(table, sqlalchemy.Table):
144 return table
144 return table
145
145
146 # Given: table name, maybe an engine
146 # Given: table name, maybe an engine
147 meta = sqlalchemy.MetaData()
147 meta = sqlalchemy.MetaData()
148 if engine is not None:
148 if engine is not None:
149 meta.bind = engine
149 meta.bind = engine
150 return sqlalchemy.Table(table, meta)
150 return sqlalchemy.Table(table, meta)
151
151
152
152
153 def _to_index(index, table=None, engine=None):
153 def _to_index(index, table=None, engine=None):
154 """Return if instance of Index, else construct new with metadata"""
154 """Return if instance of Index, else construct new with metadata"""
155 if isinstance(index, sqlalchemy.Index):
155 if isinstance(index, sqlalchemy.Index):
156 return index
156 return index
157
157
158 # Given: index name; table name required
158 # Given: index name; table name required
159 table = _to_table(table, engine)
159 table = _to_table(table, engine)
160 ret = sqlalchemy.Index(index)
160 ret = sqlalchemy.Index(index)
161 ret.table = table
161 ret.table = table
162 return ret
162 return ret
163
163
164
164
165 class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
165 class ColumnDelta(DictMixin, sqlalchemy.schema.SchemaItem):
166 """Extracts the differences between two columns/column-parameters
166 """Extracts the differences between two columns/column-parameters
167
167
168 May receive parameters arranged in several different ways:
168 May receive parameters arranged in several different ways:
169
169
170 * **current_column, new_column, \*p, \*\*kw**
170 * **current_column, new_column, \*p, \*\*kw**
171 Additional parameters can be specified to override column
171 Additional parameters can be specified to override column
172 differences.
172 differences.
173
173
174 * **current_column, \*p, \*\*kw**
174 * **current_column, \*p, \*\*kw**
175 Additional parameters alter current_column. Table name is extracted
175 Additional parameters alter current_column. Table name is extracted
176 from current_column object.
176 from current_column object.
177 Name is changed to current_column.name from current_name,
177 Name is changed to current_column.name from current_name,
178 if current_name is specified.
178 if current_name is specified.
179
179
180 * **current_col_name, \*p, \*\*kw**
180 * **current_col_name, \*p, \*\*kw**
181 Table kw must specified.
181 Table kw must specified.
182
182
183 :param table: Table at which current Column should be bound to.\
183 :param table: Table at which current Column should be bound to.\
184 If table name is given, reflection will be used.
184 If table name is given, reflection will be used.
185 :type table: string or Table instance
185 :type table: string or Table instance
186
186
187 :param metadata: A :class:`MetaData` instance to store
187 :param metadata: A :class:`MetaData` instance to store
188 reflected table names
188 reflected table names
189
189
190 :param engine: When reflecting tables, either engine or metadata must \
190 :param engine: When reflecting tables, either engine or metadata must \
191 be specified to acquire engine object.
191 be specified to acquire engine object.
192 :type engine: :class:`Engine` instance
192 :type engine: :class:`Engine` instance
193 :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
193 :returns: :class:`ColumnDelta` instance provides interface for altered attributes to \
194 `result_column` through :func:`dict` alike object.
194 `result_column` through :func:`dict` alike object.
195
195
196 * :class:`ColumnDelta`.result_column is altered column with new attributes
196 * :class:`ColumnDelta`.result_column is altered column with new attributes
197
197
198 * :class:`ColumnDelta`.current_name is current name of column in db
198 * :class:`ColumnDelta`.current_name is current name of column in db
199
199
200
200
201 """
201 """
202
202
203 # Column attributes that can be altered
203 # Column attributes that can be altered
204 diff_keys = ('name', 'type', 'primary_key', 'nullable',
204 diff_keys = ('name', 'type', 'primary_key', 'nullable',
205 'server_onupdate', 'server_default', 'autoincrement')
205 'server_onupdate', 'server_default', 'autoincrement')
206 diffs = dict()
206 diffs = dict()
207 __visit_name__ = 'column'
207 __visit_name__ = 'column'
208
208
209 def __init__(self, *p, **kw):
209 def __init__(self, *p, **kw):
210 # 'alter_metadata' is not a public api. It exists purely
210 # 'alter_metadata' is not a public api. It exists purely
211 # as a crutch until the tests that fail when 'alter_metadata'
211 # as a crutch until the tests that fail when 'alter_metadata'
212 # behaviour always happens can be sorted out
212 # behaviour always happens can be sorted out
213 self.alter_metadata = kw.pop("alter_metadata", False)
213 self.alter_metadata = kw.pop("alter_metadata", False)
214
214
215 self.meta = kw.pop("metadata", None)
215 self.meta = kw.pop("metadata", None)
216 self.engine = kw.pop("engine", None)
216 self.engine = kw.pop("engine", None)
217
217
218 # Things are initialized differently depending on how many column
218 # Things are initialized differently depending on how many column
219 # parameters are given. Figure out how many and call the appropriate
219 # parameters are given. Figure out how many and call the appropriate
220 # method.
220 # method.
221 if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
221 if len(p) >= 1 and isinstance(p[0], sqlalchemy.Column):
222 # At least one column specified
222 # At least one column specified
223 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
223 if len(p) >= 2 and isinstance(p[1], sqlalchemy.Column):
224 # Two columns specified
224 # Two columns specified
225 diffs = self.compare_2_columns(*p, **kw)
225 diffs = self.compare_2_columns(*p, **kw)
226 else:
226 else:
227 # Exactly one column specified
227 # Exactly one column specified
228 diffs = self.compare_1_column(*p, **kw)
228 diffs = self.compare_1_column(*p, **kw)
229 else:
229 else:
230 # Zero columns specified
230 # Zero columns specified
231 if not len(p) or not isinstance(p[0], basestring):
231 if not len(p) or not isinstance(p[0], basestring):
232 raise ValueError("First argument must be column name")
232 raise ValueError("First argument must be column name")
233 diffs = self.compare_parameters(*p, **kw)
233 diffs = self.compare_parameters(*p, **kw)
234
234
235 self.apply_diffs(diffs)
235 self.apply_diffs(diffs)
236
236
237 def __repr__(self):
237 def __repr__(self):
238 return '<ColumnDelta altermetadata=%r, %s>' % (
238 return '<ColumnDelta altermetadata=%r, %s>' % (
239 self.alter_metadata,
239 self.alter_metadata,
240 super(ColumnDelta, self).__repr__()
240 super(ColumnDelta, self).__repr__()
241 )
241 )
242
242
243 def __getitem__(self, key):
243 def __getitem__(self, key):
244 if key not in self.keys():
244 if key not in self.keys():
245 raise KeyError("No such diff key, available: %s" % self.diffs )
245 raise KeyError("No such diff key, available: %s" % self.diffs )
246 return getattr(self.result_column, key)
246 return getattr(self.result_column, key)
247
247
248 def __setitem__(self, key, value):
248 def __setitem__(self, key, value):
249 if key not in self.keys():
249 if key not in self.keys():
250 raise KeyError("No such diff key, available: %s" % self.diffs )
250 raise KeyError("No such diff key, available: %s" % self.diffs )
251 setattr(self.result_column, key, value)
251 setattr(self.result_column, key, value)
252
252
253 def __delitem__(self, key):
253 def __delitem__(self, key):
254 raise NotImplementedError
254 raise NotImplementedError
255
255
256 def keys(self):
256 def keys(self):
257 return self.diffs.keys()
257 return self.diffs.keys()
258
258
259 def compare_parameters(self, current_name, *p, **k):
259 def compare_parameters(self, current_name, *p, **k):
260 """Compares Column objects with reflection"""
260 """Compares Column objects with reflection"""
261 self.table = k.pop('table')
261 self.table = k.pop('table')
262 self.result_column = self._table.c.get(current_name)
262 self.result_column = self._table.c.get(current_name)
263 if len(p):
263 if len(p):
264 k = self._extract_parameters(p, k, self.result_column)
264 k = self._extract_parameters(p, k, self.result_column)
265 return k
265 return k
266
266
267 def compare_1_column(self, col, *p, **k):
267 def compare_1_column(self, col, *p, **k):
268 """Compares one Column object"""
268 """Compares one Column object"""
269 self.table = k.pop('table', None)
269 self.table = k.pop('table', None)
270 if self.table is None:
270 if self.table is None:
271 self.table = col.table
271 self.table = col.table
272 self.result_column = col
272 self.result_column = col
273 if len(p):
273 if len(p):
274 k = self._extract_parameters(p, k, self.result_column)
274 k = self._extract_parameters(p, k, self.result_column)
275 return k
275 return k
276
276
277 def compare_2_columns(self, old_col, new_col, *p, **k):
277 def compare_2_columns(self, old_col, new_col, *p, **k):
278 """Compares two Column objects"""
278 """Compares two Column objects"""
279 self.process_column(new_col)
279 self.process_column(new_col)
280 self.table = k.pop('table', None)
280 self.table = k.pop('table', None)
281 # we cannot use bool() on table in SA06
281 # we cannot use bool() on table in SA06
282 if self.table is None:
282 if self.table is None:
283 self.table = old_col.table
283 self.table = old_col.table
284 if self.table is None:
284 if self.table is None:
285 new_col.table
285 new_col.table
286 self.result_column = old_col
286 self.result_column = old_col
287
287
288 # set differences
288 # set differences
289 # leave out some stuff for later comp
289 # leave out some stuff for later comp
290 for key in (set(self.diff_keys) - set(('type',))):
290 for key in (set(self.diff_keys) - set(('type',))):
291 val = getattr(new_col, key, None)
291 val = getattr(new_col, key, None)
292 if getattr(self.result_column, key, None) != val:
292 if getattr(self.result_column, key, None) != val:
293 k.setdefault(key, val)
293 k.setdefault(key, val)
294
294
295 # inspect types
295 # inspect types
296 if not self.are_column_types_eq(self.result_column.type, new_col.type):
296 if not self.are_column_types_eq(self.result_column.type, new_col.type):
297 k.setdefault('type', new_col.type)
297 k.setdefault('type', new_col.type)
298
298
299 if len(p):
299 if len(p):
300 k = self._extract_parameters(p, k, self.result_column)
300 k = self._extract_parameters(p, k, self.result_column)
301 return k
301 return k
302
302
303 def apply_diffs(self, diffs):
303 def apply_diffs(self, diffs):
304 """Populate dict and column object with new values"""
304 """Populate dict and column object with new values"""
305 self.diffs = diffs
305 self.diffs = diffs
306 for key in self.diff_keys:
306 for key in self.diff_keys:
307 if key in diffs:
307 if key in diffs:
308 setattr(self.result_column, key, diffs[key])
308 setattr(self.result_column, key, diffs[key])
309
309
310 self.process_column(self.result_column)
310 self.process_column(self.result_column)
311
311
312 # create an instance of class type if not yet
312 # create an instance of class type if not yet
313 if 'type' in diffs and callable(self.result_column.type):
313 if 'type' in diffs and callable(self.result_column.type):
314 self.result_column.type = self.result_column.type()
314 self.result_column.type = self.result_column.type()
315
315
316 # add column to the table
316 # add column to the table
317 if self.table is not None and self.alter_metadata:
317 if self.table is not None and self.alter_metadata:
318 self.result_column.add_to_table(self.table)
318 self.result_column.add_to_table(self.table)
319
319
320 def are_column_types_eq(self, old_type, new_type):
320 def are_column_types_eq(self, old_type, new_type):
321 """Compares two types to be equal"""
321 """Compares two types to be equal"""
322 ret = old_type.__class__ == new_type.__class__
322 ret = old_type.__class__ == new_type.__class__
323
323
324 # String length is a special case
324 # String length is a special case
325 if ret and isinstance(new_type, sqlalchemy.types.String):
325 if ret and isinstance(new_type, sqlalchemy.types.String):
326 ret = (getattr(old_type, 'length', None) == \
326 ret = (getattr(old_type, 'length', None) == \
327 getattr(new_type, 'length', None))
327 getattr(new_type, 'length', None))
328 return ret
328 return ret
329
329
330 def _extract_parameters(self, p, k, column):
330 def _extract_parameters(self, p, k, column):
331 """Extracts data from p and modifies diffs"""
331 """Extracts data from p and modifies diffs"""
332 p = list(p)
332 p = list(p)
333 while len(p):
333 while len(p):
334 if isinstance(p[0], basestring):
334 if isinstance(p[0], basestring):
335 k.setdefault('name', p.pop(0))
335 k.setdefault('name', p.pop(0))
336 elif isinstance(p[0], sqlalchemy.types.AbstractType):
336 elif isinstance(p[0], sqlalchemy.types.AbstractType):
337 k.setdefault('type', p.pop(0))
337 k.setdefault('type', p.pop(0))
338 elif callable(p[0]):
338 elif callable(p[0]):
339 p[0] = p[0]()
339 p[0] = p[0]()
340 else:
340 else:
341 break
341 break
342
342
343 if len(p):
343 if len(p):
344 new_col = column.copy_fixed()
344 new_col = column.copy_fixed()
345 new_col._init_items(*p)
345 new_col._init_items(*p)
346 k = self.compare_2_columns(column, new_col, **k)
346 k = self.compare_2_columns(column, new_col, **k)
347 return k
347 return k
348
348
349 def process_column(self, column):
349 def process_column(self, column):
350 """Processes default values for column"""
350 """Processes default values for column"""
351 # XXX: this is a snippet from SA processing of positional parameters
351 # XXX: this is a snippet from SA processing of positional parameters
352 if not SQLA_06 and column.args:
352 if not SQLA_06 and column.args:
353 toinit = list(column.args)
353 toinit = list(column.args)
354 else:
354 else:
355 toinit = list()
355 toinit = list()
356
356
357 if column.server_default is not None:
357 if column.server_default is not None:
358 if isinstance(column.server_default, sqlalchemy.FetchedValue):
358 if isinstance(column.server_default, sqlalchemy.FetchedValue):
359 toinit.append(column.server_default)
359 toinit.append(column.server_default)
360 else:
360 else:
361 toinit.append(sqlalchemy.DefaultClause(column.server_default))
361 toinit.append(sqlalchemy.DefaultClause(column.server_default))
362 if column.server_onupdate is not None:
362 if column.server_onupdate is not None:
363 if isinstance(column.server_onupdate, FetchedValue):
363 if isinstance(column.server_onupdate, FetchedValue):
364 toinit.append(column.server_default)
364 toinit.append(column.server_default)
365 else:
365 else:
366 toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
366 toinit.append(sqlalchemy.DefaultClause(column.server_onupdate,
367 for_update=True))
367 for_update=True))
368 if toinit:
368 if toinit:
369 column._init_items(*toinit)
369 column._init_items(*toinit)
370
370
371 if not SQLA_06:
371 if not SQLA_06:
372 column.args = []
372 column.args = []
373
373
374 def _get_table(self):
374 def _get_table(self):
375 return getattr(self, '_table', None)
375 return getattr(self, '_table', None)
376
376
377 def _set_table(self, table):
377 def _set_table(self, table):
378 if isinstance(table, basestring):
378 if isinstance(table, basestring):
379 if self.alter_metadata:
379 if self.alter_metadata:
380 if not self.meta:
380 if not self.meta:
381 raise ValueError("metadata must be specified for table"
381 raise ValueError("metadata must be specified for table"
382 " reflection when using alter_metadata")
382 " reflection when using alter_metadata")
383 meta = self.meta
383 meta = self.meta
384 if self.engine:
384 if self.engine:
385 meta.bind = self.engine
385 meta.bind = self.engine
386 else:
386 else:
387 if not self.engine and not self.meta:
387 if not self.engine and not self.meta:
388 raise ValueError("engine or metadata must be specified"
388 raise ValueError("engine or metadata must be specified"
389 " to reflect tables")
389 " to reflect tables")
390 if not self.engine:
390 if not self.engine:
391 self.engine = self.meta.bind
391 self.engine = self.meta.bind
392 meta = sqlalchemy.MetaData(bind=self.engine)
392 meta = sqlalchemy.MetaData(bind=self.engine)
393 self._table = sqlalchemy.Table(table, meta, autoload=True)
393 self._table = sqlalchemy.Table(table, meta, autoload=True)
394 elif isinstance(table, sqlalchemy.Table):
394 elif isinstance(table, sqlalchemy.Table):
395 self._table = table
395 self._table = table
396 if not self.alter_metadata:
396 if not self.alter_metadata:
397 self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
397 self._table.meta = sqlalchemy.MetaData(bind=self._table.bind)
398 def _get_result_column(self):
398 def _get_result_column(self):
399 return getattr(self, '_result_column', None)
399 return getattr(self, '_result_column', None)
400
400
401 def _set_result_column(self, column):
401 def _set_result_column(self, column):
402 """Set Column to Table based on alter_metadata evaluation."""
402 """Set Column to Table based on alter_metadata evaluation."""
403 self.process_column(column)
403 self.process_column(column)
404 if not hasattr(self, 'current_name'):
404 if not hasattr(self, 'current_name'):
405 self.current_name = column.name
405 self.current_name = column.name
406 if self.alter_metadata:
406 if self.alter_metadata:
407 self._result_column = column
407 self._result_column = column
408 else:
408 else:
409 self._result_column = column.copy_fixed()
409 self._result_column = column.copy_fixed()
410
410
411 table = property(_get_table, _set_table)
411 table = property(_get_table, _set_table)
412 result_column = property(_get_result_column, _set_result_column)
412 result_column = property(_get_result_column, _set_result_column)
413
413
414
414
415 class ChangesetTable(object):
415 class ChangesetTable(object):
416 """Changeset extensions to SQLAlchemy tables."""
416 """Changeset extensions to SQLAlchemy tables."""
417
417
418 def create_column(self, column, *p, **kw):
418 def create_column(self, column, *p, **kw):
419 """Creates a column.
419 """Creates a column.
420
420
421 The column parameter may be a column definition or the name of
421 The column parameter may be a column definition or the name of
422 a column in this table.
422 a column in this table.
423
423
424 API to :meth:`ChangesetColumn.create`
424 API to :meth:`ChangesetColumn.create`
425
425
426 :param column: Column to be created
426 :param column: Column to be created
427 :type column: Column instance or string
427 :type column: Column instance or string
428 """
428 """
429 if not isinstance(column, sqlalchemy.Column):
429 if not isinstance(column, sqlalchemy.Column):
430 # It's a column name
430 # It's a column name
431 column = getattr(self.c, str(column))
431 column = getattr(self.c, str(column))
432 column.create(table=self, *p, **kw)
432 column.create(table=self, *p, **kw)
433
433
434 def drop_column(self, column, *p, **kw):
434 def drop_column(self, column, *p, **kw):
435 """Drop a column, given its name or definition.
435 """Drop a column, given its name or definition.
436
436
437 API to :meth:`ChangesetColumn.drop`
437 API to :meth:`ChangesetColumn.drop`
438
438
439 :param column: Column to be droped
439 :param column: Column to be droped
440 :type column: Column instance or string
440 :type column: Column instance or string
441 """
441 """
442 if not isinstance(column, sqlalchemy.Column):
442 if not isinstance(column, sqlalchemy.Column):
443 # It's a column name
443 # It's a column name
444 try:
444 try:
445 column = getattr(self.c, str(column))
445 column = getattr(self.c, str(column))
446 except AttributeError:
446 except AttributeError:
447 # That column isn't part of the table. We don't need
447 # That column isn't part of the table. We don't need
448 # its entire definition to drop the column, just its
448 # its entire definition to drop the column, just its
449 # name, so create a dummy column with the same name.
449 # name, so create a dummy column with the same name.
450 column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
450 column = sqlalchemy.Column(str(column), sqlalchemy.Integer())
451 column.drop(table=self, *p, **kw)
451 column.drop(table=self, *p, **kw)
452
452
453 def rename(self, name, connection=None, **kwargs):
453 def rename(self, name, connection=None, **kwargs):
454 """Rename this table.
454 """Rename this table.
455
455
456 :param name: New name of the table.
456 :param name: New name of the table.
457 :type name: string
457 :type name: string
458 :param connection: reuse connection istead of creating new one.
458 :param connection: reuse connection istead of creating new one.
459 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
459 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
460 """
460 """
461 engine = self.bind
461 engine = self.bind
462 self.new_name = name
462 self.new_name = name
463 visitorcallable = get_engine_visitor(engine, 'schemachanger')
463 visitorcallable = get_engine_visitor(engine, 'schemachanger')
464 run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
464 run_single_visitor(engine, visitorcallable, self, connection, **kwargs)
465
465
466 # Fix metadata registration
466 # Fix metadata registration
467 self.name = name
467 self.name = name
468 self.deregister()
468 self.deregister()
469 self._set_parent(self.metadata)
469 self._set_parent(self.metadata)
470
470
471 def _meta_key(self):
471 def _meta_key(self):
472 return sqlalchemy.schema._get_table_key(self.name, self.schema)
472 return sqlalchemy.schema._get_table_key(self.name, self.schema)
473
473
474 def deregister(self):
474 def deregister(self):
475 """Remove this table from its metadata"""
475 """Remove this table from its metadata"""
476 key = self._meta_key()
476 key = self._meta_key()
477 meta = self.metadata
477 meta = self.metadata
478 if key in meta.tables:
478 if key in meta.tables:
479 del meta.tables[key]
479 del meta.tables[key]
480
480
481
481
482 class ChangesetColumn(object):
482 class ChangesetColumn(object):
483 """Changeset extensions to SQLAlchemy columns."""
483 """Changeset extensions to SQLAlchemy columns."""
484
484
485 def alter(self, *p, **k):
485 def alter(self, *p, **k):
486 """Makes a call to :func:`alter_column` for the column this
486 """Makes a call to :func:`alter_column` for the column this
487 method is called on.
487 method is called on.
488 """
488 """
489 if 'table' not in k:
489 if 'table' not in k:
490 k['table'] = self.table
490 k['table'] = self.table
491 if 'engine' not in k:
491 if 'engine' not in k:
492 k['engine'] = k['table'].bind
492 k['engine'] = k['table'].bind
493 return alter_column(self, *p, **k)
493 return alter_column(self, *p, **k)
494
494
495 def create(self, table=None, index_name=None, unique_name=None,
495 def create(self, table=None, index_name=None, unique_name=None,
496 primary_key_name=None, populate_default=True, connection=None, **kwargs):
496 primary_key_name=None, populate_default=True, connection=None, **kwargs):
497 """Create this column in the database.
497 """Create this column in the database.
498
498
499 Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
499 Assumes the given table exists. ``ALTER TABLE ADD COLUMN``,
500 for most databases.
500 for most databases.
501
501
502 :param table: Table instance to create on.
502 :param table: Table instance to create on.
503 :param index_name: Creates :class:`ChangesetIndex` on this column.
503 :param index_name: Creates :class:`ChangesetIndex` on this column.
504 :param unique_name: Creates :class:\
504 :param unique_name: Creates :class:\
505 `~migrate.changeset.constraint.UniqueConstraint` on this column.
505 `~migrate.changeset.constraint.UniqueConstraint` on this column.
506 :param primary_key_name: Creates :class:\
506 :param primary_key_name: Creates :class:\
507 `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column.
507 `~migrate.changeset.constraint.PrimaryKeyConstraint` on this column.
508 :param populate_default: If True, created column will be \
508 :param populate_default: If True, created column will be \
509 populated with defaults
509 populated with defaults
510 :param connection: reuse connection istead of creating new one.
510 :param connection: reuse connection istead of creating new one.
511 :type table: Table instance
511 :type table: Table instance
512 :type index_name: string
512 :type index_name: string
513 :type unique_name: string
513 :type unique_name: string
514 :type primary_key_name: string
514 :type primary_key_name: string
515 :type populate_default: bool
515 :type populate_default: bool
516 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
516 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
517
517
518 :returns: self
518 :returns: self
519 """
519 """
520 self.populate_default = populate_default
520 self.populate_default = populate_default
521 self.index_name = index_name
521 self.index_name = index_name
522 self.unique_name = unique_name
522 self.unique_name = unique_name
523 self.primary_key_name = primary_key_name
523 self.primary_key_name = primary_key_name
524 for cons in ('index_name', 'unique_name', 'primary_key_name'):
524 for cons in ('index_name', 'unique_name', 'primary_key_name'):
525 self._check_sanity_constraints(cons)
525 self._check_sanity_constraints(cons)
526
526
527 self.add_to_table(table)
527 self.add_to_table(table)
528 engine = self.table.bind
528 engine = self.table.bind
529 visitorcallable = get_engine_visitor(engine, 'columngenerator')
529 visitorcallable = get_engine_visitor(engine, 'columngenerator')
530 engine._run_visitor(visitorcallable, self, connection, **kwargs)
530 engine._run_visitor(visitorcallable, self, connection, **kwargs)
531
531
532 # TODO: reuse existing connection
532 # TODO: reuse existing connection
533 if self.populate_default and self.default is not None:
533 if self.populate_default and self.default is not None:
534 stmt = table.update().values({self: engine._execute_default(self.default)})
534 stmt = table.update().values({self: engine._execute_default(self.default)})
535 engine.execute(stmt)
535 engine.execute(stmt)
536
536
537 return self
537 return self
538
538
539 def drop(self, table=None, connection=None, **kwargs):
539 def drop(self, table=None, connection=None, **kwargs):
540 """Drop this column from the database, leaving its table intact.
540 """Drop this column from the database, leaving its table intact.
541
541
542 ``ALTER TABLE DROP COLUMN``, for most databases.
542 ``ALTER TABLE DROP COLUMN``, for most databases.
543
543
544 :param connection: reuse connection istead of creating new one.
544 :param connection: reuse connection istead of creating new one.
545 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
545 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
546 """
546 """
547 if table is not None:
547 if table is not None:
548 self.table = table
548 self.table = table
549 engine = self.table.bind
549 engine = self.table.bind
550 visitorcallable = get_engine_visitor(engine, 'columndropper')
550 visitorcallable = get_engine_visitor(engine, 'columndropper')
551 engine._run_visitor(visitorcallable, self, connection, **kwargs)
551 engine._run_visitor(visitorcallable, self, connection, **kwargs)
552 self.remove_from_table(self.table, unset_table=False)
552 self.remove_from_table(self.table, unset_table=False)
553 self.table = None
553 self.table = None
554 return self
554 return self
555
555
556 def add_to_table(self, table):
556 def add_to_table(self, table):
557 if table is not None and self.table is None:
557 if table is not None and self.table is None:
558 self._set_parent(table)
558 if SQLA_07:
559 table.append_column(self)
560 else:
561 self._set_parent(table)
559
562
560 def _col_name_in_constraint(self,cons,name):
563 def _col_name_in_constraint(self,cons,name):
561 return False
564 return False
562
565
563 def remove_from_table(self, table, unset_table=True):
566 def remove_from_table(self, table, unset_table=True):
564 # TODO: remove primary keys, constraints, etc
567 # TODO: remove primary keys, constraints, etc
565 if unset_table:
568 if unset_table:
566 self.table = None
569 self.table = None
567
570
568 to_drop = set()
571 to_drop = set()
569 for index in table.indexes:
572 for index in table.indexes:
570 columns = []
573 columns = []
571 for col in index.columns:
574 for col in index.columns:
572 if col.name!=self.name:
575 if col.name!=self.name:
573 columns.append(col)
576 columns.append(col)
574 if columns:
577 if columns:
575 index.columns=columns
578 index.columns=columns
576 else:
579 else:
577 to_drop.add(index)
580 to_drop.add(index)
578 table.indexes = table.indexes - to_drop
581 table.indexes = table.indexes - to_drop
579
582
580 to_drop = set()
583 to_drop = set()
581 for cons in table.constraints:
584 for cons in table.constraints:
582 # TODO: deal with other types of constraint
585 # TODO: deal with other types of constraint
583 if isinstance(cons,(ForeignKeyConstraint,
586 if isinstance(cons,(ForeignKeyConstraint,
584 UniqueConstraint)):
587 UniqueConstraint)):
585 for col_name in cons.columns:
588 for col_name in cons.columns:
586 if not isinstance(col_name,basestring):
589 if not isinstance(col_name,basestring):
587 col_name = col_name.name
590 col_name = col_name.name
588 if self.name==col_name:
591 if self.name==col_name:
589 to_drop.add(cons)
592 to_drop.add(cons)
590 table.constraints = table.constraints - to_drop
593 table.constraints = table.constraints - to_drop
591
594
592 if table.c.contains_column(self):
595 if table.c.contains_column(self):
593 table.c.remove(self)
596 if SQLA_07:
597 table._columns.remove(self)
598 else:
599 table.c.remove(self)
594
600
595 # TODO: this is fixed in 0.6
601 # TODO: this is fixed in 0.6
596 def copy_fixed(self, **kw):
602 def copy_fixed(self, **kw):
597 """Create a copy of this ``Column``, with all attributes."""
603 """Create a copy of this ``Column``, with all attributes."""
598 return sqlalchemy.Column(self.name, self.type, self.default,
604 return sqlalchemy.Column(self.name, self.type, self.default,
599 key=self.key,
605 key=self.key,
600 primary_key=self.primary_key,
606 primary_key=self.primary_key,
601 nullable=self.nullable,
607 nullable=self.nullable,
602 quote=self.quote,
608 quote=self.quote,
603 index=self.index,
609 index=self.index,
604 unique=self.unique,
610 unique=self.unique,
605 onupdate=self.onupdate,
611 onupdate=self.onupdate,
606 autoincrement=self.autoincrement,
612 autoincrement=self.autoincrement,
607 server_default=self.server_default,
613 server_default=self.server_default,
608 server_onupdate=self.server_onupdate,
614 server_onupdate=self.server_onupdate,
609 *[c.copy(**kw) for c in self.constraints])
615 *[c.copy(**kw) for c in self.constraints])
610
616
611 def _check_sanity_constraints(self, name):
617 def _check_sanity_constraints(self, name):
612 """Check if constraints names are correct"""
618 """Check if constraints names are correct"""
613 obj = getattr(self, name)
619 obj = getattr(self, name)
614 if (getattr(self, name[:-5]) and not obj):
620 if (getattr(self, name[:-5]) and not obj):
615 raise InvalidConstraintError("Column.create() accepts index_name,"
621 raise InvalidConstraintError("Column.create() accepts index_name,"
616 " primary_key_name and unique_name to generate constraints")
622 " primary_key_name and unique_name to generate constraints")
617 if not isinstance(obj, basestring) and obj is not None:
623 if not isinstance(obj, basestring) and obj is not None:
618 raise InvalidConstraintError(
624 raise InvalidConstraintError(
619 "%s argument for column must be constraint name" % name)
625 "%s argument for column must be constraint name" % name)
620
626
621
627
622 class ChangesetIndex(object):
628 class ChangesetIndex(object):
623 """Changeset extensions to SQLAlchemy Indexes."""
629 """Changeset extensions to SQLAlchemy Indexes."""
624
630
625 __visit_name__ = 'index'
631 __visit_name__ = 'index'
626
632
627 def rename(self, name, connection=None, **kwargs):
633 def rename(self, name, connection=None, **kwargs):
628 """Change the name of an index.
634 """Change the name of an index.
629
635
630 :param name: New name of the Index.
636 :param name: New name of the Index.
631 :type name: string
637 :type name: string
632 :param connection: reuse connection istead of creating new one.
638 :param connection: reuse connection istead of creating new one.
633 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
639 :type connection: :class:`sqlalchemy.engine.base.Connection` instance
634 """
640 """
635 engine = self.table.bind
641 engine = self.table.bind
636 self.new_name = name
642 self.new_name = name
637 visitorcallable = get_engine_visitor(engine, 'schemachanger')
643 visitorcallable = get_engine_visitor(engine, 'schemachanger')
638 engine._run_visitor(visitorcallable, self, connection, **kwargs)
644 engine._run_visitor(visitorcallable, self, connection, **kwargs)
639 self.name = name
645 self.name = name
640
646
641
647
642 class ChangesetDefaultClause(object):
648 class ChangesetDefaultClause(object):
643 """Implements comparison between :class:`DefaultClause` instances"""
649 """Implements comparison between :class:`DefaultClause` instances"""
644
650
645 def __eq__(self, other):
651 def __eq__(self, other):
646 if isinstance(other, self.__class__):
652 if isinstance(other, self.__class__):
647 if self.arg == other.arg:
653 if self.arg == other.arg:
648 return True
654 return True
649
655
650 def __ne__(self, other):
656 def __ne__(self, other):
651 return not self.__eq__(other)
657 return not self.__eq__(other)
@@ -1,83 +1,88 b''
1 """
1 """
2 Provide exception classes for :mod:`migrate`
2 Provide exception classes for :mod:`migrate`
3 """
3 """
4
4
5
5
6 class Error(Exception):
6 class Error(Exception):
7 """Error base class."""
7 """Error base class."""
8
8
9
9
10 class ApiError(Error):
10 class ApiError(Error):
11 """Base class for API errors."""
11 """Base class for API errors."""
12
12
13
13
14 class KnownError(ApiError):
14 class KnownError(ApiError):
15 """A known error condition."""
15 """A known error condition."""
16
16
17
17
18 class UsageError(ApiError):
18 class UsageError(ApiError):
19 """A known error condition where help should be displayed."""
19 """A known error condition where help should be displayed."""
20
20
21
21
22 class ControlledSchemaError(Error):
22 class ControlledSchemaError(Error):
23 """Base class for controlled schema errors."""
23 """Base class for controlled schema errors."""
24
24
25
25
26 class InvalidVersionError(ControlledSchemaError):
26 class InvalidVersionError(ControlledSchemaError):
27 """Invalid version number."""
27 """Invalid version number."""
28
28
29
29
30 class DatabaseNotControlledError(ControlledSchemaError):
30 class DatabaseNotControlledError(ControlledSchemaError):
31 """Database should be under version control, but it's not."""
31 """Database should be under version control, but it's not."""
32
32
33
33
34 class DatabaseAlreadyControlledError(ControlledSchemaError):
34 class DatabaseAlreadyControlledError(ControlledSchemaError):
35 """Database shouldn't be under version control, but it is"""
35 """Database shouldn't be under version control, but it is"""
36
36
37
37
38 class WrongRepositoryError(ControlledSchemaError):
38 class WrongRepositoryError(ControlledSchemaError):
39 """This database is under version control by another repository."""
39 """This database is under version control by another repository."""
40
40
41
41
42 class NoSuchTableError(ControlledSchemaError):
42 class NoSuchTableError(ControlledSchemaError):
43 """The table does not exist."""
43 """The table does not exist."""
44
44
45
45
46 class PathError(Error):
46 class PathError(Error):
47 """Base class for path errors."""
47 """Base class for path errors."""
48
48
49
49
50 class PathNotFoundError(PathError):
50 class PathNotFoundError(PathError):
51 """A path with no file was required; found a file."""
51 """A path with no file was required; found a file."""
52
52
53
53
54 class PathFoundError(PathError):
54 class PathFoundError(PathError):
55 """A path with a file was required; found no file."""
55 """A path with a file was required; found no file."""
56
56
57
57
58 class RepositoryError(Error):
58 class RepositoryError(Error):
59 """Base class for repository errors."""
59 """Base class for repository errors."""
60
60
61
61
62 class InvalidRepositoryError(RepositoryError):
62 class InvalidRepositoryError(RepositoryError):
63 """Invalid repository error."""
63 """Invalid repository error."""
64
64
65
65
66 class ScriptError(Error):
66 class ScriptError(Error):
67 """Base class for script errors."""
67 """Base class for script errors."""
68
68
69
69
70 class InvalidScriptError(ScriptError):
70 class InvalidScriptError(ScriptError):
71 """Invalid script error."""
71 """Invalid script error."""
72
72
73
73
74 class InvalidVersionError(Error):
75 """Invalid version error."""
76
77 # migrate.changeset
78
74 class NotSupportedError(Error):
79 class NotSupportedError(Error):
75 """Not supported error"""
80 """Not supported error"""
76
81
77
82
78 class InvalidConstraintError(Error):
83 class InvalidConstraintError(Error):
79 """Invalid constraint error"""
84 """Invalid constraint error"""
80
85
81
86
82 class MigrateDeprecationWarning(DeprecationWarning):
87 class MigrateDeprecationWarning(DeprecationWarning):
83 """Warning for deprecated features in Migrate"""
88 """Warning for deprecated features in Migrate"""
@@ -1,383 +1,383 b''
1 """
1 """
2 This module provides an external API to the versioning system.
2 This module provides an external API to the versioning system.
3
3
4 .. versionchanged:: 0.6.0
4 .. versionchanged:: 0.6.0
5 :func:`migrate.versioning.api.test` and schema diff functions
5 :func:`migrate.versioning.api.test` and schema diff functions
6 changed order of positional arguments so all accept `url` and `repository`
6 changed order of positional arguments so all accept `url` and `repository`
7 as first arguments.
7 as first arguments.
8
8
9 .. versionchanged:: 0.5.4
9 .. versionchanged:: 0.5.4
10 ``--preview_sql`` displays source file when using SQL scripts.
10 ``--preview_sql`` displays source file when using SQL scripts.
11 If Python script is used, it runs the action with mocked engine and
11 If Python script is used, it runs the action with mocked engine and
12 returns captured SQL statements.
12 returns captured SQL statements.
13
13
14 .. versionchanged:: 0.5.4
14 .. versionchanged:: 0.5.4
15 Deprecated ``--echo`` parameter in favour of new
15 Deprecated ``--echo`` parameter in favour of new
16 :func:`migrate.versioning.util.construct_engine` behavior.
16 :func:`migrate.versioning.util.construct_engine` behavior.
17 """
17 """
18
18
19 # Dear migrate developers,
19 # Dear migrate developers,
20 #
20 #
21 # please do not comment this module using sphinx syntax because its
21 # please do not comment this module using sphinx syntax because its
22 # docstrings are presented as user help and most users cannot
22 # docstrings are presented as user help and most users cannot
23 # interpret sphinx annotated ReStructuredText.
23 # interpret sphinx annotated ReStructuredText.
24 #
24 #
25 # Thanks,
25 # Thanks,
26 # Jan Dittberner
26 # Jan Dittberner
27
27
28 import sys
28 import sys
29 import inspect
29 import inspect
30 import logging
30 import logging
31
31
32 from rhodecode.lib.dbmigrate.migrate import exceptions
32 from rhodecode.lib.dbmigrate.migrate import exceptions
33 from rhodecode.lib.dbmigrate.migrate.versioning import repository, schema, version, \
33 from rhodecode.lib.dbmigrate.migrate.versioning import repository, schema, version, \
34 script as script_ # command name conflict
34 script as script_ # command name conflict
35 from rhodecode.lib.dbmigrate.migrate.versioning.util import catch_known_errors, with_engine
35 from rhodecode.lib.dbmigrate.migrate.versioning.util import catch_known_errors, with_engine
36
36
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39 command_desc = {
39 command_desc = {
40 'help': 'displays help on a given command',
40 'help': 'displays help on a given command',
41 'create': 'create an empty repository at the specified path',
41 'create': 'create an empty repository at the specified path',
42 'script': 'create an empty change Python script',
42 'script': 'create an empty change Python script',
43 'script_sql': 'create empty change SQL scripts for given database',
43 'script_sql': 'create empty change SQL scripts for given database',
44 'version': 'display the latest version available in a repository',
44 'version': 'display the latest version available in a repository',
45 'db_version': 'show the current version of the repository under version control',
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',
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',
47 'version_control': 'mark a database as under this repository\'s version control',
48 'upgrade': 'upgrade a database to a later version',
48 'upgrade': 'upgrade a database to a later version',
49 'downgrade': 'downgrade a database to an earlier version',
49 'downgrade': 'downgrade a database to an earlier version',
50 'drop_version_control': 'removes version control from a database',
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',
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',
52 'test': 'performs the upgrade and downgrade command on the given database',
53 'compare_model_to_db': 'compare MetaData against the current database state',
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',
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',
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',
56 'update_db_from_model': 'modify the database to match the structure of the current MetaData',
57 }
57 }
58 __all__ = command_desc.keys()
58 __all__ = command_desc.keys()
59
59
60 Repository = repository.Repository
60 Repository = repository.Repository
61 ControlledSchema = schema.ControlledSchema
61 ControlledSchema = schema.ControlledSchema
62 VerNum = version.VerNum
62 VerNum = version.VerNum
63 PythonScript = script_.PythonScript
63 PythonScript = script_.PythonScript
64 SqlScript = script_.SqlScript
64 SqlScript = script_.SqlScript
65
65
66
66
67 # deprecated
67 # deprecated
68 def help(cmd=None, **opts):
68 def help(cmd=None, **opts):
69 """%prog help COMMAND
69 """%prog help COMMAND
70
70
71 Displays help on a given command.
71 Displays help on a given command.
72 """
72 """
73 if cmd is None:
73 if cmd is None:
74 raise exceptions.UsageError(None)
74 raise exceptions.UsageError(None)
75 try:
75 try:
76 func = globals()[cmd]
76 func = globals()[cmd]
77 except:
77 except:
78 raise exceptions.UsageError(
78 raise exceptions.UsageError(
79 "'%s' isn't a valid command. Try 'help COMMAND'" % cmd)
79 "'%s' isn't a valid command. Try 'help COMMAND'" % cmd)
80 ret = func.__doc__
80 ret = func.__doc__
81 if sys.argv[0]:
81 if sys.argv[0]:
82 ret = ret.replace('%prog', sys.argv[0])
82 ret = ret.replace('%prog', sys.argv[0])
83 return ret
83 return ret
84
84
85 @catch_known_errors
85 @catch_known_errors
86 def create(repository, name, **opts):
86 def create(repository, name, **opts):
87 """%prog create REPOSITORY_PATH NAME [--table=TABLE]
87 """%prog create REPOSITORY_PATH NAME [--table=TABLE]
88
88
89 Create an empty repository at the specified path.
89 Create an empty repository at the specified path.
90
90
91 You can specify the version_table to be used; by default, it is
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
92 'migrate_version'. This table is created in all version-controlled
93 databases.
93 databases.
94 """
94 """
95 repo_path = Repository.create(repository, name, **opts)
95 repo_path = Repository.create(repository, name, **opts)
96
96
97
97
98 @catch_known_errors
98 @catch_known_errors
99 def script(description, repository, **opts):
99 def script(description, repository, **opts):
100 """%prog script DESCRIPTION REPOSITORY_PATH
100 """%prog script DESCRIPTION REPOSITORY_PATH
101
101
102 Create an empty change script using the next unused version number
102 Create an empty change script using the next unused version number
103 appended with the given description.
103 appended with the given description.
104
104
105 For instance, manage.py script "Add initial tables" creates:
105 For instance, manage.py script "Add initial tables" creates:
106 repository/versions/001_Add_initial_tables.py
106 repository/versions/001_Add_initial_tables.py
107 """
107 """
108 repo = Repository(repository)
108 repo = Repository(repository)
109 repo.create_script(description, **opts)
109 repo.create_script(description, **opts)
110
110
111
111
112 @catch_known_errors
112 @catch_known_errors
113 def script_sql(database, repository, **opts):
113 def script_sql(database, description, repository, **opts):
114 """%prog script_sql DATABASE REPOSITORY_PATH
114 """%prog script_sql DATABASE DESCRIPTION REPOSITORY_PATH
115
115
116 Create empty change SQL scripts for given DATABASE, where DATABASE
116 Create empty change SQL scripts for given DATABASE, where DATABASE
117 is either specific ('postgres', 'mysql', 'oracle', 'sqlite', etc.)
117 is either specific ('postgresql', 'mysql', 'oracle', 'sqlite', etc.)
118 or generic ('default').
118 or generic ('default').
119
119
120 For instance, manage.py script_sql postgres creates:
120 For instance, manage.py script_sql postgresql description creates:
121 repository/versions/001_postgres_upgrade.sql and
121 repository/versions/001_description_postgresql_upgrade.sql and
122 repository/versions/001_postgres_postgres.sql
122 repository/versions/001_description_postgresql_postgres.sql
123 """
123 """
124 repo = Repository(repository)
124 repo = Repository(repository)
125 repo.create_script_sql(database, **opts)
125 repo.create_script_sql(database, description, **opts)
126
126
127
127
128 def version(repository, **opts):
128 def version(repository, **opts):
129 """%prog version REPOSITORY_PATH
129 """%prog version REPOSITORY_PATH
130
130
131 Display the latest version available in a repository.
131 Display the latest version available in a repository.
132 """
132 """
133 repo = Repository(repository)
133 repo = Repository(repository)
134 return repo.latest
134 return repo.latest
135
135
136
136
137 @with_engine
137 @with_engine
138 def db_version(url, repository, **opts):
138 def db_version(url, repository, **opts):
139 """%prog db_version URL REPOSITORY_PATH
139 """%prog db_version URL REPOSITORY_PATH
140
140
141 Show the current version of the repository with the given
141 Show the current version of the repository with the given
142 connection string, under version control of the specified
142 connection string, under version control of the specified
143 repository.
143 repository.
144
144
145 The url should be any valid SQLAlchemy connection string.
145 The url should be any valid SQLAlchemy connection string.
146 """
146 """
147 engine = opts.pop('engine')
147 engine = opts.pop('engine')
148 schema = ControlledSchema(engine, repository)
148 schema = ControlledSchema(engine, repository)
149 return schema.version
149 return schema.version
150
150
151
151
152 def source(version, dest=None, repository=None, **opts):
152 def source(version, dest=None, repository=None, **opts):
153 """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH
153 """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH
154
154
155 Display the Python code for a particular version in this
155 Display the Python code for a particular version in this
156 repository. Save it to the file at DESTINATION or, if omitted,
156 repository. Save it to the file at DESTINATION or, if omitted,
157 send to stdout.
157 send to stdout.
158 """
158 """
159 if repository is None:
159 if repository is None:
160 raise exceptions.UsageError("A repository must be specified")
160 raise exceptions.UsageError("A repository must be specified")
161 repo = Repository(repository)
161 repo = Repository(repository)
162 ret = repo.version(version).script().source()
162 ret = repo.version(version).script().source()
163 if dest is not None:
163 if dest is not None:
164 dest = open(dest, 'w')
164 dest = open(dest, 'w')
165 dest.write(ret)
165 dest.write(ret)
166 dest.close()
166 dest.close()
167 ret = None
167 ret = None
168 return ret
168 return ret
169
169
170
170
171 def upgrade(url, repository, version=None, **opts):
171 def upgrade(url, repository, version=None, **opts):
172 """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql]
172 """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql]
173
173
174 Upgrade a database to a later version.
174 Upgrade a database to a later version.
175
175
176 This runs the upgrade() function defined in your change scripts.
176 This runs the upgrade() function defined in your change scripts.
177
177
178 By default, the database is updated to the latest available
178 By default, the database is updated to the latest available
179 version. You may specify a version instead, if you wish.
179 version. You may specify a version instead, if you wish.
180
180
181 You may preview the Python or SQL code to be executed, rather than
181 You may preview the Python or SQL code to be executed, rather than
182 actually executing it, using the appropriate 'preview' option.
182 actually executing it, using the appropriate 'preview' option.
183 """
183 """
184 err = "Cannot upgrade a database of version %s to version %s. "\
184 err = "Cannot upgrade a database of version %s to version %s. "\
185 "Try 'downgrade' instead."
185 "Try 'downgrade' instead."
186 return _migrate(url, repository, version, upgrade=True, err=err, **opts)
186 return _migrate(url, repository, version, upgrade=True, err=err, **opts)
187
187
188
188
189 def downgrade(url, repository, version, **opts):
189 def downgrade(url, repository, version, **opts):
190 """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql]
190 """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql]
191
191
192 Downgrade a database to an earlier version.
192 Downgrade a database to an earlier version.
193
193
194 This is the reverse of upgrade; this runs the downgrade() function
194 This is the reverse of upgrade; this runs the downgrade() function
195 defined in your change scripts.
195 defined in your change scripts.
196
196
197 You may preview the Python or SQL code to be executed, rather than
197 You may preview the Python or SQL code to be executed, rather than
198 actually executing it, using the appropriate 'preview' option.
198 actually executing it, using the appropriate 'preview' option.
199 """
199 """
200 err = "Cannot downgrade a database of version %s to version %s. "\
200 err = "Cannot downgrade a database of version %s to version %s. "\
201 "Try 'upgrade' instead."
201 "Try 'upgrade' instead."
202 return _migrate(url, repository, version, upgrade=False, err=err, **opts)
202 return _migrate(url, repository, version, upgrade=False, err=err, **opts)
203
203
204 @with_engine
204 @with_engine
205 def test(url, repository, **opts):
205 def test(url, repository, **opts):
206 """%prog test URL REPOSITORY_PATH [VERSION]
206 """%prog test URL REPOSITORY_PATH [VERSION]
207
207
208 Performs the upgrade and downgrade option on the given
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
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
210 bad state. You should therefore better run the test on a copy of
211 your database.
211 your database.
212 """
212 """
213 engine = opts.pop('engine')
213 engine = opts.pop('engine')
214 repos = Repository(repository)
214 repos = Repository(repository)
215 script = repos.version(None).script()
215 script = repos.version(None).script()
216
216
217 # Upgrade
217 # Upgrade
218 log.info("Upgrading...")
218 log.info("Upgrading...")
219 script.run(engine, 1)
219 script.run(engine, 1)
220 log.info("done")
220 log.info("done")
221
221
222 log.info("Downgrading...")
222 log.info("Downgrading...")
223 script.run(engine, -1)
223 script.run(engine, -1)
224 log.info("done")
224 log.info("done")
225 log.info("Success")
225 log.info("Success")
226
226
227
227
228 @with_engine
228 @with_engine
229 def version_control(url, repository, version=None, **opts):
229 def version_control(url, repository, version=None, **opts):
230 """%prog version_control URL REPOSITORY_PATH [VERSION]
230 """%prog version_control URL REPOSITORY_PATH [VERSION]
231
231
232 Mark a database as under this repository's version control.
232 Mark a database as under this repository's version control.
233
233
234 Once a database is under version control, schema changes should
234 Once a database is under version control, schema changes should
235 only be done via change scripts in this repository.
235 only be done via change scripts in this repository.
236
236
237 This creates the table version_table in the database.
237 This creates the table version_table in the database.
238
238
239 The url should be any valid SQLAlchemy connection string.
239 The url should be any valid SQLAlchemy connection string.
240
240
241 By default, the database begins at version 0 and is assumed to be
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
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
243 which to begin instead. No attempt is made to verify this
244 version's correctness - the database schema is expected to be
244 version's correctness - the database schema is expected to be
245 identical to what it would be if the database were created from
245 identical to what it would be if the database were created from
246 scratch.
246 scratch.
247 """
247 """
248 engine = opts.pop('engine')
248 engine = opts.pop('engine')
249 ControlledSchema.create(engine, repository, version)
249 ControlledSchema.create(engine, repository, version)
250
250
251
251
252 @with_engine
252 @with_engine
253 def drop_version_control(url, repository, **opts):
253 def drop_version_control(url, repository, **opts):
254 """%prog drop_version_control URL REPOSITORY_PATH
254 """%prog drop_version_control URL REPOSITORY_PATH
255
255
256 Removes version control from a database.
256 Removes version control from a database.
257 """
257 """
258 engine = opts.pop('engine')
258 engine = opts.pop('engine')
259 schema = ControlledSchema(engine, repository)
259 schema = ControlledSchema(engine, repository)
260 schema.drop()
260 schema.drop()
261
261
262
262
263 def manage(file, **opts):
263 def manage(file, **opts):
264 """%prog manage FILENAME [VARIABLES...]
264 """%prog manage FILENAME [VARIABLES...]
265
265
266 Creates a script that runs Migrate with a set of default values.
266 Creates a script that runs Migrate with a set of default values.
267
267
268 For example::
268 For example::
269
269
270 %prog manage manage.py --repository=/path/to/repository \
270 %prog manage manage.py --repository=/path/to/repository \
271 --url=sqlite:///project.db
271 --url=sqlite:///project.db
272
272
273 would create the script manage.py. The following two commands
273 would create the script manage.py. The following two commands
274 would then have exactly the same results::
274 would then have exactly the same results::
275
275
276 python manage.py version
276 python manage.py version
277 %prog version --repository=/path/to/repository
277 %prog version --repository=/path/to/repository
278 """
278 """
279 Repository.create_manage_file(file, **opts)
279 Repository.create_manage_file(file, **opts)
280
280
281
281
282 @with_engine
282 @with_engine
283 def compare_model_to_db(url, repository, model, **opts):
283 def compare_model_to_db(url, repository, model, **opts):
284 """%prog compare_model_to_db URL REPOSITORY_PATH MODEL
284 """%prog compare_model_to_db URL REPOSITORY_PATH MODEL
285
285
286 Compare the current model (assumed to be a module level variable
286 Compare the current model (assumed to be a module level variable
287 of type sqlalchemy.MetaData) against the current database.
287 of type sqlalchemy.MetaData) against the current database.
288
288
289 NOTE: This is EXPERIMENTAL.
289 NOTE: This is EXPERIMENTAL.
290 """ # TODO: get rid of EXPERIMENTAL label
290 """ # TODO: get rid of EXPERIMENTAL label
291 engine = opts.pop('engine')
291 engine = opts.pop('engine')
292 return ControlledSchema.compare_model_to_db(engine, model, repository)
292 return ControlledSchema.compare_model_to_db(engine, model, repository)
293
293
294
294
295 @with_engine
295 @with_engine
296 def create_model(url, repository, **opts):
296 def create_model(url, repository, **opts):
297 """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True]
297 """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True]
298
298
299 Dump the current database as a Python model to stdout.
299 Dump the current database as a Python model to stdout.
300
300
301 NOTE: This is EXPERIMENTAL.
301 NOTE: This is EXPERIMENTAL.
302 """ # TODO: get rid of EXPERIMENTAL label
302 """ # TODO: get rid of EXPERIMENTAL label
303 engine = opts.pop('engine')
303 engine = opts.pop('engine')
304 declarative = opts.get('declarative', False)
304 declarative = opts.get('declarative', False)
305 return ControlledSchema.create_model(engine, repository, declarative)
305 return ControlledSchema.create_model(engine, repository, declarative)
306
306
307
307
308 @catch_known_errors
308 @catch_known_errors
309 @with_engine
309 @with_engine
310 def make_update_script_for_model(url, repository, oldmodel, model, **opts):
310 def make_update_script_for_model(url, repository, oldmodel, model, **opts):
311 """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
311 """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
312
312
313 Create a script changing the old Python model to the new (current)
313 Create a script changing the old Python model to the new (current)
314 Python model, sending to stdout.
314 Python model, sending to stdout.
315
315
316 NOTE: This is EXPERIMENTAL.
316 NOTE: This is EXPERIMENTAL.
317 """ # TODO: get rid of EXPERIMENTAL label
317 """ # TODO: get rid of EXPERIMENTAL label
318 engine = opts.pop('engine')
318 engine = opts.pop('engine')
319 return PythonScript.make_update_script_for_model(
319 return PythonScript.make_update_script_for_model(
320 engine, oldmodel, model, repository, **opts)
320 engine, oldmodel, model, repository, **opts)
321
321
322
322
323 @with_engine
323 @with_engine
324 def update_db_from_model(url, repository, model, **opts):
324 def update_db_from_model(url, repository, model, **opts):
325 """%prog update_db_from_model URL REPOSITORY_PATH MODEL
325 """%prog update_db_from_model URL REPOSITORY_PATH MODEL
326
326
327 Modify the database to match the structure of the current Python
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
328 model. This also sets the db_version number to the latest in the
329 repository.
329 repository.
330
330
331 NOTE: This is EXPERIMENTAL.
331 NOTE: This is EXPERIMENTAL.
332 """ # TODO: get rid of EXPERIMENTAL label
332 """ # TODO: get rid of EXPERIMENTAL label
333 engine = opts.pop('engine')
333 engine = opts.pop('engine')
334 schema = ControlledSchema(engine, repository)
334 schema = ControlledSchema(engine, repository)
335 schema.update_db_from_model(model)
335 schema.update_db_from_model(model)
336
336
337 @with_engine
337 @with_engine
338 def _migrate(url, repository, version, upgrade, err, **opts):
338 def _migrate(url, repository, version, upgrade, err, **opts):
339 engine = opts.pop('engine')
339 engine = opts.pop('engine')
340 url = str(engine.url)
340 url = str(engine.url)
341 schema = ControlledSchema(engine, repository)
341 schema = ControlledSchema(engine, repository)
342 version = _migrate_version(schema, version, upgrade, err)
342 version = _migrate_version(schema, version, upgrade, err)
343
343
344 changeset = schema.changeset(version)
344 changeset = schema.changeset(version)
345 for ver, change in changeset:
345 for ver, change in changeset:
346 nextver = ver + changeset.step
346 nextver = ver + changeset.step
347 log.info('%s -> %s... ', ver, nextver)
347 log.info('%s -> %s... ', ver, nextver)
348
348
349 if opts.get('preview_sql'):
349 if opts.get('preview_sql'):
350 if isinstance(change, PythonScript):
350 if isinstance(change, PythonScript):
351 log.info(change.preview_sql(url, changeset.step, **opts))
351 log.info(change.preview_sql(url, changeset.step, **opts))
352 elif isinstance(change, SqlScript):
352 elif isinstance(change, SqlScript):
353 log.info(change.source())
353 log.info(change.source())
354
354
355 elif opts.get('preview_py'):
355 elif opts.get('preview_py'):
356 if not isinstance(change, PythonScript):
356 if not isinstance(change, PythonScript):
357 raise exceptions.UsageError("Python source can be only displayed"
357 raise exceptions.UsageError("Python source can be only displayed"
358 " for python migration files")
358 " for python migration files")
359 source_ver = max(ver, nextver)
359 source_ver = max(ver, nextver)
360 module = schema.repository.version(source_ver).script().module
360 module = schema.repository.version(source_ver).script().module
361 funcname = upgrade and "upgrade" or "downgrade"
361 funcname = upgrade and "upgrade" or "downgrade"
362 func = getattr(module, funcname)
362 func = getattr(module, funcname)
363 log.info(inspect.getsource(func))
363 log.info(inspect.getsource(func))
364 else:
364 else:
365 schema.runchange(ver, change, changeset.step)
365 schema.runchange(ver, change, changeset.step)
366 log.info('done')
366 log.info('done')
367
367
368
368
369 def _migrate_version(schema, version, upgrade, err):
369 def _migrate_version(schema, version, upgrade, err):
370 if version is None:
370 if version is None:
371 return version
371 return version
372 # Version is specified: ensure we're upgrading in the right direction
372 # Version is specified: ensure we're upgrading in the right direction
373 # (current version < target version for upgrading; reverse for down)
373 # (current version < target version for upgrading; reverse for down)
374 version = VerNum(version)
374 version = VerNum(version)
375 cur = schema.version
375 cur = schema.version
376 if upgrade is not None:
376 if upgrade is not None:
377 if upgrade:
377 if upgrade:
378 direction = cur <= version
378 direction = cur <= version
379 else:
379 else:
380 direction = cur >= version
380 direction = cur >= version
381 if not direction:
381 if not direction:
382 raise exceptions.KnownError(err % (cur, version))
382 raise exceptions.KnownError(err % (cur, version))
383 return version
383 return version
@@ -1,253 +1,285 b''
1 """
1 """
2 Code to generate a Python model from a database or differences
2 Code to generate a Python model from a database or differences
3 between a model and database.
3 between a model and database.
4
4
5 Some of this is borrowed heavily from the AutoCode project at:
5 Some of this is borrowed heavily from the AutoCode project at:
6 http://code.google.com/p/sqlautocode/
6 http://code.google.com/p/sqlautocode/
7 """
7 """
8
8
9 import sys
9 import sys
10 import logging
10 import logging
11
11
12 import sqlalchemy
12 import sqlalchemy
13
13
14 from rhodecode.lib.dbmigrate import migrate
14 from rhodecode.lib.dbmigrate import migrate
15 from rhodecode.lib.dbmigrate.migrate import changeset
15 from rhodecode.lib.dbmigrate.migrate import changeset
16
16
17
17 log = logging.getLogger(__name__)
18 log = logging.getLogger(__name__)
18 HEADER = """
19 HEADER = """
19 ## File autogenerated by genmodel.py
20 ## File autogenerated by genmodel.py
20
21
21 from sqlalchemy import *
22 from sqlalchemy import *
22 meta = MetaData()
23 meta = MetaData()
23 """
24 """
24
25
25 DECLARATIVE_HEADER = """
26 DECLARATIVE_HEADER = """
26 ## File autogenerated by genmodel.py
27 ## File autogenerated by genmodel.py
27
28
28 from sqlalchemy import *
29 from sqlalchemy import *
29 from sqlalchemy.ext import declarative
30 from sqlalchemy.ext import declarative
30
31
31 Base = declarative.declarative_base()
32 Base = declarative.declarative_base()
32 """
33 """
33
34
34
35
35 class ModelGenerator(object):
36 class ModelGenerator(object):
37 """Various transformations from an A, B diff.
38
39 In the implementation, A tends to be called the model and B
40 the database (although this is not true of all diffs).
41 The diff is directionless, but transformations apply the diff
42 in a particular direction, described in the method name.
43 """
36
44
37 def __init__(self, diff, engine, declarative=False):
45 def __init__(self, diff, engine, declarative=False):
38 self.diff = diff
46 self.diff = diff
39 self.engine = engine
47 self.engine = engine
40 self.declarative = declarative
48 self.declarative = declarative
41
49
42 def column_repr(self, col):
50 def column_repr(self, col):
43 kwarg = []
51 kwarg = []
44 if col.key != col.name:
52 if col.key != col.name:
45 kwarg.append('key')
53 kwarg.append('key')
46 if col.primary_key:
54 if col.primary_key:
47 col.primary_key = True # otherwise it dumps it as 1
55 col.primary_key = True # otherwise it dumps it as 1
48 kwarg.append('primary_key')
56 kwarg.append('primary_key')
49 if not col.nullable:
57 if not col.nullable:
50 kwarg.append('nullable')
58 kwarg.append('nullable')
51 if col.onupdate:
59 if col.onupdate:
52 kwarg.append('onupdate')
60 kwarg.append('onupdate')
53 if col.default:
61 if col.default:
54 if col.primary_key:
62 if col.primary_key:
55 # I found that PostgreSQL automatically creates a
63 # I found that PostgreSQL automatically creates a
56 # default value for the sequence, but let's not show
64 # default value for the sequence, but let's not show
57 # that.
65 # that.
58 pass
66 pass
59 else:
67 else:
60 kwarg.append('default')
68 kwarg.append('default')
61 ks = ', '.join('%s=%r' % (k, getattr(col, k)) for k in kwarg)
69 args = ['%s=%r' % (k, getattr(col, k)) for k in kwarg]
62
70
63 # crs: not sure if this is good idea, but it gets rid of extra
71 # crs: not sure if this is good idea, but it gets rid of extra
64 # u''
72 # u''
65 name = col.name.encode('utf8')
73 name = col.name.encode('utf8')
66
74
67 type_ = col.type
75 type_ = col.type
68 for cls in col.type.__class__.__mro__:
76 for cls in col.type.__class__.__mro__:
69 if cls.__module__ == 'sqlalchemy.types' and \
77 if cls.__module__ == 'sqlalchemy.types' and \
70 not cls.__name__.isupper():
78 not cls.__name__.isupper():
71 if cls is not type_.__class__:
79 if cls is not type_.__class__:
72 type_ = cls()
80 type_ = cls()
73 break
81 break
74
82
83 type_repr = repr(type_)
84 if type_repr.endswith('()'):
85 type_repr = type_repr[:-2]
86
87 constraints = [repr(cn) for cn in col.constraints]
88
75 data = {
89 data = {
76 'name': name,
90 'name': name,
77 'type': type_,
91 'commonStuff': ', '.join([type_repr] + constraints + args),
78 'constraints': ', '.join([repr(cn) for cn in col.constraints]),
92 }
79 'args': ks and ks or ''}
80
93
81 if data['constraints']:
94 if self.declarative:
82 if data['args']:
95 return """%(name)s = Column(%(commonStuff)s)""" % data
83 data['args'] = ',' + data['args']
84
85 if data['constraints'] or data['args']:
86 data['maybeComma'] = ','
87 else:
96 else:
88 data['maybeComma'] = ''
97 return """Column(%(name)r, %(commonStuff)s)""" % data
89
98
90 commonStuff = """ %(maybeComma)s %(constraints)s %(args)s)""" % data
99 def _getTableDefn(self, table, metaName='meta'):
91 commonStuff = commonStuff.strip()
92 data['commonStuff'] = commonStuff
93 if self.declarative:
94 return """%(name)s = Column(%(type)r%(commonStuff)s""" % data
95 else:
96 return """Column(%(name)r, %(type)r%(commonStuff)s""" % data
97
98 def getTableDefn(self, table):
99 out = []
100 out = []
100 tableName = table.name
101 tableName = table.name
101 if self.declarative:
102 if self.declarative:
102 out.append("class %(table)s(Base):" % {'table': tableName})
103 out.append("class %(table)s(Base):" % {'table': tableName})
103 out.append(" __tablename__ = '%(table)s'" % {'table': tableName})
104 out.append(" __tablename__ = '%(table)s'\n" %
105 {'table': tableName})
104 for col in table.columns:
106 for col in table.columns:
105 out.append(" %s" % self.column_repr(col))
107 out.append(" %s" % self.column_repr(col))
108 out.append('\n')
106 else:
109 else:
107 out.append("%(table)s = Table('%(table)s', meta," % \
110 out.append("%(table)s = Table('%(table)s', %(meta)s," %
108 {'table': tableName})
111 {'table': tableName, 'meta': metaName})
109 for col in table.columns:
112 for col in table.columns:
110 out.append(" %s," % self.column_repr(col))
113 out.append(" %s," % self.column_repr(col))
111 out.append(")")
114 out.append(")\n")
112 return out
115 return out
113
116
114 def _get_tables(self,missingA=False,missingB=False,modified=False):
117 def _get_tables(self,missingA=False,missingB=False,modified=False):
115 to_process = []
118 to_process = []
116 for bool_,names,metadata in (
119 for bool_,names,metadata in (
117 (missingA,self.diff.tables_missing_from_A,self.diff.metadataB),
120 (missingA,self.diff.tables_missing_from_A,self.diff.metadataB),
118 (missingB,self.diff.tables_missing_from_B,self.diff.metadataA),
121 (missingB,self.diff.tables_missing_from_B,self.diff.metadataA),
119 (modified,self.diff.tables_different,self.diff.metadataA),
122 (modified,self.diff.tables_different,self.diff.metadataA),
120 ):
123 ):
121 if bool_:
124 if bool_:
122 for name in names:
125 for name in names:
123 yield metadata.tables.get(name)
126 yield metadata.tables.get(name)
124
127
125 def toPython(self):
128 def genBDefinition(self):
126 """Assume database is current and model is empty."""
129 """Generates the source code for a definition of B.
130
131 Assumes a diff where A is empty.
132
133 Was: toPython. Assume database (B) is current and model (A) is empty.
134 """
135
127 out = []
136 out = []
128 if self.declarative:
137 if self.declarative:
129 out.append(DECLARATIVE_HEADER)
138 out.append(DECLARATIVE_HEADER)
130 else:
139 else:
131 out.append(HEADER)
140 out.append(HEADER)
132 out.append("")
141 out.append("")
133 for table in self._get_tables(missingA=True):
142 for table in self._get_tables(missingA=True):
134 out.extend(self.getTableDefn(table))
143 out.extend(self._getTableDefn(table))
135 out.append("")
136 return '\n'.join(out)
144 return '\n'.join(out)
137
145
138 def toUpgradeDowngradePython(self, indent=' '):
146 def genB2AMigration(self, indent=' '):
139 ''' Assume model is most current and database is out-of-date. '''
147 '''Generate a migration from B to A.
140 decls = ['from rhodecode.lib.dbmigrate.migrate.changeset import schema',
148
141 'meta = MetaData()']
149 Was: toUpgradeDowngradePython
142 for table in self._get_tables(
150 Assume model (A) is most current and database (B) is out-of-date.
143 missingA=True,missingB=True,modified=True
151 '''
144 ):
152
145 decls.extend(self.getTableDefn(table))
153 decls = ['from migrate.changeset import schema',
154 'pre_meta = MetaData()',
155 'post_meta = MetaData()',
156 ]
157 upgradeCommands = ['pre_meta.bind = migrate_engine',
158 'post_meta.bind = migrate_engine']
159 downgradeCommands = list(upgradeCommands)
160
161 for tn in self.diff.tables_missing_from_A:
162 pre_table = self.diff.metadataB.tables[tn]
163 decls.extend(self._getTableDefn(pre_table, metaName='pre_meta'))
164 upgradeCommands.append(
165 "pre_meta.tables[%(table)r].drop()" % {'table': tn})
166 downgradeCommands.append(
167 "pre_meta.tables[%(table)r].create()" % {'table': tn})
146
168
147 upgradeCommands, downgradeCommands = [], []
169 for tn in self.diff.tables_missing_from_B:
148 for tableName in self.diff.tables_missing_from_A:
170 post_table = self.diff.metadataA.tables[tn]
149 upgradeCommands.append("%(table)s.drop()" % {'table': tableName})
171 decls.extend(self._getTableDefn(post_table, metaName='post_meta'))
150 downgradeCommands.append("%(table)s.create()" % \
172 upgradeCommands.append(
151 {'table': tableName})
173 "post_meta.tables[%(table)r].create()" % {'table': tn})
152 for tableName in self.diff.tables_missing_from_B:
174 downgradeCommands.append(
153 upgradeCommands.append("%(table)s.create()" % {'table': tableName})
175 "post_meta.tables[%(table)r].drop()" % {'table': tn})
154 downgradeCommands.append("%(table)s.drop()" % {'table': tableName})
155
176
156 for tableName in self.diff.tables_different:
177 for (tn, td) in self.diff.tables_different.iteritems():
157 dbTable = self.diff.metadataB.tables[tableName]
178 if td.columns_missing_from_A or td.columns_different:
158 missingInDatabase, missingInModel, diffDecl = \
179 pre_table = self.diff.metadataB.tables[tn]
159 self.diff.colDiffs[tableName]
180 decls.extend(self._getTableDefn(
160 for col in missingInDatabase:
181 pre_table, metaName='pre_meta'))
161 upgradeCommands.append('%s.columns[%r].create()' % (
182 if td.columns_missing_from_B or td.columns_different:
162 modelTable, col.name))
183 post_table = self.diff.metadataA.tables[tn]
163 downgradeCommands.append('%s.columns[%r].drop()' % (
184 decls.extend(self._getTableDefn(
164 modelTable, col.name))
185 post_table, metaName='post_meta'))
165 for col in missingInModel:
186
166 upgradeCommands.append('%s.columns[%r].drop()' % (
187 for col in td.columns_missing_from_A:
167 modelTable, col.name))
188 upgradeCommands.append(
168 downgradeCommands.append('%s.columns[%r].create()' % (
189 'pre_meta.tables[%r].columns[%r].drop()' % (tn, col))
169 modelTable, col.name))
190 downgradeCommands.append(
170 for modelCol, databaseCol, modelDecl, databaseDecl in diffDecl:
191 'pre_meta.tables[%r].columns[%r].create()' % (tn, col))
192 for col in td.columns_missing_from_B:
193 upgradeCommands.append(
194 'post_meta.tables[%r].columns[%r].create()' % (tn, col))
195 downgradeCommands.append(
196 'post_meta.tables[%r].columns[%r].drop()' % (tn, col))
197 for modelCol, databaseCol, modelDecl, databaseDecl in td.columns_different:
171 upgradeCommands.append(
198 upgradeCommands.append(
172 'assert False, "Can\'t alter columns: %s:%s=>%s"' % (
199 'assert False, "Can\'t alter columns: %s:%s=>%s"' % (
173 modelTable, modelCol.name, databaseCol.name))
200 tn, modelCol.name, databaseCol.name))
174 downgradeCommands.append(
201 downgradeCommands.append(
175 'assert False, "Can\'t alter columns: %s:%s=>%s"' % (
202 'assert False, "Can\'t alter columns: %s:%s=>%s"' % (
176 modelTable, modelCol.name, databaseCol.name))
203 tn, modelCol.name, databaseCol.name))
177 pre_command = ' meta.bind = migrate_engine'
178
204
179 return (
205 return (
180 '\n'.join(decls),
206 '\n'.join(decls),
181 '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in upgradeCommands]),
207 '\n'.join('%s%s' % (indent, line) for line in upgradeCommands),
182 '\n'.join([pre_command] + ['%s%s' % (indent, line) for line in downgradeCommands]))
208 '\n'.join('%s%s' % (indent, line) for line in downgradeCommands))
183
209
184 def _db_can_handle_this_change(self,td):
210 def _db_can_handle_this_change(self,td):
211 """Check if the database can handle going from B to A."""
212
185 if (td.columns_missing_from_B
213 if (td.columns_missing_from_B
186 and not td.columns_missing_from_A
214 and not td.columns_missing_from_A
187 and not td.columns_different):
215 and not td.columns_different):
188 # Even sqlite can handle this.
216 # Even sqlite can handle column additions.
189 return True
217 return True
190 else:
218 else:
191 return not self.engine.url.drivername.startswith('sqlite')
219 return not self.engine.url.drivername.startswith('sqlite')
192
220
193 def applyModel(self):
221 def runB2A(self):
194 """Apply model to current database."""
222 """Goes from B to A.
223
224 Was: applyModel. Apply model (A) to current database (B).
225 """
195
226
196 meta = sqlalchemy.MetaData(self.engine)
227 meta = sqlalchemy.MetaData(self.engine)
197
228
198 for table in self._get_tables(missingA=True):
229 for table in self._get_tables(missingA=True):
199 table = table.tometadata(meta)
230 table = table.tometadata(meta)
200 table.drop()
231 table.drop()
201 for table in self._get_tables(missingB=True):
232 for table in self._get_tables(missingB=True):
202 table = table.tometadata(meta)
233 table = table.tometadata(meta)
203 table.create()
234 table.create()
204 for modelTable in self._get_tables(modified=True):
235 for modelTable in self._get_tables(modified=True):
205 tableName = modelTable.name
236 tableName = modelTable.name
206 modelTable = modelTable.tometadata(meta)
237 modelTable = modelTable.tometadata(meta)
207 dbTable = self.diff.metadataB.tables[tableName]
238 dbTable = self.diff.metadataB.tables[tableName]
208
239
209 td = self.diff.tables_different[tableName]
240 td = self.diff.tables_different[tableName]
210
241
211 if self._db_can_handle_this_change(td):
242 if self._db_can_handle_this_change(td):
212
243
213 for col in td.columns_missing_from_B:
244 for col in td.columns_missing_from_B:
214 modelTable.columns[col].create()
245 modelTable.columns[col].create()
215 for col in td.columns_missing_from_A:
246 for col in td.columns_missing_from_A:
216 dbTable.columns[col].drop()
247 dbTable.columns[col].drop()
217 # XXX handle column changes here.
248 # XXX handle column changes here.
218 else:
249 else:
219 # Sqlite doesn't support drop column, so you have to
250 # Sqlite doesn't support drop column, so you have to
220 # do more: create temp table, copy data to it, drop
251 # do more: create temp table, copy data to it, drop
221 # old table, create new table, copy data back.
252 # old table, create new table, copy data back.
222 #
253 #
223 # I wonder if this is guaranteed to be unique?
254 # I wonder if this is guaranteed to be unique?
224 tempName = '_temp_%s' % modelTable.name
255 tempName = '_temp_%s' % modelTable.name
225
256
226 def getCopyStatement():
257 def getCopyStatement():
227 preparer = self.engine.dialect.preparer
258 preparer = self.engine.dialect.preparer
228 commonCols = []
259 commonCols = []
229 for modelCol in modelTable.columns:
260 for modelCol in modelTable.columns:
230 if modelCol.name in dbTable.columns:
261 if modelCol.name in dbTable.columns:
231 commonCols.append(modelCol.name)
262 commonCols.append(modelCol.name)
232 commonColsStr = ', '.join(commonCols)
263 commonColsStr = ', '.join(commonCols)
233 return 'INSERT INTO %s (%s) SELECT %s FROM %s' % \
264 return 'INSERT INTO %s (%s) SELECT %s FROM %s' % \
234 (tableName, commonColsStr, commonColsStr, tempName)
265 (tableName, commonColsStr, commonColsStr, tempName)
235
266
236 # Move the data in one transaction, so that we don't
267 # Move the data in one transaction, so that we don't
237 # leave the database in a nasty state.
268 # leave the database in a nasty state.
238 connection = self.engine.connect()
269 connection = self.engine.connect()
239 trans = connection.begin()
270 trans = connection.begin()
240 try:
271 try:
241 connection.execute(
272 connection.execute(
242 'CREATE TEMPORARY TABLE %s as SELECT * from %s' % \
273 'CREATE TEMPORARY TABLE %s as SELECT * from %s' % \
243 (tempName, modelTable.name))
274 (tempName, modelTable.name))
244 # make sure the drop takes place inside our
275 # make sure the drop takes place inside our
245 # transaction with the bind parameter
276 # transaction with the bind parameter
246 modelTable.drop(bind=connection)
277 modelTable.drop(bind=connection)
247 modelTable.create(bind=connection)
278 modelTable.create(bind=connection)
248 connection.execute(getCopyStatement())
279 connection.execute(getCopyStatement())
249 connection.execute('DROP TABLE %s' % tempName)
280 connection.execute('DROP TABLE %s' % tempName)
250 trans.commit()
281 trans.commit()
251 except:
282 except:
252 trans.rollback()
283 trans.rollback()
253 raise
284 raise
285
@@ -1,231 +1,242 b''
1 """
1 """
2 SQLAlchemy migrate repository management.
2 SQLAlchemy migrate repository management.
3 """
3 """
4 import os
4 import os
5 import shutil
5 import shutil
6 import string
6 import string
7 import logging
7 import logging
8
8
9 from pkg_resources import resource_filename
9 from pkg_resources import resource_filename
10 from tempita import Template as TempitaTemplate
10 from tempita import Template as TempitaTemplate
11
11
12 from rhodecode.lib.dbmigrate.migrate import exceptions
12 from rhodecode.lib.dbmigrate.migrate import exceptions
13 from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse
13 from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse
14 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
14 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
15 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
15 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
16
16
17
17
18 log = logging.getLogger(__name__)
18 log = logging.getLogger(__name__)
19
19
20 class Changeset(dict):
20 class Changeset(dict):
21 """A collection of changes to be applied to a database.
21 """A collection of changes to be applied to a database.
22
22
23 Changesets are bound to a repository and manage a set of
23 Changesets are bound to a repository and manage a set of
24 scripts from that repository.
24 scripts from that repository.
25
25
26 Behaves like a dict, for the most part. Keys are ordered based on step value.
26 Behaves like a dict, for the most part. Keys are ordered based on step value.
27 """
27 """
28
28
29 def __init__(self, start, *changes, **k):
29 def __init__(self, start, *changes, **k):
30 """
30 """
31 Give a start version; step must be explicitly stated.
31 Give a start version; step must be explicitly stated.
32 """
32 """
33 self.step = k.pop('step', 1)
33 self.step = k.pop('step', 1)
34 self.start = version.VerNum(start)
34 self.start = version.VerNum(start)
35 self.end = self.start
35 self.end = self.start
36 for change in changes:
36 for change in changes:
37 self.add(change)
37 self.add(change)
38
38
39 def __iter__(self):
39 def __iter__(self):
40 return iter(self.items())
40 return iter(self.items())
41
41
42 def keys(self):
42 def keys(self):
43 """
43 """
44 In a series of upgrades x -> y, keys are version x. Sorted.
44 In a series of upgrades x -> y, keys are version x. Sorted.
45 """
45 """
46 ret = super(Changeset, self).keys()
46 ret = super(Changeset, self).keys()
47 # Reverse order if downgrading
47 # Reverse order if downgrading
48 ret.sort(reverse=(self.step < 1))
48 ret.sort(reverse=(self.step < 1))
49 return ret
49 return ret
50
50
51 def values(self):
51 def values(self):
52 return [self[k] for k in self.keys()]
52 return [self[k] for k in self.keys()]
53
53
54 def items(self):
54 def items(self):
55 return zip(self.keys(), self.values())
55 return zip(self.keys(), self.values())
56
56
57 def add(self, change):
57 def add(self, change):
58 """Add new change to changeset"""
58 """Add new change to changeset"""
59 key = self.end
59 key = self.end
60 self.end += self.step
60 self.end += self.step
61 self[key] = change
61 self[key] = change
62
62
63 def run(self, *p, **k):
63 def run(self, *p, **k):
64 """Run the changeset scripts"""
64 """Run the changeset scripts"""
65 for version, script in self:
65 for version, script in self:
66 script.run(*p, **k)
66 script.run(*p, **k)
67
67
68
68
69 class Repository(pathed.Pathed):
69 class Repository(pathed.Pathed):
70 """A project's change script repository"""
70 """A project's change script repository"""
71
71
72 _config = 'migrate.cfg'
72 _config = 'migrate.cfg'
73 _versions = 'versions'
73 _versions = 'versions'
74
74
75 def __init__(self, path):
75 def __init__(self, path):
76 log.debug('Loading repository %s...' % path)
76 log.debug('Loading repository %s...' % path)
77 self.verify(path)
77 self.verify(path)
78 super(Repository, self).__init__(path)
78 super(Repository, self).__init__(path)
79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
80 self.versions = version.Collection(os.path.join(self.path,
80 self.versions = version.Collection(os.path.join(self.path,
81 self._versions))
81 self._versions))
82 log.debug('Repository %s loaded successfully' % path)
82 log.debug('Repository %s loaded successfully' % path)
83 log.debug('Config: %r' % self.config.to_dict())
83 log.debug('Config: %r' % self.config.to_dict())
84
84
85 @classmethod
85 @classmethod
86 def verify(cls, path):
86 def verify(cls, path):
87 """
87 """
88 Ensure the target path is a valid repository.
88 Ensure the target path is a valid repository.
89
89
90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
91 """
91 """
92 # Ensure the existence of required files
92 # Ensure the existence of required files
93 try:
93 try:
94 cls.require_found(path)
94 cls.require_found(path)
95 cls.require_found(os.path.join(path, cls._config))
95 cls.require_found(os.path.join(path, cls._config))
96 cls.require_found(os.path.join(path, cls._versions))
96 cls.require_found(os.path.join(path, cls._versions))
97 except exceptions.PathNotFoundError, e:
97 except exceptions.PathNotFoundError, e:
98 raise exceptions.InvalidRepositoryError(path)
98 raise exceptions.InvalidRepositoryError(path)
99
99
100 @classmethod
100 @classmethod
101 def prepare_config(cls, tmpl_dir, name, options=None):
101 def prepare_config(cls, tmpl_dir, name, options=None):
102 """
102 """
103 Prepare a project configuration file for a new project.
103 Prepare a project configuration file for a new project.
104
104
105 :param tmpl_dir: Path to Repository template
105 :param tmpl_dir: Path to Repository template
106 :param config_file: Name of the config file in Repository template
106 :param config_file: Name of the config file in Repository template
107 :param name: Repository name
107 :param name: Repository name
108 :type tmpl_dir: string
108 :type tmpl_dir: string
109 :type config_file: string
109 :type config_file: string
110 :type name: string
110 :type name: string
111 :returns: Populated config file
111 :returns: Populated config file
112 """
112 """
113 if options is None:
113 if options is None:
114 options = {}
114 options = {}
115 options.setdefault('version_table', 'migrate_version')
115 options.setdefault('version_table', 'migrate_version')
116 options.setdefault('repository_id', name)
116 options.setdefault('repository_id', name)
117 options.setdefault('required_dbs', [])
117 options.setdefault('required_dbs', [])
118 options.setdefault('use_timestamp_numbering', '0')
118
119
119 tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
120 tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
120 ret = TempitaTemplate(tmpl).substitute(options)
121 ret = TempitaTemplate(tmpl).substitute(options)
121
122
122 # cleanup
123 # cleanup
123 del options['__template_name__']
124 del options['__template_name__']
124
125
125 return ret
126 return ret
126
127
127 @classmethod
128 @classmethod
128 def create(cls, path, name, **opts):
129 def create(cls, path, name, **opts):
129 """Create a repository at a specified path"""
130 """Create a repository at a specified path"""
130 cls.require_notfound(path)
131 cls.require_notfound(path)
131 theme = opts.pop('templates_theme', None)
132 theme = opts.pop('templates_theme', None)
132 t_path = opts.pop('templates_path', None)
133 t_path = opts.pop('templates_path', None)
133
134
134 # Create repository
135 # Create repository
135 tmpl_dir = Template(t_path).get_repository(theme=theme)
136 tmpl_dir = Template(t_path).get_repository(theme=theme)
136 shutil.copytree(tmpl_dir, path)
137 shutil.copytree(tmpl_dir, path)
137
138
138 # Edit config defaults
139 # Edit config defaults
139 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
140 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
140 fd = open(os.path.join(path, cls._config), 'w')
141 fd = open(os.path.join(path, cls._config), 'w')
141 fd.write(config_text)
142 fd.write(config_text)
142 fd.close()
143 fd.close()
143
144
144 opts['repository_name'] = name
145 opts['repository_name'] = name
145
146
146 # Create a management script
147 # Create a management script
147 manager = os.path.join(path, 'manage.py')
148 manager = os.path.join(path, 'manage.py')
148 Repository.create_manage_file(manager, templates_theme=theme,
149 Repository.create_manage_file(manager, templates_theme=theme,
149 templates_path=t_path, **opts)
150 templates_path=t_path, **opts)
150
151
151 return cls(path)
152 return cls(path)
152
153
153 def create_script(self, description, **k):
154 def create_script(self, description, **k):
154 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
155 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
156
157 k['use_timestamp_numbering'] = self.use_timestamp_numbering
155 self.versions.create_new_python_version(description, **k)
158 self.versions.create_new_python_version(description, **k)
156
159
157 def create_script_sql(self, database, **k):
160 def create_script_sql(self, database, description, **k):
158 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
161 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
159 self.versions.create_new_sql_version(database, **k)
162 k['use_timestamp_numbering'] = self.use_timestamp_numbering
163 self.versions.create_new_sql_version(database, description, **k)
160
164
161 @property
165 @property
162 def latest(self):
166 def latest(self):
163 """API to :attr:`migrate.versioning.version.Collection.latest`"""
167 """API to :attr:`migrate.versioning.version.Collection.latest`"""
164 return self.versions.latest
168 return self.versions.latest
165
169
166 @property
170 @property
167 def version_table(self):
171 def version_table(self):
168 """Returns version_table name specified in config"""
172 """Returns version_table name specified in config"""
169 return self.config.get('db_settings', 'version_table')
173 return self.config.get('db_settings', 'version_table')
170
174
171 @property
175 @property
172 def id(self):
176 def id(self):
173 """Returns repository id specified in config"""
177 """Returns repository id specified in config"""
174 return self.config.get('db_settings', 'repository_id')
178 return self.config.get('db_settings', 'repository_id')
175
179
180 @property
181 def use_timestamp_numbering(self):
182 """Returns use_timestamp_numbering specified in config"""
183 ts_numbering = self.config.get('db_settings', 'use_timestamp_numbering', raw=True)
184
185 return ts_numbering
186
176 def version(self, *p, **k):
187 def version(self, *p, **k):
177 """API to :attr:`migrate.versioning.version.Collection.version`"""
188 """API to :attr:`migrate.versioning.version.Collection.version`"""
178 return self.versions.version(*p, **k)
189 return self.versions.version(*p, **k)
179
190
180 @classmethod
191 @classmethod
181 def clear(cls):
192 def clear(cls):
182 # TODO: deletes repo
193 # TODO: deletes repo
183 super(Repository, cls).clear()
194 super(Repository, cls).clear()
184 version.Collection.clear()
195 version.Collection.clear()
185
196
186 def changeset(self, database, start, end=None):
197 def changeset(self, database, start, end=None):
187 """Create a changeset to migrate this database from ver. start to end/latest.
198 """Create a changeset to migrate this database from ver. start to end/latest.
188
199
189 :param database: name of database to generate changeset
200 :param database: name of database to generate changeset
190 :param start: version to start at
201 :param start: version to start at
191 :param end: version to end at (latest if None given)
202 :param end: version to end at (latest if None given)
192 :type database: string
203 :type database: string
193 :type start: int
204 :type start: int
194 :type end: int
205 :type end: int
195 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
206 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
196 """
207 """
197 start = version.VerNum(start)
208 start = version.VerNum(start)
198
209
199 if end is None:
210 if end is None:
200 end = self.latest
211 end = self.latest
201 else:
212 else:
202 end = version.VerNum(end)
213 end = version.VerNum(end)
203
214
204 if start <= end:
215 if start <= end:
205 step = 1
216 step = 1
206 range_mod = 1
217 range_mod = 1
207 op = 'upgrade'
218 op = 'upgrade'
208 else:
219 else:
209 step = -1
220 step = -1
210 range_mod = 0
221 range_mod = 0
211 op = 'downgrade'
222 op = 'downgrade'
212
223
213 versions = range(start + range_mod, end + range_mod, step)
224 versions = range(start + range_mod, end + range_mod, step)
214 changes = [self.version(v).script(database, op) for v in versions]
225 changes = [self.version(v).script(database, op) for v in versions]
215 ret = Changeset(start, step=step, *changes)
226 ret = Changeset(start, step=step, *changes)
216 return ret
227 return ret
217
228
218 @classmethod
229 @classmethod
219 def create_manage_file(cls, file_, **opts):
230 def create_manage_file(cls, file_, **opts):
220 """Create a project management script (manage.py)
231 """Create a project management script (manage.py)
221
232
222 :param file_: Destination file to be written
233 :param file_: Destination file to be written
223 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
234 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
224 """
235 """
225 mng_file = Template(opts.pop('templates_path', None))\
236 mng_file = Template(opts.pop('templates_path', None))\
226 .get_manage(theme=opts.pop('templates_theme', None))
237 .get_manage(theme=opts.pop('templates_theme', None))
227
238
228 tmpl = open(mng_file).read()
239 tmpl = open(mng_file).read()
229 fd = open(file_, 'w')
240 fd = open(file_, 'w')
230 fd.write(TempitaTemplate(tmpl).substitute(opts))
241 fd.write(TempitaTemplate(tmpl).substitute(opts))
231 fd.close()
242 fd.close()
@@ -1,213 +1,220 b''
1 """
1 """
2 Database schema version management.
2 Database schema version management.
3 """
3 """
4 import sys
4 import sys
5 import logging
5 import logging
6
6
7 from sqlalchemy import (Table, Column, MetaData, String, Text, Integer,
7 from sqlalchemy import (Table, Column, MetaData, String, Text, Integer,
8 create_engine)
8 create_engine)
9 from sqlalchemy.sql import and_
9 from sqlalchemy.sql import and_
10 from sqlalchemy import exceptions as sa_exceptions
10 from sqlalchemy import exceptions as sa_exceptions
11 from sqlalchemy.sql import bindparam
11 from sqlalchemy.sql import bindparam
12
12
13 from rhodecode.lib.dbmigrate.migrate import exceptions
13 from rhodecode.lib.dbmigrate.migrate import exceptions
14 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_07
14 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
15 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
15 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
16 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
16 from rhodecode.lib.dbmigrate.migrate.versioning.util import load_model
17 from rhodecode.lib.dbmigrate.migrate.versioning.util import load_model
17 from rhodecode.lib.dbmigrate.migrate.versioning.version import VerNum
18 from rhodecode.lib.dbmigrate.migrate.versioning.version import VerNum
18
19
19
20
20 log = logging.getLogger(__name__)
21 log = logging.getLogger(__name__)
21
22
22 class ControlledSchema(object):
23 class ControlledSchema(object):
23 """A database under version control"""
24 """A database under version control"""
24
25
25 def __init__(self, engine, repository):
26 def __init__(self, engine, repository):
26 if isinstance(repository, basestring):
27 if isinstance(repository, basestring):
27 repository = Repository(repository)
28 repository = Repository(repository)
28 self.engine = engine
29 self.engine = engine
29 self.repository = repository
30 self.repository = repository
30 self.meta = MetaData(engine)
31 self.meta = MetaData(engine)
31 self.load()
32 self.load()
32
33
33 def __eq__(self, other):
34 def __eq__(self, other):
34 """Compare two schemas by repositories and versions"""
35 """Compare two schemas by repositories and versions"""
35 return (self.repository is other.repository \
36 return (self.repository is other.repository \
36 and self.version == other.version)
37 and self.version == other.version)
37
38
38 def load(self):
39 def load(self):
39 """Load controlled schema version info from DB"""
40 """Load controlled schema version info from DB"""
40 tname = self.repository.version_table
41 tname = self.repository.version_table
41 try:
42 try:
42 if not hasattr(self, 'table') or self.table is None:
43 if not hasattr(self, 'table') or self.table is None:
43 self.table = Table(tname, self.meta, autoload=True)
44 self.table = Table(tname, self.meta, autoload=True)
44
45
45 result = self.engine.execute(self.table.select(
46 result = self.engine.execute(self.table.select(
46 self.table.c.repository_id == str(self.repository.id)))
47 self.table.c.repository_id == str(self.repository.id)))
47
48
48 data = list(result)[0]
49 data = list(result)[0]
49 except:
50 except:
50 cls, exc, tb = sys.exc_info()
51 cls, exc, tb = sys.exc_info()
51 raise exceptions.DatabaseNotControlledError, exc.__str__(), tb
52 raise exceptions.DatabaseNotControlledError, exc.__str__(), tb
52
53
53 self.version = data['version']
54 self.version = data['version']
54 return data
55 return data
55
56
56 def drop(self):
57 def drop(self):
57 """
58 """
58 Remove version control from a database.
59 Remove version control from a database.
59 """
60 """
60 try:
61 if SQLA_07:
61 self.table.drop()
62 try:
62 except (sa_exceptions.SQLError):
63 self.table.drop()
63 raise exceptions.DatabaseNotControlledError(str(self.table))
64 except sa_exceptions.DatabaseError:
65 raise exceptions.DatabaseNotControlledError(str(self.table))
66 else:
67 try:
68 self.table.drop()
69 except (sa_exceptions.SQLError):
70 raise exceptions.DatabaseNotControlledError(str(self.table))
64
71
65 def changeset(self, version=None):
72 def changeset(self, version=None):
66 """API to Changeset creation.
73 """API to Changeset creation.
67
74
68 Uses self.version for start version and engine.name
75 Uses self.version for start version and engine.name
69 to get database name.
76 to get database name.
70 """
77 """
71 database = self.engine.name
78 database = self.engine.name
72 start_ver = self.version
79 start_ver = self.version
73 changeset = self.repository.changeset(database, start_ver, version)
80 changeset = self.repository.changeset(database, start_ver, version)
74 return changeset
81 return changeset
75
82
76 def runchange(self, ver, change, step):
83 def runchange(self, ver, change, step):
77 startver = ver
84 startver = ver
78 endver = ver + step
85 endver = ver + step
79 # Current database version must be correct! Don't run if corrupt!
86 # Current database version must be correct! Don't run if corrupt!
80 if self.version != startver:
87 if self.version != startver:
81 raise exceptions.InvalidVersionError("%s is not %s" % \
88 raise exceptions.InvalidVersionError("%s is not %s" % \
82 (self.version, startver))
89 (self.version, startver))
83 # Run the change
90 # Run the change
84 change.run(self.engine, step)
91 change.run(self.engine, step)
85
92
86 # Update/refresh database version
93 # Update/refresh database version
87 self.update_repository_table(startver, endver)
94 self.update_repository_table(startver, endver)
88 self.load()
95 self.load()
89
96
90 def update_repository_table(self, startver, endver):
97 def update_repository_table(self, startver, endver):
91 """Update version_table with new information"""
98 """Update version_table with new information"""
92 update = self.table.update(and_(self.table.c.version == int(startver),
99 update = self.table.update(and_(self.table.c.version == int(startver),
93 self.table.c.repository_id == str(self.repository.id)))
100 self.table.c.repository_id == str(self.repository.id)))
94 self.engine.execute(update, version=int(endver))
101 self.engine.execute(update, version=int(endver))
95
102
96 def upgrade(self, version=None):
103 def upgrade(self, version=None):
97 """
104 """
98 Upgrade (or downgrade) to a specified version, or latest version.
105 Upgrade (or downgrade) to a specified version, or latest version.
99 """
106 """
100 changeset = self.changeset(version)
107 changeset = self.changeset(version)
101 for ver, change in changeset:
108 for ver, change in changeset:
102 self.runchange(ver, change, changeset.step)
109 self.runchange(ver, change, changeset.step)
103
110
104 def update_db_from_model(self, model):
111 def update_db_from_model(self, model):
105 """
112 """
106 Modify the database to match the structure of the current Python model.
113 Modify the database to match the structure of the current Python model.
107 """
114 """
108 model = load_model(model)
115 model = load_model(model)
109
116
110 diff = schemadiff.getDiffOfModelAgainstDatabase(
117 diff = schemadiff.getDiffOfModelAgainstDatabase(
111 model, self.engine, excludeTables=[self.repository.version_table]
118 model, self.engine, excludeTables=[self.repository.version_table]
112 )
119 )
113 genmodel.ModelGenerator(diff,self.engine).applyModel()
120 genmodel.ModelGenerator(diff,self.engine).runB2A()
114
121
115 self.update_repository_table(self.version, int(self.repository.latest))
122 self.update_repository_table(self.version, int(self.repository.latest))
116
123
117 self.load()
124 self.load()
118
125
119 @classmethod
126 @classmethod
120 def create(cls, engine, repository, version=None):
127 def create(cls, engine, repository, version=None):
121 """
128 """
122 Declare a database to be under a repository's version control.
129 Declare a database to be under a repository's version control.
123
130
124 :raises: :exc:`DatabaseAlreadyControlledError`
131 :raises: :exc:`DatabaseAlreadyControlledError`
125 :returns: :class:`ControlledSchema`
132 :returns: :class:`ControlledSchema`
126 """
133 """
127 # Confirm that the version # is valid: positive, integer,
134 # Confirm that the version # is valid: positive, integer,
128 # exists in repos
135 # exists in repos
129 if isinstance(repository, basestring):
136 if isinstance(repository, basestring):
130 repository = Repository(repository)
137 repository = Repository(repository)
131 version = cls._validate_version(repository, version)
138 version = cls._validate_version(repository, version)
132 table = cls._create_table_version(engine, repository, version)
139 table = cls._create_table_version(engine, repository, version)
133 # TODO: history table
140 # TODO: history table
134 # Load repository information and return
141 # Load repository information and return
135 return cls(engine, repository)
142 return cls(engine, repository)
136
143
137 @classmethod
144 @classmethod
138 def _validate_version(cls, repository, version):
145 def _validate_version(cls, repository, version):
139 """
146 """
140 Ensures this is a valid version number for this repository.
147 Ensures this is a valid version number for this repository.
141
148
142 :raises: :exc:`InvalidVersionError` if invalid
149 :raises: :exc:`InvalidVersionError` if invalid
143 :return: valid version number
150 :return: valid version number
144 """
151 """
145 if version is None:
152 if version is None:
146 version = 0
153 version = 0
147 try:
154 try:
148 version = VerNum(version) # raises valueerror
155 version = VerNum(version) # raises valueerror
149 if version < 0 or version > repository.latest:
156 if version < 0 or version > repository.latest:
150 raise ValueError()
157 raise ValueError()
151 except ValueError:
158 except ValueError:
152 raise exceptions.InvalidVersionError(version)
159 raise exceptions.InvalidVersionError(version)
153 return version
160 return version
154
161
155 @classmethod
162 @classmethod
156 def _create_table_version(cls, engine, repository, version):
163 def _create_table_version(cls, engine, repository, version):
157 """
164 """
158 Creates the versioning table in a database.
165 Creates the versioning table in a database.
159
166
160 :raises: :exc:`DatabaseAlreadyControlledError`
167 :raises: :exc:`DatabaseAlreadyControlledError`
161 """
168 """
162 # Create tables
169 # Create tables
163 tname = repository.version_table
170 tname = repository.version_table
164 meta = MetaData(engine)
171 meta = MetaData(engine)
165
172
166 table = Table(
173 table = Table(
167 tname, meta,
174 tname, meta,
168 Column('repository_id', String(250), primary_key=True),
175 Column('repository_id', String(250), primary_key=True),
169 Column('repository_path', Text),
176 Column('repository_path', Text),
170 Column('version', Integer), )
177 Column('version', Integer), )
171
178
172 # there can be multiple repositories/schemas in the same db
179 # there can be multiple repositories/schemas in the same db
173 if not table.exists():
180 if not table.exists():
174 table.create()
181 table.create()
175
182
176 # test for existing repository_id
183 # test for existing repository_id
177 s = table.select(table.c.repository_id == bindparam("repository_id"))
184 s = table.select(table.c.repository_id == bindparam("repository_id"))
178 result = engine.execute(s, repository_id=repository.id)
185 result = engine.execute(s, repository_id=repository.id)
179 if result.fetchone():
186 if result.fetchone():
180 raise exceptions.DatabaseAlreadyControlledError
187 raise exceptions.DatabaseAlreadyControlledError
181
188
182 # Insert data
189 # Insert data
183 engine.execute(table.insert().values(
190 engine.execute(table.insert().values(
184 repository_id=repository.id,
191 repository_id=repository.id,
185 repository_path=repository.path,
192 repository_path=repository.path,
186 version=int(version)))
193 version=int(version)))
187 return table
194 return table
188
195
189 @classmethod
196 @classmethod
190 def compare_model_to_db(cls, engine, model, repository):
197 def compare_model_to_db(cls, engine, model, repository):
191 """
198 """
192 Compare the current model against the current database.
199 Compare the current model against the current database.
193 """
200 """
194 if isinstance(repository, basestring):
201 if isinstance(repository, basestring):
195 repository = Repository(repository)
202 repository = Repository(repository)
196 model = load_model(model)
203 model = load_model(model)
197
204
198 diff = schemadiff.getDiffOfModelAgainstDatabase(
205 diff = schemadiff.getDiffOfModelAgainstDatabase(
199 model, engine, excludeTables=[repository.version_table])
206 model, engine, excludeTables=[repository.version_table])
200 return diff
207 return diff
201
208
202 @classmethod
209 @classmethod
203 def create_model(cls, engine, repository, declarative=False):
210 def create_model(cls, engine, repository, declarative=False):
204 """
211 """
205 Dump the current database as a Python model.
212 Dump the current database as a Python model.
206 """
213 """
207 if isinstance(repository, basestring):
214 if isinstance(repository, basestring):
208 repository = Repository(repository)
215 repository = Repository(repository)
209
216
210 diff = schemadiff.getDiffOfModelAgainstDatabase(
217 diff = schemadiff.getDiffOfModelAgainstDatabase(
211 MetaData(), engine, excludeTables=[repository.version_table]
218 MetaData(), engine, excludeTables=[repository.version_table]
212 )
219 )
213 return genmodel.ModelGenerator(diff, engine, declarative).toPython()
220 return genmodel.ModelGenerator(diff, engine, declarative).genBDefinition()
@@ -1,160 +1,160 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3
3
4 import shutil
4 import shutil
5 import warnings
5 import warnings
6 import logging
6 import logging
7 import inspect
7 import inspect
8 from StringIO import StringIO
8 from StringIO import StringIO
9
9
10 from rhodecode.lib.dbmigrate import migrate
10 from rhodecode.lib.dbmigrate import migrate
11 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
11 from rhodecode.lib.dbmigrate.migrate.versioning import genmodel, schemadiff
12 from rhodecode.lib.dbmigrate.migrate.versioning.config import operations
12 from rhodecode.lib.dbmigrate.migrate.versioning.config import operations
13 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
13 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
14 from rhodecode.lib.dbmigrate.migrate.versioning.script import base
14 from rhodecode.lib.dbmigrate.migrate.versioning.script import base
15 from rhodecode.lib.dbmigrate.migrate.versioning.util import import_path, load_model, with_engine
15 from rhodecode.lib.dbmigrate.migrate.versioning.util import import_path, load_model, with_engine
16 from rhodecode.lib.dbmigrate.migrate.exceptions import MigrateDeprecationWarning, InvalidScriptError, ScriptError
16 from rhodecode.lib.dbmigrate.migrate.exceptions import MigrateDeprecationWarning, InvalidScriptError, ScriptError
17
17
18 log = logging.getLogger(__name__)
18 log = logging.getLogger(__name__)
19 __all__ = ['PythonScript']
19 __all__ = ['PythonScript']
20
20
21
21
22 class PythonScript(base.BaseScript):
22 class PythonScript(base.BaseScript):
23 """Base for Python scripts"""
23 """Base for Python scripts"""
24
24
25 @classmethod
25 @classmethod
26 def create(cls, path, **opts):
26 def create(cls, path, **opts):
27 """Create an empty migration script at specified path
27 """Create an empty migration script at specified path
28
28
29 :returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
29 :returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
30 cls.require_notfound(path)
30 cls.require_notfound(path)
31
31
32 src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None))
32 src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None))
33 shutil.copy(src, path)
33 shutil.copy(src, path)
34
34
35 return cls(path)
35 return cls(path)
36
36
37 @classmethod
37 @classmethod
38 def make_update_script_for_model(cls, engine, oldmodel,
38 def make_update_script_for_model(cls, engine, oldmodel,
39 model, repository, **opts):
39 model, repository, **opts):
40 """Create a migration script based on difference between two SA models.
40 """Create a migration script based on difference between two SA models.
41
41
42 :param repository: path to migrate repository
42 :param repository: path to migrate repository
43 :param oldmodel: dotted.module.name:SAClass or SAClass object
43 :param oldmodel: dotted.module.name:SAClass or SAClass object
44 :param model: dotted.module.name:SAClass or SAClass object
44 :param model: dotted.module.name:SAClass or SAClass object
45 :param engine: SQLAlchemy engine
45 :param engine: SQLAlchemy engine
46 :type repository: string or :class:`Repository instance <migrate.versioning.repository.Repository>`
46 :type repository: string or :class:`Repository instance <migrate.versioning.repository.Repository>`
47 :type oldmodel: string or Class
47 :type oldmodel: string or Class
48 :type model: string or Class
48 :type model: string or Class
49 :type engine: Engine instance
49 :type engine: Engine instance
50 :returns: Upgrade / Downgrade script
50 :returns: Upgrade / Downgrade script
51 :rtype: string
51 :rtype: string
52 """
52 """
53
53
54 if isinstance(repository, basestring):
54 if isinstance(repository, basestring):
55 # oh dear, an import cycle!
55 # oh dear, an import cycle!
56 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
56 from rhodecode.lib.dbmigrate.migrate.versioning.repository import Repository
57 repository = Repository(repository)
57 repository = Repository(repository)
58
58
59 oldmodel = load_model(oldmodel)
59 oldmodel = load_model(oldmodel)
60 model = load_model(model)
60 model = load_model(model)
61
61
62 # Compute differences.
62 # Compute differences.
63 diff = schemadiff.getDiffOfModelAgainstModel(
63 diff = schemadiff.getDiffOfModelAgainstModel(
64 model,
64 oldmodel,
65 oldmodel,
65 model,
66 excludeTables=[repository.version_table])
66 excludeTables=[repository.version_table])
67 # TODO: diff can be False (there is no difference?)
67 # TODO: diff can be False (there is no difference?)
68 decls, upgradeCommands, downgradeCommands = \
68 decls, upgradeCommands, downgradeCommands = \
69 genmodel.ModelGenerator(diff,engine).toUpgradeDowngradePython()
69 genmodel.ModelGenerator(diff,engine).genB2AMigration()
70
70
71 # Store differences into file.
71 # Store differences into file.
72 src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))
72 src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))
73 f = open(src)
73 f = open(src)
74 contents = f.read()
74 contents = f.read()
75 f.close()
75 f.close()
76
76
77 # generate source
77 # generate source
78 search = 'def upgrade(migrate_engine):'
78 search = 'def upgrade(migrate_engine):'
79 contents = contents.replace(search, '\n\n'.join((decls, search)), 1)
79 contents = contents.replace(search, '\n\n'.join((decls, search)), 1)
80 if upgradeCommands:
80 if upgradeCommands:
81 contents = contents.replace(' pass', upgradeCommands, 1)
81 contents = contents.replace(' pass', upgradeCommands, 1)
82 if downgradeCommands:
82 if downgradeCommands:
83 contents = contents.replace(' pass', downgradeCommands, 1)
83 contents = contents.replace(' pass', downgradeCommands, 1)
84 return contents
84 return contents
85
85
86 @classmethod
86 @classmethod
87 def verify_module(cls, path):
87 def verify_module(cls, path):
88 """Ensure path is a valid script
88 """Ensure path is a valid script
89
89
90 :param path: Script location
90 :param path: Script location
91 :type path: string
91 :type path: string
92 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
92 :raises: :exc:`InvalidScriptError <migrate.exceptions.InvalidScriptError>`
93 :returns: Python module
93 :returns: Python module
94 """
94 """
95 # Try to import and get the upgrade() func
95 # Try to import and get the upgrade() func
96 module = import_path(path)
96 module = import_path(path)
97 try:
97 try:
98 assert callable(module.upgrade)
98 assert callable(module.upgrade)
99 except Exception, e:
99 except Exception, e:
100 raise InvalidScriptError(path + ': %s' % str(e))
100 raise InvalidScriptError(path + ': %s' % str(e))
101 return module
101 return module
102
102
103 def preview_sql(self, url, step, **args):
103 def preview_sql(self, url, step, **args):
104 """Mocks SQLAlchemy Engine to store all executed calls in a string
104 """Mocks SQLAlchemy Engine to store all executed calls in a string
105 and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>`
105 and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>`
106
106
107 :returns: SQL file
107 :returns: SQL file
108 """
108 """
109 buf = StringIO()
109 buf = StringIO()
110 args['engine_arg_strategy'] = 'mock'
110 args['engine_arg_strategy'] = 'mock'
111 args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p)
111 args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p)
112
112
113 @with_engine
113 @with_engine
114 def go(url, step, **kw):
114 def go(url, step, **kw):
115 engine = kw.pop('engine')
115 engine = kw.pop('engine')
116 self.run(engine, step)
116 self.run(engine, step)
117 return buf.getvalue()
117 return buf.getvalue()
118
118
119 return go(url, step, **args)
119 return go(url, step, **args)
120
120
121 def run(self, engine, step):
121 def run(self, engine, step):
122 """Core method of Script file.
122 """Core method of Script file.
123 Exectues :func:`update` or :func:`downgrade` functions
123 Exectues :func:`update` or :func:`downgrade` functions
124
124
125 :param engine: SQLAlchemy Engine
125 :param engine: SQLAlchemy Engine
126 :param step: Operation to run
126 :param step: Operation to run
127 :type engine: string
127 :type engine: string
128 :type step: int
128 :type step: int
129 """
129 """
130 if step > 0:
130 if step > 0:
131 op = 'upgrade'
131 op = 'upgrade'
132 elif step < 0:
132 elif step < 0:
133 op = 'downgrade'
133 op = 'downgrade'
134 else:
134 else:
135 raise ScriptError("%d is not a valid step" % step)
135 raise ScriptError("%d is not a valid step" % step)
136
136
137 funcname = base.operations[op]
137 funcname = base.operations[op]
138 script_func = self._func(funcname)
138 script_func = self._func(funcname)
139
139
140 # check for old way of using engine
140 # check for old way of using engine
141 if not inspect.getargspec(script_func)[0]:
141 if not inspect.getargspec(script_func)[0]:
142 raise TypeError("upgrade/downgrade functions must accept engine"
142 raise TypeError("upgrade/downgrade functions must accept engine"
143 " parameter (since version 0.5.4)")
143 " parameter (since version 0.5.4)")
144
144
145 script_func(engine)
145 script_func(engine)
146
146
147 @property
147 @property
148 def module(self):
148 def module(self):
149 """Calls :meth:`migrate.versioning.script.py.verify_module`
149 """Calls :meth:`migrate.versioning.script.py.verify_module`
150 and returns it.
150 and returns it.
151 """
151 """
152 if not hasattr(self, '_module'):
152 if not hasattr(self, '_module'):
153 self._module = self.verify_module(self.path)
153 self._module = self.verify_module(self.path)
154 return self._module
154 return self._module
155
155
156 def _func(self, funcname):
156 def _func(self, funcname):
157 if not hasattr(self.module, funcname):
157 if not hasattr(self.module, funcname):
158 msg = "Function '%s' is not defined in this script"
158 msg = "Function '%s' is not defined in this script"
159 raise ScriptError(msg % funcname)
159 raise ScriptError(msg % funcname)
160 return getattr(self.module, funcname)
160 return getattr(self.module, funcname)
@@ -1,20 +1,25 b''
1 [db_settings]
1 [db_settings]
2 # Used to identify which repository this database is versioned under.
2 # Used to identify which repository this database is versioned under.
3 # You can use the name of your project.
3 # You can use the name of your project.
4 repository_id={{ locals().pop('repository_id') }}
4 repository_id={{ locals().pop('repository_id') }}
5
5
6 # The name of the database table used to track the schema version.
6 # The name of the database table used to track the schema version.
7 # This name shouldn't already be used by your project.
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
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.
9 # change the table name in each database too.
10 version_table={{ locals().pop('version_table') }}
10 version_table={{ locals().pop('version_table') }}
11
11
12 # When committing a change script, Migrate will attempt to generate the
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
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
14 # because you don't have that database installed - it is ignored and the
15 # commit continues, perhaps ending successfully.
15 # commit continues, perhaps ending successfully.
16 # Databases in this list MUST compile successfully during a commit, or the
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
17 # entire commit will fail. List the databases your application will actually
18 # be using to ensure your updates to that database work properly.
18 # be using to ensure your updates to that database work properly.
19 # This must be a list; example: ['postgres','sqlite']
19 # This must be a list; example: ['postgres','sqlite']
20 required_dbs={{ locals().pop('required_dbs') }}
20 required_dbs={{ locals().pop('required_dbs') }}
21
22 # When creating new change scripts, Migrate will stamp the new script with
23 # a version number. By default this is latest_version + 1. You can set this
24 # to 'true' to tell Migrate to use the UTC timestamp instead.
25 use_timestamp_numbering='false' No newline at end of file
@@ -1,215 +1,240 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3
3
4 import os
4 import os
5 import re
5 import re
6 import shutil
6 import shutil
7 import logging
7 import logging
8
8
9 from rhodecode.lib.dbmigrate.migrate import exceptions
9 from rhodecode.lib.dbmigrate.migrate import exceptions
10 from rhodecode.lib.dbmigrate.migrate.versioning import pathed, script
10 from rhodecode.lib.dbmigrate.migrate.versioning import pathed, script
11 from datetime import datetime
11
12
12
13
13 log = logging.getLogger(__name__)
14 log = logging.getLogger(__name__)
14
15
15 class VerNum(object):
16 class VerNum(object):
16 """A version number that behaves like a string and int at the same time"""
17 """A version number that behaves like a string and int at the same time"""
17
18
18 _instances = dict()
19 _instances = dict()
19
20
20 def __new__(cls, value):
21 def __new__(cls, value):
21 val = str(value)
22 val = str(value)
22 if val not in cls._instances:
23 if val not in cls._instances:
23 cls._instances[val] = super(VerNum, cls).__new__(cls)
24 cls._instances[val] = super(VerNum, cls).__new__(cls)
24 ret = cls._instances[val]
25 ret = cls._instances[val]
25 return ret
26 return ret
26
27
27 def __init__(self,value):
28 def __init__(self,value):
28 self.value = str(int(value))
29 self.value = str(int(value))
29 if self < 0:
30 if self < 0:
30 raise ValueError("Version number cannot be negative")
31 raise ValueError("Version number cannot be negative")
31
32
32 def __add__(self, value):
33 def __add__(self, value):
33 ret = int(self) + int(value)
34 ret = int(self) + int(value)
34 return VerNum(ret)
35 return VerNum(ret)
35
36
36 def __sub__(self, value):
37 def __sub__(self, value):
37 return self + (int(value) * -1)
38 return self + (int(value) * -1)
38
39
39 def __cmp__(self, value):
40 def __cmp__(self, value):
40 return int(self) - int(value)
41 return int(self) - int(value)
41
42
42 def __repr__(self):
43 def __repr__(self):
43 return "<VerNum(%s)>" % self.value
44 return "<VerNum(%s)>" % self.value
44
45
45 def __str__(self):
46 def __str__(self):
46 return str(self.value)
47 return str(self.value)
47
48
48 def __int__(self):
49 def __int__(self):
49 return int(self.value)
50 return int(self.value)
50
51
51
52
52 class Collection(pathed.Pathed):
53 class Collection(pathed.Pathed):
53 """A collection of versioning scripts in a repository"""
54 """A collection of versioning scripts in a repository"""
54
55
55 FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*')
56 FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*')
56
57
57 def __init__(self, path):
58 def __init__(self, path):
58 """Collect current version scripts in repository
59 """Collect current version scripts in repository
59 and store them in self.versions
60 and store them in self.versions
60 """
61 """
61 super(Collection, self).__init__(path)
62 super(Collection, self).__init__(path)
62
63
63 # Create temporary list of files, allowing skipped version numbers.
64 # Create temporary list of files, allowing skipped version numbers.
64 files = os.listdir(path)
65 files = os.listdir(path)
65 if '1' in files:
66 if '1' in files:
66 # deprecation
67 # deprecation
67 raise Exception('It looks like you have a repository in the old '
68 raise Exception('It looks like you have a repository in the old '
68 'format (with directories for each version). '
69 'format (with directories for each version). '
69 'Please convert repository before proceeding.')
70 'Please convert repository before proceeding.')
70
71
71 tempVersions = dict()
72 tempVersions = dict()
72 for filename in files:
73 for filename in files:
73 match = self.FILENAME_WITH_VERSION.match(filename)
74 match = self.FILENAME_WITH_VERSION.match(filename)
74 if match:
75 if match:
75 num = int(match.group(1))
76 num = int(match.group(1))
76 tempVersions.setdefault(num, []).append(filename)
77 tempVersions.setdefault(num, []).append(filename)
77 else:
78 else:
78 pass # Must be a helper file or something, let's ignore it.
79 pass # Must be a helper file or something, let's ignore it.
79
80
80 # Create the versions member where the keys
81 # Create the versions member where the keys
81 # are VerNum's and the values are Version's.
82 # are VerNum's and the values are Version's.
82 self.versions = dict()
83 self.versions = dict()
83 for num, files in tempVersions.items():
84 for num, files in tempVersions.items():
84 self.versions[VerNum(num)] = Version(num, path, files)
85 self.versions[VerNum(num)] = Version(num, path, files)
85
86
86 @property
87 @property
87 def latest(self):
88 def latest(self):
88 """:returns: Latest version in Collection"""
89 """:returns: Latest version in Collection"""
89 return max([VerNum(0)] + self.versions.keys())
90 return max([VerNum(0)] + self.versions.keys())
90
91
92 def _next_ver_num(self, use_timestamp_numbering):
93 print use_timestamp_numbering
94 if use_timestamp_numbering == True:
95 print "Creating new timestamp version!"
96 return VerNum(int(datetime.utcnow().strftime('%Y%m%d%H%M%S')))
97 else:
98 return self.latest + 1
99
91 def create_new_python_version(self, description, **k):
100 def create_new_python_version(self, description, **k):
92 """Create Python files for new version"""
101 """Create Python files for new version"""
93 ver = self.latest + 1
102 ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
94 extra = str_to_filename(description)
103 extra = str_to_filename(description)
95
104
96 if extra:
105 if extra:
97 if extra == '_':
106 if extra == '_':
98 extra = ''
107 extra = ''
99 elif not extra.startswith('_'):
108 elif not extra.startswith('_'):
100 extra = '_%s' % extra
109 extra = '_%s' % extra
101
110
102 filename = '%03d%s.py' % (ver, extra)
111 filename = '%03d%s.py' % (ver, extra)
103 filepath = self._version_path(filename)
112 filepath = self._version_path(filename)
104
113
105 script.PythonScript.create(filepath, **k)
114 script.PythonScript.create(filepath, **k)
106 self.versions[ver] = Version(ver, self.path, [filename])
115 self.versions[ver] = Version(ver, self.path, [filename])
107
116
108 def create_new_sql_version(self, database, **k):
117 def create_new_sql_version(self, database, description, **k):
109 """Create SQL files for new version"""
118 """Create SQL files for new version"""
110 ver = self.latest + 1
119 ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
111 self.versions[ver] = Version(ver, self.path, [])
120 self.versions[ver] = Version(ver, self.path, [])
112
121
122 extra = str_to_filename(description)
123
124 if extra:
125 if extra == '_':
126 extra = ''
127 elif not extra.startswith('_'):
128 extra = '_%s' % extra
129
113 # Create new files.
130 # Create new files.
114 for op in ('upgrade', 'downgrade'):
131 for op in ('upgrade', 'downgrade'):
115 filename = '%03d_%s_%s.sql' % (ver, database, op)
132 filename = '%03d%s_%s_%s.sql' % (ver, extra, database, op)
116 filepath = self._version_path(filename)
133 filepath = self._version_path(filename)
117 script.SqlScript.create(filepath, **k)
134 script.SqlScript.create(filepath, **k)
118 self.versions[ver].add_script(filepath)
135 self.versions[ver].add_script(filepath)
119
136
120 def version(self, vernum=None):
137 def version(self, vernum=None):
121 """Returns latest Version if vernum is not given.
138 """Returns latest Version if vernum is not given.
122 Otherwise, returns wanted version"""
139 Otherwise, returns wanted version"""
123 if vernum is None:
140 if vernum is None:
124 vernum = self.latest
141 vernum = self.latest
125 return self.versions[VerNum(vernum)]
142 return self.versions[VerNum(vernum)]
126
143
127 @classmethod
144 @classmethod
128 def clear(cls):
145 def clear(cls):
129 super(Collection, cls).clear()
146 super(Collection, cls).clear()
130
147
131 def _version_path(self, ver):
148 def _version_path(self, ver):
132 """Returns path of file in versions repository"""
149 """Returns path of file in versions repository"""
133 return os.path.join(self.path, str(ver))
150 return os.path.join(self.path, str(ver))
134
151
135
152
136 class Version(object):
153 class Version(object):
137 """A single version in a collection
154 """A single version in a collection
138 :param vernum: Version Number
155 :param vernum: Version Number
139 :param path: Path to script files
156 :param path: Path to script files
140 :param filelist: List of scripts
157 :param filelist: List of scripts
141 :type vernum: int, VerNum
158 :type vernum: int, VerNum
142 :type path: string
159 :type path: string
143 :type filelist: list
160 :type filelist: list
144 """
161 """
145
162
146 def __init__(self, vernum, path, filelist):
163 def __init__(self, vernum, path, filelist):
147 self.version = VerNum(vernum)
164 self.version = VerNum(vernum)
148
165
149 # Collect scripts in this folder
166 # Collect scripts in this folder
150 self.sql = dict()
167 self.sql = dict()
151 self.python = None
168 self.python = None
152
169
153 for script in filelist:
170 for script in filelist:
154 self.add_script(os.path.join(path, script))
171 self.add_script(os.path.join(path, script))
155
172
156 def script(self, database=None, operation=None):
173 def script(self, database=None, operation=None):
157 """Returns SQL or Python Script"""
174 """Returns SQL or Python Script"""
158 for db in (database, 'default'):
175 for db in (database, 'default'):
159 # Try to return a .sql script first
176 # Try to return a .sql script first
160 try:
177 try:
161 return self.sql[db][operation]
178 return self.sql[db][operation]
162 except KeyError:
179 except KeyError:
163 continue # No .sql script exists
180 continue # No .sql script exists
164
181
165 # TODO: maybe add force Python parameter?
182 # TODO: maybe add force Python parameter?
166 ret = self.python
183 ret = self.python
167
184
168 assert ret is not None, \
185 assert ret is not None, \
169 "There is no script for %d version" % self.version
186 "There is no script for %d version" % self.version
170 return ret
187 return ret
171
188
172 def add_script(self, path):
189 def add_script(self, path):
173 """Add script to Collection/Version"""
190 """Add script to Collection/Version"""
174 if path.endswith(Extensions.py):
191 if path.endswith(Extensions.py):
175 self._add_script_py(path)
192 self._add_script_py(path)
176 elif path.endswith(Extensions.sql):
193 elif path.endswith(Extensions.sql):
177 self._add_script_sql(path)
194 self._add_script_sql(path)
178
195
179 SQL_FILENAME = re.compile(r'^(\d+)_([^_]+)_([^_]+).sql')
196 SQL_FILENAME = re.compile(r'^.*\.sql')
180
197
181 def _add_script_sql(self, path):
198 def _add_script_sql(self, path):
182 basename = os.path.basename(path)
199 basename = os.path.basename(path)
183 match = self.SQL_FILENAME.match(basename)
200 match = self.SQL_FILENAME.match(basename)
184
201
185 if match:
202 if match:
186 version, dbms, op = match.group(1), match.group(2), match.group(3)
203 basename = basename.replace('.sql', '')
204 parts = basename.split('_')
205 if len(parts) < 3:
206 raise exceptions.ScriptError(
207 "Invalid SQL script name %s " % basename + \
208 "(needs to be ###_description_database_operation.sql)")
209 version = parts[0]
210 op = parts[-1]
211 dbms = parts[-2]
187 else:
212 else:
188 raise exceptions.ScriptError(
213 raise exceptions.ScriptError(
189 "Invalid SQL script name %s " % basename + \
214 "Invalid SQL script name %s " % basename + \
190 "(needs to be ###_database_operation.sql)")
215 "(needs to be ###_description_database_operation.sql)")
191
216
192 # File the script into a dictionary
217 # File the script into a dictionary
193 self.sql.setdefault(dbms, {})[op] = script.SqlScript(path)
218 self.sql.setdefault(dbms, {})[op] = script.SqlScript(path)
194
219
195 def _add_script_py(self, path):
220 def _add_script_py(self, path):
196 if self.python is not None:
221 if self.python is not None:
197 raise exceptions.ScriptError('You can only have one Python script '
222 raise exceptions.ScriptError('You can only have one Python script '
198 'per version, but you have: %s and %s' % (self.python, path))
223 'per version, but you have: %s and %s' % (self.python, path))
199 self.python = script.PythonScript(path)
224 self.python = script.PythonScript(path)
200
225
201
226
202 class Extensions:
227 class Extensions:
203 """A namespace for file extensions"""
228 """A namespace for file extensions"""
204 py = 'py'
229 py = 'py'
205 sql = 'sql'
230 sql = 'sql'
206
231
207 def str_to_filename(s):
232 def str_to_filename(s):
208 """Replaces spaces, (double and single) quotes
233 """Replaces spaces, (double and single) quotes
209 and double underscores to underscores
234 and double underscores to underscores
210 """
235 """
211
236
212 s = s.replace(' ', '_').replace('"', '_').replace("'", '_').replace(".", "_")
237 s = s.replace(' ', '_').replace('"', '_').replace("'", '_').replace(".", "_")
213 while '__' in s:
238 while '__' in s:
214 s = s.replace('__', '_')
239 s = s.replace('__', '_')
215 return s
240 return s
@@ -1,102 +1,116 b''
1 import logging
1 import logging
2 import datetime
2 import datetime
3
3
4 from sqlalchemy import *
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper
6 from sqlalchemy.orm import relation, backref, class_mapper
7 from sqlalchemy.orm.session import Session
7 from sqlalchemy.orm.session import Session
8
8
9 from rhodecode.lib.dbmigrate.migrate import *
9 from rhodecode.lib.dbmigrate.migrate import *
10 from rhodecode.lib.dbmigrate.migrate.changeset import *
10 from rhodecode.lib.dbmigrate.migrate.changeset import *
11
11
12 from rhodecode.model.meta import Base
12 from rhodecode.model.meta import Base
13
13
14 log = logging.getLogger(__name__)
14 log = logging.getLogger(__name__)
15
15
16 def upgrade(migrate_engine):
16 def upgrade(migrate_engine):
17 """ Upgrade operations go here.
17 """ Upgrade operations go here.
18 Don't create your own engine; bind migrate_engine to your metadata
18 Don't create your own engine; bind migrate_engine to your metadata
19 """
19 """
20
20
21 #==========================================================================
21 #==========================================================================
22 # Add table `groups``
22 # Add table `groups``
23 #==========================================================================
23 #==========================================================================
24 from rhodecode.model.db import Group
24 from rhodecode.model.db import Group
25 Group().__table__.create()
25 Group().__table__.create()
26
26
27 #==========================================================================
27 #==========================================================================
28 # Add table `group_to_perm`
28 # Add table `group_to_perm`
29 #==========================================================================
29 #==========================================================================
30 from rhodecode.model.db import GroupToPerm
30 from rhodecode.model.db import GroupToPerm
31 GroupToPerm().__table__.create()
31 GroupToPerm().__table__.create()
32
32
33 #==========================================================================
33 #==========================================================================
34 # Add table `users_groups`
34 # Add table `users_groups`
35 #==========================================================================
35 #==========================================================================
36 from rhodecode.model.db import UsersGroup
36 from rhodecode.model.db import UsersGroup
37 UsersGroup().__table__.create()
37 UsersGroup().__table__.create()
38
38
39 #==========================================================================
39 #==========================================================================
40 # Add table `users_groups_members`
40 # Add table `users_groups_members`
41 #==========================================================================
41 #==========================================================================
42 from rhodecode.model.db import UsersGroupMember
42 from rhodecode.model.db import UsersGroupMember
43 UsersGroupMember().__table__.create()
43 UsersGroupMember().__table__.create()
44
44
45 #==========================================================================
45 #==========================================================================
46 # Add table `users_group_repo_to_perm`
46 # Add table `users_group_repo_to_perm`
47 #==========================================================================
47 #==========================================================================
48 from rhodecode.model.db import UsersGroupRepoToPerm
48 from rhodecode.model.db import UsersGroupRepoToPerm
49 UsersGroupRepoToPerm().__table__.create()
49 UsersGroupRepoToPerm().__table__.create()
50
50
51 #==========================================================================
51 #==========================================================================
52 # Add table `users_group_to_perm`
52 # Add table `users_group_to_perm`
53 #==========================================================================
53 #==========================================================================
54 from rhodecode.model.db import UsersGroupToPerm
54 from rhodecode.model.db import UsersGroupToPerm
55 UsersGroupToPerm().__table__.create()
55 UsersGroupToPerm().__table__.create()
56
56
57 #==========================================================================
57 #==========================================================================
58 # Upgrade of `users` table
58 # Upgrade of `users` table
59 #==========================================================================
59 #==========================================================================
60 from rhodecode.model.db import User
60 from rhodecode.model.db import User
61
61
62 #add column
62 #add column
63 ldap_dn = Column("ldap_dn", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
63 ldap_dn = Column("ldap_dn", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
64 ldap_dn.create(User().__table__)
64 ldap_dn.create(User().__table__)
65
65
66 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
66 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
67 api_key.create(User().__table__)
67 api_key.create(User().__table__)
68
68
69 #remove old column
69 #remove old column
70 is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
70 is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
71 is_ldap.drop(User().__table__)
71 is_ldap.drop(User().__table__)
72
72
73
73
74 #==========================================================================
74 #==========================================================================
75 # Upgrade of `repositories` table
75 # Upgrade of `repositories` table
76 #==========================================================================
76 #==========================================================================
77 from rhodecode.model.db import Repository
77 from rhodecode.model.db import Repository
78
78
79 #ADD downloads column#
79 #ADD downloads column#
80 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
80 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
81 enable_downloads.create(Repository().__table__)
81 enable_downloads.create(Repository().__table__)
82
82
83 #ADD column created_on
84 created_on = Column('created_on', DateTime(timezone=False), nullable=True,
85 unique=None, default=datetime.datetime.now)
86 created_on.create(Repository().__table__)
87
83 #ADD group_id column#
88 #ADD group_id column#
84 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'),
89 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'),
85 nullable=True, unique=False, default=None)
90 nullable=True, unique=False, default=None)
86
91
87 group_id.create(Repository().__table__)
92 group_id.create(Repository().__table__)
88
93
89
94
90 #ADD clone_uri column#
95 #ADD clone_uri column#
91
96
92 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False,
97 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False,
93 assert_unicode=None),
98 assert_unicode=None),
94 nullable=True, unique=False, default=None)
99 nullable=True, unique=False, default=None)
95
100
96 clone_uri.create(Repository().__table__)
101 clone_uri.create(Repository().__table__)
102
103
104 #==========================================================================
105 # Upgrade of `user_followings` table
106 #==========================================================================
107
108 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
109 follows_from.create(Repository().__table__)
110
97 return
111 return
98
112
99
113
100 def downgrade(migrate_engine):
114 def downgrade(migrate_engine):
101 meta = MetaData()
115 meta = MetaData()
102 meta.bind = migrate_engine
116 meta.bind = migrate_engine
General Comments 0
You need to be logged in to leave comments. Login now