##// END OF EJS Templates
Synced with latest sqlalchemy-migrate, added new upcomming migration for 1.3
marcink -
r1632:5b2cf21b beta
parent child Browse files
Show More
@@ -0,0 +1,24 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.model.db
4 ~~~~~~~~~~~~~~~~~~
5
6 Database Models for RhodeCode
7
8 :created_on: Apr 08, 2010
9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>. No newline at end of file
@@ -0,0 +1,28 b''
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper
7 from sqlalchemy.orm.session import Session
8
9 from rhodecode.lib.dbmigrate.migrate import *
10 from rhodecode.lib.dbmigrate.migrate.changeset import *
11
12 from rhodecode.model.meta import Base
13
14 log = logging.getLogger(__name__)
15
16 def upgrade(migrate_engine):
17 """ Upgrade operations go here.
18 Don't create your own engine; bind migrate_engine to your metadata
19 """
20
21
22
23 return
24
25
26 def downgrade(migrate_engine):
27 meta = MetaData()
28 meta.bind = migrate_engine
@@ -1,11 +1,11 b''
1 1 """
2 2 SQLAlchemy migrate provides two APIs :mod:`migrate.versioning` for
3 3 database schema version and repository management and
4 4 :mod:`migrate.changeset` that allows to define database schema changes
5 5 using Python.
6 6 """
7 7
8 8 from rhodecode.lib.dbmigrate.migrate.versioning import *
9 9 from rhodecode.lib.dbmigrate.migrate.changeset import *
10 10
11 __version__ = '0.7.2.dev' No newline at end of file
11 __version__ = '0.7.3.dev' No newline at end of file
@@ -1,155 +1,155 b''
1 1 """
2 2 `SQLite`_ database specific implementations of changeset classes.
3 3
4 4 .. _`SQLite`: http://www.sqlite.org/
5 5 """
6 6 from UserDict import DictMixin
7 7 from copy import copy
8 8
9 9 from sqlalchemy.databases import sqlite as sa_base
10 10
11 11 from rhodecode.lib.dbmigrate.migrate import exceptions
12 12 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
13 13
14 14
15 15 if not SQLA_06:
16 16 SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator
17 17 else:
18 18 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
19 19
20 20 class SQLiteCommon(object):
21 21
22 22 def _not_supported(self, op):
23 23 raise exceptions.NotSupportedError("SQLite does not support "
24 24 "%s; see http://www.sqlite.org/lang_altertable.html" % op)
25 25
26 26
27 27 class SQLiteHelper(SQLiteCommon):
28 28
29 29 def recreate_table(self,table,column=None,delta=None):
30 30 table_name = self.preparer.format_table(table)
31 31
32 32 # we remove all indexes so as not to have
33 33 # problems during copy and re-create
34 34 for index in table.indexes:
35 35 index.drop()
36 36
37 37 self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name)
38 38 self.execute()
39 39
40 40 insertion_string = self._modify_table(table, column, delta)
41 41
42 table.create()
42 table.create(bind=self.connection)
43 43 self.append(insertion_string % {'table_name': table_name})
44 44 self.execute()
45 45 self.append('DROP TABLE migration_tmp')
46 46 self.execute()
47 47
48 48 def visit_column(self, delta):
49 49 if isinstance(delta, DictMixin):
50 50 column = delta.result_column
51 51 table = self._to_table(delta.table)
52 52 else:
53 53 column = delta
54 54 table = self._to_table(column.table)
55 55 self.recreate_table(table,column,delta)
56 56
57 57 class SQLiteColumnGenerator(SQLiteSchemaGenerator,
58 58 ansisql.ANSIColumnGenerator,
59 59 # at the end so we get the normal
60 60 # visit_column by default
61 61 SQLiteHelper,
62 62 SQLiteCommon
63 63 ):
64 64 """SQLite ColumnGenerator"""
65 65
66 66 def _modify_table(self, table, column, delta):
67 67 columns = ' ,'.join(map(
68 68 self.preparer.format_column,
69 69 [c for c in table.columns if c.name!=column.name]))
70 70 return ('INSERT INTO %%(table_name)s (%(cols)s) '
71 71 'SELECT %(cols)s from migration_tmp')%{'cols':columns}
72 72
73 73 def visit_column(self,column):
74 74 if column.foreign_keys:
75 75 SQLiteHelper.visit_column(self,column)
76 76 else:
77 77 super(SQLiteColumnGenerator,self).visit_column(column)
78 78
79 79 class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper):
80 80 """SQLite ColumnDropper"""
81 81
82 82 def _modify_table(self, table, column, delta):
83 83
84 84 columns = ' ,'.join(map(self.preparer.format_column, table.columns))
85 85 return 'INSERT INTO %(table_name)s SELECT ' + columns + \
86 86 ' from migration_tmp'
87 87
88 88 def visit_column(self,column):
89 89 # For SQLite, we *have* to remove the column here so the table
90 90 # is re-created properly.
91 91 column.remove_from_table(column.table,unset_table=False)
92 92 super(SQLiteColumnDropper,self).visit_column(column)
93 93
94 94
95 95 class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger):
96 96 """SQLite SchemaChanger"""
97 97
98 98 def _modify_table(self, table, column, delta):
99 99 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
100 100
101 101 def visit_index(self, index):
102 102 """Does not support ALTER INDEX"""
103 103 self._not_supported('ALTER INDEX')
104 104
105 105
106 106 class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon):
107 107
108 108 def visit_migrate_primary_key_constraint(self, constraint):
109 109 tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )"
110 110 cols = ', '.join(map(self.preparer.format_column, constraint.columns))
111 111 tname = self.preparer.format_table(constraint.table)
112 112 name = self.get_constraint_name(constraint)
113 113 msg = tmpl % (name, tname, cols)
114 114 self.append(msg)
115 115 self.execute()
116 116
117 117 def _modify_table(self, table, column, delta):
118 118 return 'INSERT INTO %(table_name)s SELECT * from migration_tmp'
119 119
120 120 def visit_migrate_foreign_key_constraint(self, *p, **k):
121 121 self.recreate_table(p[0].table)
122 122
123 123 def visit_migrate_unique_constraint(self, *p, **k):
124 124 self.recreate_table(p[0].table)
125 125
126 126
127 127 class SQLiteConstraintDropper(ansisql.ANSIColumnDropper,
128 128 SQLiteCommon,
129 129 ansisql.ANSIConstraintCommon):
130 130
131 131 def visit_migrate_primary_key_constraint(self, constraint):
132 132 tmpl = "DROP INDEX %s "
133 133 name = self.get_constraint_name(constraint)
134 134 msg = tmpl % (name)
135 135 self.append(msg)
136 136 self.execute()
137 137
138 138 def visit_migrate_foreign_key_constraint(self, *p, **k):
139 139 self._not_supported('ALTER TABLE DROP CONSTRAINT')
140 140
141 141 def visit_migrate_check_constraint(self, *p, **k):
142 142 self._not_supported('ALTER TABLE DROP CONSTRAINT')
143 143
144 144 def visit_migrate_unique_constraint(self, *p, **k):
145 145 self._not_supported('ALTER TABLE DROP CONSTRAINT')
146 146
147 147
148 148 # TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index
149 149
150 150 class SQLiteDialect(ansisql.ANSIDialect):
151 151 columngenerator = SQLiteColumnGenerator
152 152 columndropper = SQLiteColumnDropper
153 153 schemachanger = SQLiteSchemaChanger
154 154 constraintgenerator = SQLiteConstraintGenerator
155 155 constraintdropper = SQLiteConstraintDropper
@@ -1,88 +1,87 b''
1 1 """
2 2 Provide exception classes for :mod:`migrate`
3 3 """
4 4
5 5
6 6 class Error(Exception):
7 7 """Error base class."""
8 8
9 9
10 10 class ApiError(Error):
11 11 """Base class for API errors."""
12 12
13 13
14 14 class KnownError(ApiError):
15 15 """A known error condition."""
16 16
17 17
18 18 class UsageError(ApiError):
19 19 """A known error condition where help should be displayed."""
20 20
21 21
22 22 class ControlledSchemaError(Error):
23 23 """Base class for controlled schema errors."""
24 24
25 25
26 26 class InvalidVersionError(ControlledSchemaError):
27 27 """Invalid version number."""
28 28
29 29
30 30 class DatabaseNotControlledError(ControlledSchemaError):
31 31 """Database should be under version control, but it's not."""
32 32
33 33
34 34 class DatabaseAlreadyControlledError(ControlledSchemaError):
35 35 """Database shouldn't be under version control, but it is"""
36 36
37 37
38 38 class WrongRepositoryError(ControlledSchemaError):
39 39 """This database is under version control by another repository."""
40 40
41 41
42 42 class NoSuchTableError(ControlledSchemaError):
43 43 """The table does not exist."""
44 44
45 45
46 46 class PathError(Error):
47 47 """Base class for path errors."""
48 48
49 49
50 50 class PathNotFoundError(PathError):
51 51 """A path with no file was required; found a file."""
52 52
53 53
54 54 class PathFoundError(PathError):
55 55 """A path with a file was required; found no file."""
56 56
57 57
58 58 class RepositoryError(Error):
59 59 """Base class for repository errors."""
60 60
61 61
62 62 class InvalidRepositoryError(RepositoryError):
63 63 """Invalid repository error."""
64 64
65 65
66 66 class ScriptError(Error):
67 67 """Base class for script errors."""
68 68
69 69
70 70 class InvalidScriptError(ScriptError):
71 71 """Invalid script error."""
72 72
73 73
74 74 class InvalidVersionError(Error):
75 75 """Invalid version error."""
76 76
77 77 # migrate.changeset
78 78
79 79 class NotSupportedError(Error):
80 80 """Not supported error"""
81 81
82 82
83 83 class InvalidConstraintError(Error):
84 84 """Invalid constraint error"""
85 85
86
87 86 class MigrateDeprecationWarning(DeprecationWarning):
88 87 """Warning for deprecated features in Migrate"""
@@ -1,383 +1,384 b''
1 1 """
2 2 This module provides an external API to the versioning system.
3 3
4 4 .. versionchanged:: 0.6.0
5 5 :func:`migrate.versioning.api.test` and schema diff functions
6 6 changed order of positional arguments so all accept `url` and `repository`
7 7 as first arguments.
8 8
9 9 .. versionchanged:: 0.5.4
10 10 ``--preview_sql`` displays source file when using SQL scripts.
11 11 If Python script is used, it runs the action with mocked engine and
12 12 returns captured SQL statements.
13 13
14 14 .. versionchanged:: 0.5.4
15 15 Deprecated ``--echo`` parameter in favour of new
16 16 :func:`migrate.versioning.util.construct_engine` behavior.
17 17 """
18 18
19 19 # Dear migrate developers,
20 20 #
21 21 # please do not comment this module using sphinx syntax because its
22 22 # docstrings are presented as user help and most users cannot
23 23 # interpret sphinx annotated ReStructuredText.
24 24 #
25 25 # Thanks,
26 26 # Jan Dittberner
27 27
28 28 import sys
29 29 import inspect
30 30 import logging
31 31
32 32 from rhodecode.lib.dbmigrate.migrate import exceptions
33 33 from rhodecode.lib.dbmigrate.migrate.versioning import repository, schema, version, \
34 34 script as script_ # command name conflict
35 35 from rhodecode.lib.dbmigrate.migrate.versioning.util import catch_known_errors, with_engine
36 36
37 37
38 38 log = logging.getLogger(__name__)
39 39 command_desc = {
40 40 'help': 'displays help on a given command',
41 41 'create': 'create an empty repository at the specified path',
42 42 'script': 'create an empty change Python script',
43 43 'script_sql': 'create empty change SQL scripts for given database',
44 44 'version': 'display the latest version available in a repository',
45 45 'db_version': 'show the current version of the repository under version control',
46 46 'source': 'display the Python code for a particular version in this repository',
47 47 'version_control': 'mark a database as under this repository\'s version control',
48 48 'upgrade': 'upgrade a database to a later version',
49 49 'downgrade': 'downgrade a database to an earlier version',
50 50 'drop_version_control': 'removes version control from a database',
51 51 'manage': 'creates a Python script that runs Migrate with a set of default values',
52 52 'test': 'performs the upgrade and downgrade command on the given database',
53 53 'compare_model_to_db': 'compare MetaData against the current database state',
54 54 'create_model': 'dump the current database as a Python model to stdout',
55 55 'make_update_script_for_model': 'create a script changing the old MetaData to the new (current) MetaData',
56 56 'update_db_from_model': 'modify the database to match the structure of the current MetaData',
57 57 }
58 58 __all__ = command_desc.keys()
59 59
60 60 Repository = repository.Repository
61 61 ControlledSchema = schema.ControlledSchema
62 62 VerNum = version.VerNum
63 63 PythonScript = script_.PythonScript
64 64 SqlScript = script_.SqlScript
65 65
66 66
67 67 # deprecated
68 68 def help(cmd=None, **opts):
69 69 """%prog help COMMAND
70 70
71 71 Displays help on a given command.
72 72 """
73 73 if cmd is None:
74 74 raise exceptions.UsageError(None)
75 75 try:
76 76 func = globals()[cmd]
77 77 except:
78 78 raise exceptions.UsageError(
79 79 "'%s' isn't a valid command. Try 'help COMMAND'" % cmd)
80 80 ret = func.__doc__
81 81 if sys.argv[0]:
82 82 ret = ret.replace('%prog', sys.argv[0])
83 83 return ret
84 84
85 85 @catch_known_errors
86 86 def create(repository, name, **opts):
87 87 """%prog create REPOSITORY_PATH NAME [--table=TABLE]
88 88
89 89 Create an empty repository at the specified path.
90 90
91 91 You can specify the version_table to be used; by default, it is
92 92 'migrate_version'. This table is created in all version-controlled
93 93 databases.
94 94 """
95 95 repo_path = Repository.create(repository, name, **opts)
96 96
97 97
98 98 @catch_known_errors
99 99 def script(description, repository, **opts):
100 100 """%prog script DESCRIPTION REPOSITORY_PATH
101 101
102 102 Create an empty change script using the next unused version number
103 103 appended with the given description.
104 104
105 105 For instance, manage.py script "Add initial tables" creates:
106 106 repository/versions/001_Add_initial_tables.py
107 107 """
108 108 repo = Repository(repository)
109 109 repo.create_script(description, **opts)
110 110
111 111
112 112 @catch_known_errors
113 113 def script_sql(database, description, repository, **opts):
114 114 """%prog script_sql DATABASE DESCRIPTION REPOSITORY_PATH
115 115
116 116 Create empty change SQL scripts for given DATABASE, where DATABASE
117 117 is either specific ('postgresql', 'mysql', 'oracle', 'sqlite', etc.)
118 118 or generic ('default').
119 119
120 120 For instance, manage.py script_sql postgresql description creates:
121 121 repository/versions/001_description_postgresql_upgrade.sql and
122 repository/versions/001_description_postgresql_postgres.sql
122 repository/versions/001_description_postgresql_downgrade.sql
123 123 """
124 124 repo = Repository(repository)
125 125 repo.create_script_sql(database, description, **opts)
126 126
127 127
128 128 def version(repository, **opts):
129 129 """%prog version REPOSITORY_PATH
130 130
131 131 Display the latest version available in a repository.
132 132 """
133 133 repo = Repository(repository)
134 134 return repo.latest
135 135
136 136
137 137 @with_engine
138 138 def db_version(url, repository, **opts):
139 139 """%prog db_version URL REPOSITORY_PATH
140 140
141 141 Show the current version of the repository with the given
142 142 connection string, under version control of the specified
143 143 repository.
144 144
145 145 The url should be any valid SQLAlchemy connection string.
146 146 """
147 147 engine = opts.pop('engine')
148 148 schema = ControlledSchema(engine, repository)
149 149 return schema.version
150 150
151 151
152 152 def source(version, dest=None, repository=None, **opts):
153 153 """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH
154 154
155 155 Display the Python code for a particular version in this
156 156 repository. Save it to the file at DESTINATION or, if omitted,
157 157 send to stdout.
158 158 """
159 159 if repository is None:
160 160 raise exceptions.UsageError("A repository must be specified")
161 161 repo = Repository(repository)
162 162 ret = repo.version(version).script().source()
163 163 if dest is not None:
164 164 dest = open(dest, 'w')
165 165 dest.write(ret)
166 166 dest.close()
167 167 ret = None
168 168 return ret
169 169
170 170
171 171 def upgrade(url, repository, version=None, **opts):
172 172 """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql]
173 173
174 174 Upgrade a database to a later version.
175 175
176 176 This runs the upgrade() function defined in your change scripts.
177 177
178 178 By default, the database is updated to the latest available
179 179 version. You may specify a version instead, if you wish.
180 180
181 181 You may preview the Python or SQL code to be executed, rather than
182 182 actually executing it, using the appropriate 'preview' option.
183 183 """
184 184 err = "Cannot upgrade a database of version %s to version %s. "\
185 185 "Try 'downgrade' instead."
186 186 return _migrate(url, repository, version, upgrade=True, err=err, **opts)
187 187
188 188
189 189 def downgrade(url, repository, version, **opts):
190 190 """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql]
191 191
192 192 Downgrade a database to an earlier version.
193 193
194 194 This is the reverse of upgrade; this runs the downgrade() function
195 195 defined in your change scripts.
196 196
197 197 You may preview the Python or SQL code to be executed, rather than
198 198 actually executing it, using the appropriate 'preview' option.
199 199 """
200 200 err = "Cannot downgrade a database of version %s to version %s. "\
201 201 "Try 'upgrade' instead."
202 202 return _migrate(url, repository, version, upgrade=False, err=err, **opts)
203 203
204 204 @with_engine
205 205 def test(url, repository, **opts):
206 206 """%prog test URL REPOSITORY_PATH [VERSION]
207 207
208 208 Performs the upgrade and downgrade option on the given
209 209 database. This is not a real test and may leave the database in a
210 210 bad state. You should therefore better run the test on a copy of
211 211 your database.
212 212 """
213 213 engine = opts.pop('engine')
214 214 repos = Repository(repository)
215 script = repos.version(None).script()
216 215
217 216 # Upgrade
218 217 log.info("Upgrading...")
218 script = repos.version(None).script(engine.name, 'upgrade')
219 219 script.run(engine, 1)
220 220 log.info("done")
221 221
222 222 log.info("Downgrading...")
223 script = repos.version(None).script(engine.name, 'downgrade')
223 224 script.run(engine, -1)
224 225 log.info("done")
225 226 log.info("Success")
226 227
227 228
228 229 @with_engine
229 230 def version_control(url, repository, version=None, **opts):
230 231 """%prog version_control URL REPOSITORY_PATH [VERSION]
231 232
232 233 Mark a database as under this repository's version control.
233 234
234 235 Once a database is under version control, schema changes should
235 236 only be done via change scripts in this repository.
236 237
237 238 This creates the table version_table in the database.
238 239
239 240 The url should be any valid SQLAlchemy connection string.
240 241
241 242 By default, the database begins at version 0 and is assumed to be
242 243 empty. If the database is not empty, you may specify a version at
243 244 which to begin instead. No attempt is made to verify this
244 245 version's correctness - the database schema is expected to be
245 246 identical to what it would be if the database were created from
246 247 scratch.
247 248 """
248 249 engine = opts.pop('engine')
249 250 ControlledSchema.create(engine, repository, version)
250 251
251 252
252 253 @with_engine
253 254 def drop_version_control(url, repository, **opts):
254 255 """%prog drop_version_control URL REPOSITORY_PATH
255 256
256 257 Removes version control from a database.
257 258 """
258 259 engine = opts.pop('engine')
259 260 schema = ControlledSchema(engine, repository)
260 261 schema.drop()
261 262
262 263
263 264 def manage(file, **opts):
264 265 """%prog manage FILENAME [VARIABLES...]
265 266
266 267 Creates a script that runs Migrate with a set of default values.
267 268
268 269 For example::
269 270
270 271 %prog manage manage.py --repository=/path/to/repository \
271 272 --url=sqlite:///project.db
272 273
273 274 would create the script manage.py. The following two commands
274 275 would then have exactly the same results::
275 276
276 277 python manage.py version
277 278 %prog version --repository=/path/to/repository
278 279 """
279 280 Repository.create_manage_file(file, **opts)
280 281
281 282
282 283 @with_engine
283 284 def compare_model_to_db(url, repository, model, **opts):
284 285 """%prog compare_model_to_db URL REPOSITORY_PATH MODEL
285 286
286 287 Compare the current model (assumed to be a module level variable
287 288 of type sqlalchemy.MetaData) against the current database.
288 289
289 290 NOTE: This is EXPERIMENTAL.
290 291 """ # TODO: get rid of EXPERIMENTAL label
291 292 engine = opts.pop('engine')
292 293 return ControlledSchema.compare_model_to_db(engine, model, repository)
293 294
294 295
295 296 @with_engine
296 297 def create_model(url, repository, **opts):
297 298 """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True]
298 299
299 300 Dump the current database as a Python model to stdout.
300 301
301 302 NOTE: This is EXPERIMENTAL.
302 303 """ # TODO: get rid of EXPERIMENTAL label
303 304 engine = opts.pop('engine')
304 305 declarative = opts.get('declarative', False)
305 306 return ControlledSchema.create_model(engine, repository, declarative)
306 307
307 308
308 309 @catch_known_errors
309 310 @with_engine
310 311 def make_update_script_for_model(url, repository, oldmodel, model, **opts):
311 312 """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
312 313
313 314 Create a script changing the old Python model to the new (current)
314 315 Python model, sending to stdout.
315 316
316 317 NOTE: This is EXPERIMENTAL.
317 318 """ # TODO: get rid of EXPERIMENTAL label
318 319 engine = opts.pop('engine')
319 320 return PythonScript.make_update_script_for_model(
320 321 engine, oldmodel, model, repository, **opts)
321 322
322 323
323 324 @with_engine
324 325 def update_db_from_model(url, repository, model, **opts):
325 326 """%prog update_db_from_model URL REPOSITORY_PATH MODEL
326 327
327 328 Modify the database to match the structure of the current Python
328 329 model. This also sets the db_version number to the latest in the
329 330 repository.
330 331
331 332 NOTE: This is EXPERIMENTAL.
332 333 """ # TODO: get rid of EXPERIMENTAL label
333 334 engine = opts.pop('engine')
334 335 schema = ControlledSchema(engine, repository)
335 336 schema.update_db_from_model(model)
336 337
337 338 @with_engine
338 339 def _migrate(url, repository, version, upgrade, err, **opts):
339 340 engine = opts.pop('engine')
340 341 url = str(engine.url)
341 342 schema = ControlledSchema(engine, repository)
342 343 version = _migrate_version(schema, version, upgrade, err)
343 344
344 345 changeset = schema.changeset(version)
345 346 for ver, change in changeset:
346 347 nextver = ver + changeset.step
347 348 log.info('%s -> %s... ', ver, nextver)
348 349
349 350 if opts.get('preview_sql'):
350 351 if isinstance(change, PythonScript):
351 352 log.info(change.preview_sql(url, changeset.step, **opts))
352 353 elif isinstance(change, SqlScript):
353 354 log.info(change.source())
354 355
355 356 elif opts.get('preview_py'):
356 357 if not isinstance(change, PythonScript):
357 358 raise exceptions.UsageError("Python source can be only displayed"
358 359 " for python migration files")
359 360 source_ver = max(ver, nextver)
360 361 module = schema.repository.version(source_ver).script().module
361 362 funcname = upgrade and "upgrade" or "downgrade"
362 363 func = getattr(module, funcname)
363 364 log.info(inspect.getsource(func))
364 365 else:
365 366 schema.runchange(ver, change, changeset.step)
366 367 log.info('done')
367 368
368 369
369 370 def _migrate_version(schema, version, upgrade, err):
370 371 if version is None:
371 372 return version
372 373 # Version is specified: ensure we're upgrading in the right direction
373 374 # (current version < target version for upgrading; reverse for down)
374 375 version = VerNum(version)
375 376 cur = schema.version
376 377 if upgrade is not None:
377 378 if upgrade:
378 379 direction = cur <= version
379 380 else:
380 381 direction = cur >= version
381 382 if not direction:
382 383 raise exceptions.KnownError(err % (cur, version))
383 384 return version
@@ -1,242 +1,242 b''
1 1 """
2 2 SQLAlchemy migrate repository management.
3 3 """
4 4 import os
5 5 import shutil
6 6 import string
7 7 import logging
8 8
9 9 from pkg_resources import resource_filename
10 10 from tempita import Template as TempitaTemplate
11 11
12 12 from rhodecode.lib.dbmigrate.migrate import exceptions
13 13 from rhodecode.lib.dbmigrate.migrate.versioning import version, pathed, cfgparse
14 14 from rhodecode.lib.dbmigrate.migrate.versioning.template import Template
15 15 from rhodecode.lib.dbmigrate.migrate.versioning.config import *
16 16
17 17
18 18 log = logging.getLogger(__name__)
19 19
20 20 class Changeset(dict):
21 21 """A collection of changes to be applied to a database.
22 22
23 23 Changesets are bound to a repository and manage a set of
24 24 scripts from that repository.
25 25
26 26 Behaves like a dict, for the most part. Keys are ordered based on step value.
27 27 """
28 28
29 29 def __init__(self, start, *changes, **k):
30 30 """
31 31 Give a start version; step must be explicitly stated.
32 32 """
33 33 self.step = k.pop('step', 1)
34 34 self.start = version.VerNum(start)
35 35 self.end = self.start
36 36 for change in changes:
37 37 self.add(change)
38 38
39 39 def __iter__(self):
40 40 return iter(self.items())
41 41
42 42 def keys(self):
43 43 """
44 44 In a series of upgrades x -> y, keys are version x. Sorted.
45 45 """
46 46 ret = super(Changeset, self).keys()
47 47 # Reverse order if downgrading
48 48 ret.sort(reverse=(self.step < 1))
49 49 return ret
50 50
51 51 def values(self):
52 52 return [self[k] for k in self.keys()]
53 53
54 54 def items(self):
55 55 return zip(self.keys(), self.values())
56 56
57 57 def add(self, change):
58 58 """Add new change to changeset"""
59 59 key = self.end
60 60 self.end += self.step
61 61 self[key] = change
62 62
63 63 def run(self, *p, **k):
64 64 """Run the changeset scripts"""
65 65 for version, script in self:
66 66 script.run(*p, **k)
67 67
68 68
69 69 class Repository(pathed.Pathed):
70 70 """A project's change script repository"""
71 71
72 72 _config = 'migrate.cfg'
73 73 _versions = 'versions'
74 74
75 75 def __init__(self, path):
76 76 log.debug('Loading repository %s...' % path)
77 77 self.verify(path)
78 78 super(Repository, self).__init__(path)
79 79 self.config = cfgparse.Config(os.path.join(self.path, self._config))
80 80 self.versions = version.Collection(os.path.join(self.path,
81 81 self._versions))
82 82 log.debug('Repository %s loaded successfully' % path)
83 83 log.debug('Config: %r' % self.config.to_dict())
84 84
85 85 @classmethod
86 86 def verify(cls, path):
87 87 """
88 88 Ensure the target path is a valid repository.
89 89
90 90 :raises: :exc:`InvalidRepositoryError <migrate.exceptions.InvalidRepositoryError>`
91 91 """
92 92 # Ensure the existence of required files
93 93 try:
94 94 cls.require_found(path)
95 95 cls.require_found(os.path.join(path, cls._config))
96 96 cls.require_found(os.path.join(path, cls._versions))
97 97 except exceptions.PathNotFoundError, e:
98 98 raise exceptions.InvalidRepositoryError(path)
99 99
100 100 @classmethod
101 101 def prepare_config(cls, tmpl_dir, name, options=None):
102 102 """
103 103 Prepare a project configuration file for a new project.
104 104
105 105 :param tmpl_dir: Path to Repository template
106 106 :param config_file: Name of the config file in Repository template
107 107 :param name: Repository name
108 108 :type tmpl_dir: string
109 109 :type config_file: string
110 110 :type name: string
111 111 :returns: Populated config file
112 112 """
113 113 if options is None:
114 114 options = {}
115 115 options.setdefault('version_table', 'migrate_version')
116 116 options.setdefault('repository_id', name)
117 117 options.setdefault('required_dbs', [])
118 options.setdefault('use_timestamp_numbering', '0')
118 options.setdefault('use_timestamp_numbering', False)
119 119
120 120 tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
121 121 ret = TempitaTemplate(tmpl).substitute(options)
122 122
123 123 # cleanup
124 124 del options['__template_name__']
125 125
126 126 return ret
127 127
128 128 @classmethod
129 129 def create(cls, path, name, **opts):
130 130 """Create a repository at a specified path"""
131 131 cls.require_notfound(path)
132 132 theme = opts.pop('templates_theme', None)
133 133 t_path = opts.pop('templates_path', None)
134 134
135 135 # Create repository
136 136 tmpl_dir = Template(t_path).get_repository(theme=theme)
137 137 shutil.copytree(tmpl_dir, path)
138 138
139 139 # Edit config defaults
140 140 config_text = cls.prepare_config(tmpl_dir, name, options=opts)
141 141 fd = open(os.path.join(path, cls._config), 'w')
142 142 fd.write(config_text)
143 143 fd.close()
144 144
145 145 opts['repository_name'] = name
146 146
147 147 # Create a management script
148 148 manager = os.path.join(path, 'manage.py')
149 149 Repository.create_manage_file(manager, templates_theme=theme,
150 150 templates_path=t_path, **opts)
151 151
152 152 return cls(path)
153 153
154 154 def create_script(self, description, **k):
155 155 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
156 156
157 157 k['use_timestamp_numbering'] = self.use_timestamp_numbering
158 158 self.versions.create_new_python_version(description, **k)
159 159
160 160 def create_script_sql(self, database, description, **k):
161 161 """API to :meth:`migrate.versioning.version.Collection.create_new_sql_version`"""
162 162 k['use_timestamp_numbering'] = self.use_timestamp_numbering
163 163 self.versions.create_new_sql_version(database, description, **k)
164 164
165 165 @property
166 166 def latest(self):
167 167 """API to :attr:`migrate.versioning.version.Collection.latest`"""
168 168 return self.versions.latest
169 169
170 170 @property
171 171 def version_table(self):
172 172 """Returns version_table name specified in config"""
173 173 return self.config.get('db_settings', 'version_table')
174 174
175 175 @property
176 176 def id(self):
177 177 """Returns repository id specified in config"""
178 178 return self.config.get('db_settings', 'repository_id')
179 179
180 180 @property
181 181 def use_timestamp_numbering(self):
182 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
183 if self.config.has_option('db_settings', 'use_timestamp_numbering'):
184 return self.config.getboolean('db_settings', 'use_timestamp_numbering')
185 return False
186 186
187 187 def version(self, *p, **k):
188 188 """API to :attr:`migrate.versioning.version.Collection.version`"""
189 189 return self.versions.version(*p, **k)
190 190
191 191 @classmethod
192 192 def clear(cls):
193 193 # TODO: deletes repo
194 194 super(Repository, cls).clear()
195 195 version.Collection.clear()
196 196
197 197 def changeset(self, database, start, end=None):
198 198 """Create a changeset to migrate this database from ver. start to end/latest.
199 199
200 200 :param database: name of database to generate changeset
201 201 :param start: version to start at
202 202 :param end: version to end at (latest if None given)
203 203 :type database: string
204 204 :type start: int
205 205 :type end: int
206 206 :returns: :class:`Changeset instance <migration.versioning.repository.Changeset>`
207 207 """
208 208 start = version.VerNum(start)
209 209
210 210 if end is None:
211 211 end = self.latest
212 212 else:
213 213 end = version.VerNum(end)
214 214
215 215 if start <= end:
216 216 step = 1
217 217 range_mod = 1
218 218 op = 'upgrade'
219 219 else:
220 220 step = -1
221 221 range_mod = 0
222 222 op = 'downgrade'
223 223
224 224 versions = range(start + range_mod, end + range_mod, step)
225 225 changes = [self.version(v).script(database, op) for v in versions]
226 226 ret = Changeset(start, step=step, *changes)
227 227 return ret
228 228
229 229 @classmethod
230 230 def create_manage_file(cls, file_, **opts):
231 231 """Create a project management script (manage.py)
232 232
233 233 :param file_: Destination file to be written
234 234 :param opts: Options that are passed to :func:`migrate.versioning.shell.main`
235 235 """
236 236 mng_file = Template(opts.pop('templates_path', None))\
237 237 .get_manage(theme=opts.pop('templates_theme', None))
238 238
239 239 tmpl = open(mng_file).read()
240 240 fd = open(file_, 'w')
241 241 fd.write(TempitaTemplate(tmpl).substitute(opts))
242 242 fd.close()
@@ -1,285 +1,293 b''
1 1 """
2 2 Schema differencing support.
3 3 """
4 4
5 5 import logging
6 6 import sqlalchemy
7 7
8 8 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06
9 9 from sqlalchemy.types import Float
10 10
11 11 log = logging.getLogger(__name__)
12 12
13 13 def getDiffOfModelAgainstDatabase(metadata, engine, excludeTables=None):
14 14 """
15 15 Return differences of model against database.
16 16
17 17 :return: object which will evaluate to :keyword:`True` if there \
18 18 are differences else :keyword:`False`.
19 19 """
20 return SchemaDiff(metadata,
21 sqlalchemy.MetaData(engine, reflect=True),
20 db_metadata = sqlalchemy.MetaData(engine, reflect=True)
21
22 # sqlite will include a dynamically generated 'sqlite_sequence' table if
23 # there are autoincrement sequences in the database; this should not be
24 # compared.
25 if engine.dialect.name == 'sqlite':
26 if 'sqlite_sequence' in db_metadata.tables:
27 db_metadata.remove(db_metadata.tables['sqlite_sequence'])
28
29 return SchemaDiff(metadata, db_metadata,
22 30 labelA='model',
23 31 labelB='database',
24 32 excludeTables=excludeTables)
25 33
26 34
27 35 def getDiffOfModelAgainstModel(metadataA, metadataB, excludeTables=None):
28 36 """
29 37 Return differences of model against another model.
30 38
31 39 :return: object which will evaluate to :keyword:`True` if there \
32 40 are differences else :keyword:`False`.
33 41 """
34 42 return SchemaDiff(metadataA, metadataB, excludeTables)
35 43
36 44
37 45 class ColDiff(object):
38 46 """
39 47 Container for differences in one :class:`~sqlalchemy.schema.Column`
40 48 between two :class:`~sqlalchemy.schema.Table` instances, ``A``
41 49 and ``B``.
42 50
43 51 .. attribute:: col_A
44 52
45 53 The :class:`~sqlalchemy.schema.Column` object for A.
46 54
47 55 .. attribute:: col_B
48 56
49 57 The :class:`~sqlalchemy.schema.Column` object for B.
50 58
51 59 .. attribute:: type_A
52 60
53 61 The most generic type of the :class:`~sqlalchemy.schema.Column`
54 62 object in A.
55 63
56 64 .. attribute:: type_B
57 65
58 66 The most generic type of the :class:`~sqlalchemy.schema.Column`
59 67 object in A.
60 68
61 69 """
62 70
63 71 diff = False
64 72
65 73 def __init__(self,col_A,col_B):
66 74 self.col_A = col_A
67 75 self.col_B = col_B
68 76
69 77 self.type_A = col_A.type
70 78 self.type_B = col_B.type
71 79
72 80 self.affinity_A = self.type_A._type_affinity
73 81 self.affinity_B = self.type_B._type_affinity
74 82
75 83 if self.affinity_A is not self.affinity_B:
76 84 self.diff = True
77 85 return
78 86
79 87 if isinstance(self.type_A,Float) or isinstance(self.type_B,Float):
80 88 if not (isinstance(self.type_A,Float) and isinstance(self.type_B,Float)):
81 89 self.diff=True
82 90 return
83 91
84 92 for attr in ('precision','scale','length'):
85 93 A = getattr(self.type_A,attr,None)
86 94 B = getattr(self.type_B,attr,None)
87 95 if not (A is None or B is None) and A!=B:
88 96 self.diff=True
89 97 return
90 98
91 99 def __nonzero__(self):
92 100 return self.diff
93 101
94 102 class TableDiff(object):
95 103 """
96 104 Container for differences in one :class:`~sqlalchemy.schema.Table`
97 105 between two :class:`~sqlalchemy.schema.MetaData` instances, ``A``
98 106 and ``B``.
99 107
100 108 .. attribute:: columns_missing_from_A
101 109
102 110 A sequence of column names that were found in B but weren't in
103 111 A.
104 112
105 113 .. attribute:: columns_missing_from_B
106 114
107 115 A sequence of column names that were found in A but weren't in
108 116 B.
109 117
110 118 .. attribute:: columns_different
111 119
112 120 A dictionary containing information about columns that were
113 121 found to be different.
114 122 It maps column names to a :class:`ColDiff` objects describing the
115 123 differences found.
116 124 """
117 125 __slots__ = (
118 126 'columns_missing_from_A',
119 127 'columns_missing_from_B',
120 128 'columns_different',
121 129 )
122 130
123 131 def __nonzero__(self):
124 132 return bool(
125 133 self.columns_missing_from_A or
126 134 self.columns_missing_from_B or
127 135 self.columns_different
128 136 )
129 137
130 138 class SchemaDiff(object):
131 139 """
132 140 Compute the difference between two :class:`~sqlalchemy.schema.MetaData`
133 141 objects.
134 142
135 143 The string representation of a :class:`SchemaDiff` will summarise
136 144 the changes found between the two
137 145 :class:`~sqlalchemy.schema.MetaData` objects.
138 146
139 147 The length of a :class:`SchemaDiff` will give the number of
140 148 changes found, enabling it to be used much like a boolean in
141 149 expressions.
142 150
143 151 :param metadataA:
144 152 First :class:`~sqlalchemy.schema.MetaData` to compare.
145 153
146 154 :param metadataB:
147 155 Second :class:`~sqlalchemy.schema.MetaData` to compare.
148 156
149 157 :param labelA:
150 158 The label to use in messages about the first
151 159 :class:`~sqlalchemy.schema.MetaData`.
152 160
153 161 :param labelB:
154 162 The label to use in messages about the second
155 163 :class:`~sqlalchemy.schema.MetaData`.
156 164
157 165 :param excludeTables:
158 166 A sequence of table names to exclude.
159 167
160 168 .. attribute:: tables_missing_from_A
161 169
162 170 A sequence of table names that were found in B but weren't in
163 171 A.
164 172
165 173 .. attribute:: tables_missing_from_B
166 174
167 175 A sequence of table names that were found in A but weren't in
168 176 B.
169 177
170 178 .. attribute:: tables_different
171 179
172 180 A dictionary containing information about tables that were found
173 181 to be different.
174 182 It maps table names to a :class:`TableDiff` objects describing the
175 183 differences found.
176 184 """
177 185
178 186 def __init__(self,
179 187 metadataA, metadataB,
180 188 labelA='metadataA',
181 189 labelB='metadataB',
182 190 excludeTables=None):
183 191
184 192 self.metadataA, self.metadataB = metadataA, metadataB
185 193 self.labelA, self.labelB = labelA, labelB
186 194 self.label_width = max(len(labelA),len(labelB))
187 195 excludeTables = set(excludeTables or [])
188 196
189 197 A_table_names = set(metadataA.tables.keys())
190 198 B_table_names = set(metadataB.tables.keys())
191 199
192 200 self.tables_missing_from_A = sorted(
193 201 B_table_names - A_table_names - excludeTables
194 202 )
195 203 self.tables_missing_from_B = sorted(
196 204 A_table_names - B_table_names - excludeTables
197 205 )
198 206
199 207 self.tables_different = {}
200 208 for table_name in A_table_names.intersection(B_table_names):
201 209
202 210 td = TableDiff()
203 211
204 212 A_table = metadataA.tables[table_name]
205 213 B_table = metadataB.tables[table_name]
206 214
207 215 A_column_names = set(A_table.columns.keys())
208 216 B_column_names = set(B_table.columns.keys())
209 217
210 218 td.columns_missing_from_A = sorted(
211 219 B_column_names - A_column_names
212 220 )
213 221
214 222 td.columns_missing_from_B = sorted(
215 223 A_column_names - B_column_names
216 224 )
217 225
218 226 td.columns_different = {}
219 227
220 228 for col_name in A_column_names.intersection(B_column_names):
221 229
222 230 cd = ColDiff(
223 231 A_table.columns.get(col_name),
224 232 B_table.columns.get(col_name)
225 233 )
226 234
227 235 if cd:
228 236 td.columns_different[col_name]=cd
229 237
230 238 # XXX - index and constraint differences should
231 239 # be checked for here
232 240
233 241 if td:
234 242 self.tables_different[table_name]=td
235 243
236 244 def __str__(self):
237 245 ''' Summarize differences. '''
238 246 out = []
239 247 column_template =' %%%is: %%r' % self.label_width
240 248
241 249 for names,label in (
242 250 (self.tables_missing_from_A,self.labelA),
243 251 (self.tables_missing_from_B,self.labelB),
244 252 ):
245 253 if names:
246 254 out.append(
247 255 ' tables missing from %s: %s' % (
248 256 label,', '.join(sorted(names))
249 257 )
250 258 )
251 259
252 260 for name,td in sorted(self.tables_different.items()):
253 261 out.append(
254 262 ' table with differences: %s' % name
255 263 )
256 264 for names,label in (
257 265 (td.columns_missing_from_A,self.labelA),
258 266 (td.columns_missing_from_B,self.labelB),
259 267 ):
260 268 if names:
261 269 out.append(
262 270 ' %s missing these columns: %s' % (
263 271 label,', '.join(sorted(names))
264 272 )
265 273 )
266 274 for name,cd in td.columns_different.items():
267 275 out.append(' column with differences: %s' % name)
268 276 out.append(column_template % (self.labelA,cd.col_A))
269 277 out.append(column_template % (self.labelB,cd.col_B))
270 278
271 279 if out:
272 280 out.insert(0, 'Schema diffs:')
273 281 return '\n'.join(out)
274 282 else:
275 283 return 'No schema diffs'
276 284
277 285 def __len__(self):
278 286 """
279 287 Used in bool evaluation, return of 0 means no diffs.
280 288 """
281 289 return (
282 290 len(self.tables_missing_from_A) +
283 291 len(self.tables_missing_from_B) +
284 292 len(self.tables_different)
285 293 )
@@ -1,10 +1,12 b''
1 1 #!/usr/bin/env python
2 2 from migrate.versioning.shell import main
3 3
4 4 {{py:
5 5 _vars = locals().copy()
6 6 del _vars['__template_name__']
7 7 _vars.pop('repository_name', None)
8 8 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
9 9 }}
10 main({{ defaults }})
10
11 if __name__ == '__main__':
12 main({{ defaults }})
@@ -1,29 +1,30 b''
1 1 #!/usr/bin/python
2 2 # -*- coding: utf-8 -*-
3 3 import sys
4 4
5 5 from sqlalchemy import engine_from_config
6 6 from paste.deploy.loadwsgi import ConfigLoader
7 7
8 8 from migrate.versioning.shell import main
9 9 from {{ locals().pop('repository_name') }}.model import migrations
10 10
11 11
12 12 if '-c' in sys.argv:
13 13 pos = sys.argv.index('-c')
14 14 conf_path = sys.argv[pos + 1]
15 15 del sys.argv[pos:pos + 2]
16 16 else:
17 17 conf_path = 'development.ini'
18 18
19 19 {{py:
20 20 _vars = locals().copy()
21 21 del _vars['__template_name__']
22 22 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
23 23 }}
24 24
25 25 conf_dict = ConfigLoader(conf_path).parser._sections['app:main']
26 26
27 27 # migrate supports passing url as an existing Engine instance (since 0.6.0)
28 28 # usage: migrate -c path/to/config.ini COMMANDS
29 main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
29 if __name__ == '__main__':
30 main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
@@ -1,240 +1,238 b''
1 1 #!/usr/bin/env python
2 2 # -*- coding: utf-8 -*-
3 3
4 4 import os
5 5 import re
6 6 import shutil
7 7 import logging
8 8
9 9 from rhodecode.lib.dbmigrate.migrate import exceptions
10 10 from rhodecode.lib.dbmigrate.migrate.versioning import pathed, script
11 11 from datetime import datetime
12 12
13 13
14 14 log = logging.getLogger(__name__)
15 15
16 16 class VerNum(object):
17 17 """A version number that behaves like a string and int at the same time"""
18 18
19 19 _instances = dict()
20 20
21 21 def __new__(cls, value):
22 22 val = str(value)
23 23 if val not in cls._instances:
24 24 cls._instances[val] = super(VerNum, cls).__new__(cls)
25 25 ret = cls._instances[val]
26 26 return ret
27 27
28 28 def __init__(self,value):
29 29 self.value = str(int(value))
30 30 if self < 0:
31 31 raise ValueError("Version number cannot be negative")
32 32
33 33 def __add__(self, value):
34 34 ret = int(self) + int(value)
35 35 return VerNum(ret)
36 36
37 37 def __sub__(self, value):
38 38 return self + (int(value) * -1)
39 39
40 40 def __cmp__(self, value):
41 41 return int(self) - int(value)
42 42
43 43 def __repr__(self):
44 44 return "<VerNum(%s)>" % self.value
45 45
46 46 def __str__(self):
47 47 return str(self.value)
48 48
49 49 def __int__(self):
50 50 return int(self.value)
51 51
52 52
53 53 class Collection(pathed.Pathed):
54 54 """A collection of versioning scripts in a repository"""
55 55
56 56 FILENAME_WITH_VERSION = re.compile(r'^(\d{3,}).*')
57 57
58 58 def __init__(self, path):
59 59 """Collect current version scripts in repository
60 60 and store them in self.versions
61 61 """
62 62 super(Collection, self).__init__(path)
63 63
64 64 # Create temporary list of files, allowing skipped version numbers.
65 65 files = os.listdir(path)
66 66 if '1' in files:
67 67 # deprecation
68 68 raise Exception('It looks like you have a repository in the old '
69 69 'format (with directories for each version). '
70 70 'Please convert repository before proceeding.')
71 71
72 72 tempVersions = dict()
73 73 for filename in files:
74 74 match = self.FILENAME_WITH_VERSION.match(filename)
75 75 if match:
76 76 num = int(match.group(1))
77 77 tempVersions.setdefault(num, []).append(filename)
78 78 else:
79 79 pass # Must be a helper file or something, let's ignore it.
80 80
81 81 # Create the versions member where the keys
82 82 # are VerNum's and the values are Version's.
83 83 self.versions = dict()
84 84 for num, files in tempVersions.items():
85 85 self.versions[VerNum(num)] = Version(num, path, files)
86 86
87 87 @property
88 88 def latest(self):
89 89 """:returns: Latest version in Collection"""
90 90 return max([VerNum(0)] + self.versions.keys())
91 91
92 92 def _next_ver_num(self, use_timestamp_numbering):
93 print use_timestamp_numbering
94 93 if use_timestamp_numbering == True:
95 print "Creating new timestamp version!"
96 94 return VerNum(int(datetime.utcnow().strftime('%Y%m%d%H%M%S')))
97 95 else:
98 96 return self.latest + 1
99 97
100 98 def create_new_python_version(self, description, **k):
101 99 """Create Python files for new version"""
102 100 ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
103 101 extra = str_to_filename(description)
104 102
105 103 if extra:
106 104 if extra == '_':
107 105 extra = ''
108 106 elif not extra.startswith('_'):
109 107 extra = '_%s' % extra
110 108
111 109 filename = '%03d%s.py' % (ver, extra)
112 110 filepath = self._version_path(filename)
113 111
114 112 script.PythonScript.create(filepath, **k)
115 113 self.versions[ver] = Version(ver, self.path, [filename])
116 114
117 115 def create_new_sql_version(self, database, description, **k):
118 116 """Create SQL files for new version"""
119 117 ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
120 118 self.versions[ver] = Version(ver, self.path, [])
121 119
122 120 extra = str_to_filename(description)
123 121
124 122 if extra:
125 123 if extra == '_':
126 124 extra = ''
127 125 elif not extra.startswith('_'):
128 126 extra = '_%s' % extra
129 127
130 128 # Create new files.
131 129 for op in ('upgrade', 'downgrade'):
132 130 filename = '%03d%s_%s_%s.sql' % (ver, extra, database, op)
133 131 filepath = self._version_path(filename)
134 132 script.SqlScript.create(filepath, **k)
135 133 self.versions[ver].add_script(filepath)
136 134
137 135 def version(self, vernum=None):
138 136 """Returns latest Version if vernum is not given.
139 137 Otherwise, returns wanted version"""
140 138 if vernum is None:
141 139 vernum = self.latest
142 140 return self.versions[VerNum(vernum)]
143 141
144 142 @classmethod
145 143 def clear(cls):
146 144 super(Collection, cls).clear()
147 145
148 146 def _version_path(self, ver):
149 147 """Returns path of file in versions repository"""
150 148 return os.path.join(self.path, str(ver))
151 149
152 150
153 151 class Version(object):
154 152 """A single version in a collection
155 153 :param vernum: Version Number
156 154 :param path: Path to script files
157 155 :param filelist: List of scripts
158 156 :type vernum: int, VerNum
159 157 :type path: string
160 158 :type filelist: list
161 159 """
162 160
163 161 def __init__(self, vernum, path, filelist):
164 162 self.version = VerNum(vernum)
165 163
166 164 # Collect scripts in this folder
167 165 self.sql = dict()
168 166 self.python = None
169 167
170 168 for script in filelist:
171 169 self.add_script(os.path.join(path, script))
172 170
173 171 def script(self, database=None, operation=None):
174 172 """Returns SQL or Python Script"""
175 173 for db in (database, 'default'):
176 174 # Try to return a .sql script first
177 175 try:
178 176 return self.sql[db][operation]
179 177 except KeyError:
180 178 continue # No .sql script exists
181 179
182 180 # TODO: maybe add force Python parameter?
183 181 ret = self.python
184 182
185 183 assert ret is not None, \
186 184 "There is no script for %d version" % self.version
187 185 return ret
188 186
189 187 def add_script(self, path):
190 188 """Add script to Collection/Version"""
191 189 if path.endswith(Extensions.py):
192 190 self._add_script_py(path)
193 191 elif path.endswith(Extensions.sql):
194 192 self._add_script_sql(path)
195 193
196 194 SQL_FILENAME = re.compile(r'^.*\.sql')
197 195
198 196 def _add_script_sql(self, path):
199 197 basename = os.path.basename(path)
200 198 match = self.SQL_FILENAME.match(basename)
201 199
202 200 if match:
203 201 basename = basename.replace('.sql', '')
204 202 parts = basename.split('_')
205 203 if len(parts) < 3:
206 204 raise exceptions.ScriptError(
207 205 "Invalid SQL script name %s " % basename + \
208 206 "(needs to be ###_description_database_operation.sql)")
209 207 version = parts[0]
210 208 op = parts[-1]
211 209 dbms = parts[-2]
212 210 else:
213 211 raise exceptions.ScriptError(
214 212 "Invalid SQL script name %s " % basename + \
215 213 "(needs to be ###_description_database_operation.sql)")
216 214
217 215 # File the script into a dictionary
218 216 self.sql.setdefault(dbms, {})[op] = script.SqlScript(path)
219 217
220 218 def _add_script_py(self, path):
221 219 if self.python is not None:
222 220 raise exceptions.ScriptError('You can only have one Python script '
223 221 'per version, but you have: %s and %s' % (self.python, path))
224 222 self.python = script.PythonScript(path)
225 223
226 224
227 225 class Extensions:
228 226 """A namespace for file extensions"""
229 227 py = 'py'
230 228 sql = 'sql'
231 229
232 230 def str_to_filename(s):
233 231 """Replaces spaces, (double and single) quotes
234 232 and double underscores to underscores
235 233 """
236 234
237 235 s = s.replace(' ', '_').replace('"', '_').replace("'", '_').replace(".", "_")
238 236 while '__' in s:
239 237 s = s.replace('__', '_')
240 238 return s
@@ -1,119 +1,119 b''
1 1 import logging
2 2 import datetime
3 3
4 4 from sqlalchemy import *
5 5 from sqlalchemy.exc import DatabaseError
6 6 from sqlalchemy.orm import relation, backref, class_mapper
7 7 from sqlalchemy.orm.session import Session
8 8
9 9 from rhodecode.lib.dbmigrate.migrate import *
10 10 from rhodecode.lib.dbmigrate.migrate.changeset import *
11 11
12 12 from rhodecode.model.meta import Base
13 13
14 14 log = logging.getLogger(__name__)
15 15
16 16 def upgrade(migrate_engine):
17 17 """ Upgrade operations go here.
18 18 Don't create your own engine; bind migrate_engine to your metadata
19 19 """
20 20
21 21 #==========================================================================
22 22 # Add table `groups``
23 23 #==========================================================================
24 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import Group
24 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import RepoGroup as Group
25 25 Group().__table__.create()
26 26
27 27 #==========================================================================
28 28 # Add table `group_to_perm`
29 29 #==========================================================================
30 30 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserRepoGroupToPerm
31 31 UserRepoGroupToPerm().__table__.create()
32 32
33 33 #==========================================================================
34 34 # Add table `users_groups`
35 35 #==========================================================================
36 36 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroup
37 37 UsersGroup().__table__.create()
38 38
39 39 #==========================================================================
40 40 # Add table `users_groups_members`
41 41 #==========================================================================
42 42 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupMember
43 43 UsersGroupMember().__table__.create()
44 44
45 45 #==========================================================================
46 46 # Add table `users_group_repo_to_perm`
47 47 #==========================================================================
48 48 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupRepoToPerm
49 49 UsersGroupRepoToPerm().__table__.create()
50 50
51 51 #==========================================================================
52 52 # Add table `users_group_to_perm`
53 53 #==========================================================================
54 54 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupToPerm
55 55 UsersGroupToPerm().__table__.create()
56 56
57 57 #==========================================================================
58 58 # Upgrade of `users` table
59 59 #==========================================================================
60 60 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import User
61 61
62 62 #add column
63 63 ldap_dn = Column("ldap_dn", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
64 64 ldap_dn.create(User().__table__)
65 65
66 66 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
67 67 api_key.create(User().__table__)
68 68
69 69 #remove old column
70 70 is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
71 71 is_ldap.drop(User().__table__)
72 72
73 73
74 74 #==========================================================================
75 75 # Upgrade of `repositories` table
76 76 #==========================================================================
77 77 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import Repository
78 78
79 79 #ADD clone_uri column#
80 80
81 81 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False,
82 82 assert_unicode=None),
83 83 nullable=True, unique=False, default=None)
84 84
85 85 clone_uri.create(Repository().__table__)
86 86
87 87 #ADD downloads column#
88 88 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
89 89 enable_downloads.create(Repository().__table__)
90 90
91 91 #ADD column created_on
92 92 created_on = Column('created_on', DateTime(timezone=False), nullable=True,
93 93 unique=None, default=datetime.datetime.now)
94 94 created_on.create(Repository().__table__)
95 95
96 96 #ADD group_id column#
97 97 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'),
98 98 nullable=True, unique=False, default=None)
99 99
100 100 group_id.create(Repository().__table__)
101 101
102 102
103 103 #==========================================================================
104 104 # Upgrade of `user_followings` table
105 105 #==========================================================================
106 106
107 107 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserFollowing
108 108
109 109 follows_from = Column('follows_from', DateTime(timezone=False),
110 110 nullable=True, unique=None,
111 111 default=datetime.datetime.now)
112 112 follows_from.create(UserFollowing().__table__)
113 113
114 114 return
115 115
116 116
117 117 def downgrade(migrate_engine):
118 118 meta = MetaData()
119 119 meta.bind = migrate_engine
General Comments 0
You need to be logged in to leave comments. Login now