comparison rhodecode/lib/dbmigrate/migrate/versioning/api.py @ 833:9753e0907827 beta

added dbmigrate package, added model changes moved out upgrade db command to that package
author Marcin Kuzminski <marcin@python-works.com>
date Sat, 11 Dec 2010 01:54:12 +0100
parents
children 08d2dcd71666
comparison
equal deleted inserted replaced
832:634596f81cfd 833:9753e0907827
1 """
2 This module provides an external API to the versioning system.
3
4 .. versionchanged:: 0.6.0
5 :func:`migrate.versioning.api.test` and schema diff functions
6 changed order of positional arguments so all accept `url` and `repository`
7 as first arguments.
8
9 .. versionchanged:: 0.5.4
10 ``--preview_sql`` displays source file when using SQL scripts.
11 If Python script is used, it runs the action with mocked engine and
12 returns captured SQL statements.
13
14 .. versionchanged:: 0.5.4
15 Deprecated ``--echo`` parameter in favour of new
16 :func:`migrate.versioning.util.construct_engine` behavior.
17 """
18
19 # Dear migrate developers,
20 #
21 # please do not comment this module using sphinx syntax because its
22 # docstrings are presented as user help and most users cannot
23 # interpret sphinx annotated ReStructuredText.
24 #
25 # Thanks,
26 # Jan Dittberner
27
28 import sys
29 import inspect
30 import logging
31
32 from migrate import exceptions
33 from migrate.versioning import (repository, schema, version,
34 script as script_) # command name conflict
35 from migrate.versioning.util import catch_known_errors, with_engine
36
37
38 log = logging.getLogger(__name__)
39 command_desc = {
40 'help': 'displays help on a given command',
41 'create': 'create an empty repository at the specified path',
42 'script': 'create an empty change Python script',
43 'script_sql': 'create empty change SQL scripts for given database',
44 'version': 'display the latest version available in a repository',
45 'db_version': 'show the current version of the repository under version control',
46 'source': 'display the Python code for a particular version in this repository',
47 'version_control': 'mark a database as under this repository\'s version control',
48 'upgrade': 'upgrade a database to a later version',
49 'downgrade': 'downgrade a database to an earlier version',
50 'drop_version_control': 'removes version control from a database',
51 'manage': 'creates a Python script that runs Migrate with a set of default values',
52 'test': 'performs the upgrade and downgrade command on the given database',
53 'compare_model_to_db': 'compare MetaData against the current database state',
54 'create_model': 'dump the current database as a Python model to stdout',
55 'make_update_script_for_model': 'create a script changing the old MetaData to the new (current) MetaData',
56 'update_db_from_model': 'modify the database to match the structure of the current MetaData',
57 }
58 __all__ = command_desc.keys()
59
60 Repository = repository.Repository
61 ControlledSchema = schema.ControlledSchema
62 VerNum = version.VerNum
63 PythonScript = script_.PythonScript
64 SqlScript = script_.SqlScript
65
66
67 # deprecated
68 def help(cmd=None, **opts):
69 """%prog help COMMAND
70
71 Displays help on a given command.
72 """
73 if cmd is None:
74 raise exceptions.UsageError(None)
75 try:
76 func = globals()[cmd]
77 except:
78 raise exceptions.UsageError(
79 "'%s' isn't a valid command. Try 'help COMMAND'" % cmd)
80 ret = func.__doc__
81 if sys.argv[0]:
82 ret = ret.replace('%prog', sys.argv[0])
83 return ret
84
85 @catch_known_errors
86 def create(repository, name, **opts):
87 """%prog create REPOSITORY_PATH NAME [--table=TABLE]
88
89 Create an empty repository at the specified path.
90
91 You can specify the version_table to be used; by default, it is
92 'migrate_version'. This table is created in all version-controlled
93 databases.
94 """
95 repo_path = Repository.create(repository, name, **opts)
96
97
98 @catch_known_errors
99 def script(description, repository, **opts):
100 """%prog script DESCRIPTION REPOSITORY_PATH
101
102 Create an empty change script using the next unused version number
103 appended with the given description.
104
105 For instance, manage.py script "Add initial tables" creates:
106 repository/versions/001_Add_initial_tables.py
107 """
108 repo = Repository(repository)
109 repo.create_script(description, **opts)
110
111
112 @catch_known_errors
113 def script_sql(database, repository, **opts):
114 """%prog script_sql DATABASE REPOSITORY_PATH
115
116 Create empty change SQL scripts for given DATABASE, where DATABASE
117 is either specific ('postgres', 'mysql', 'oracle', 'sqlite', etc.)
118 or generic ('default').
119
120 For instance, manage.py script_sql postgres creates:
121 repository/versions/001_postgres_upgrade.sql and
122 repository/versions/001_postgres_postgres.sql
123 """
124 repo = Repository(repository)
125 repo.create_script_sql(database, **opts)
126
127
128 def version(repository, **opts):
129 """%prog version REPOSITORY_PATH
130
131 Display the latest version available in a repository.
132 """
133 repo = Repository(repository)
134 return repo.latest
135
136
137 @with_engine
138 def db_version(url, repository, **opts):
139 """%prog db_version URL REPOSITORY_PATH
140
141 Show the current version of the repository with the given
142 connection string, under version control of the specified
143 repository.
144
145 The url should be any valid SQLAlchemy connection string.
146 """
147 engine = opts.pop('engine')
148 schema = ControlledSchema(engine, repository)
149 return schema.version
150
151
152 def source(version, dest=None, repository=None, **opts):
153 """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH
154
155 Display the Python code for a particular version in this
156 repository. Save it to the file at DESTINATION or, if omitted,
157 send to stdout.
158 """
159 if repository is None:
160 raise exceptions.UsageError("A repository must be specified")
161 repo = Repository(repository)
162 ret = repo.version(version).script().source()
163 if dest is not None:
164 dest = open(dest, 'w')
165 dest.write(ret)
166 dest.close()
167 ret = None
168 return ret
169
170
171 def upgrade(url, repository, version=None, **opts):
172 """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql]
173
174 Upgrade a database to a later version.
175
176 This runs the upgrade() function defined in your change scripts.
177
178 By default, the database is updated to the latest available
179 version. You may specify a version instead, if you wish.
180
181 You may preview the Python or SQL code to be executed, rather than
182 actually executing it, using the appropriate 'preview' option.
183 """
184 err = "Cannot upgrade a database of version %s to version %s. "\
185 "Try 'downgrade' instead."
186 return _migrate(url, repository, version, upgrade=True, err=err, **opts)
187
188
189 def downgrade(url, repository, version, **opts):
190 """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql]
191
192 Downgrade a database to an earlier version.
193
194 This is the reverse of upgrade; this runs the downgrade() function
195 defined in your change scripts.
196
197 You may preview the Python or SQL code to be executed, rather than
198 actually executing it, using the appropriate 'preview' option.
199 """
200 err = "Cannot downgrade a database of version %s to version %s. "\
201 "Try 'upgrade' instead."
202 return _migrate(url, repository, version, upgrade=False, err=err, **opts)
203
204 @with_engine
205 def test(url, repository, **opts):
206 """%prog test URL REPOSITORY_PATH [VERSION]
207
208 Performs the upgrade and downgrade option on the given
209 database. This is not a real test and may leave the database in a
210 bad state. You should therefore better run the test on a copy of
211 your database.
212 """
213 engine = opts.pop('engine')
214 repos = Repository(repository)
215 script = repos.version(None).script()
216
217 # Upgrade
218 log.info("Upgrading...")
219 script.run(engine, 1)
220 log.info("done")
221
222 log.info("Downgrading...")
223 script.run(engine, -1)
224 log.info("done")
225 log.info("Success")
226
227
228 @with_engine
229 def version_control(url, repository, version=None, **opts):
230 """%prog version_control URL REPOSITORY_PATH [VERSION]
231
232 Mark a database as under this repository's version control.
233
234 Once a database is under version control, schema changes should
235 only be done via change scripts in this repository.
236
237 This creates the table version_table in the database.
238
239 The url should be any valid SQLAlchemy connection string.
240
241 By default, the database begins at version 0 and is assumed to be
242 empty. If the database is not empty, you may specify a version at
243 which to begin instead. No attempt is made to verify this
244 version's correctness - the database schema is expected to be
245 identical to what it would be if the database were created from
246 scratch.
247 """
248 engine = opts.pop('engine')
249 ControlledSchema.create(engine, repository, version)
250
251
252 @with_engine
253 def drop_version_control(url, repository, **opts):
254 """%prog drop_version_control URL REPOSITORY_PATH
255
256 Removes version control from a database.
257 """
258 engine = opts.pop('engine')
259 schema = ControlledSchema(engine, repository)
260 schema.drop()
261
262
263 def manage(file, **opts):
264 """%prog manage FILENAME [VARIABLES...]
265
266 Creates a script that runs Migrate with a set of default values.
267
268 For example::
269
270 %prog manage manage.py --repository=/path/to/repository \
271 --url=sqlite:///project.db
272
273 would create the script manage.py. The following two commands
274 would then have exactly the same results::
275
276 python manage.py version
277 %prog version --repository=/path/to/repository
278 """
279 Repository.create_manage_file(file, **opts)
280
281
282 @with_engine
283 def compare_model_to_db(url, repository, model, **opts):
284 """%prog compare_model_to_db URL REPOSITORY_PATH MODEL
285
286 Compare the current model (assumed to be a module level variable
287 of type sqlalchemy.MetaData) against the current database.
288
289 NOTE: This is EXPERIMENTAL.
290 """ # TODO: get rid of EXPERIMENTAL label
291 engine = opts.pop('engine')
292 return ControlledSchema.compare_model_to_db(engine, model, repository)
293
294
295 @with_engine
296 def create_model(url, repository, **opts):
297 """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True]
298
299 Dump the current database as a Python model to stdout.
300
301 NOTE: This is EXPERIMENTAL.
302 """ # TODO: get rid of EXPERIMENTAL label
303 engine = opts.pop('engine')
304 declarative = opts.get('declarative', False)
305 return ControlledSchema.create_model(engine, repository, declarative)
306
307
308 @catch_known_errors
309 @with_engine
310 def make_update_script_for_model(url, repository, oldmodel, model, **opts):
311 """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
312
313 Create a script changing the old Python model to the new (current)
314 Python model, sending to stdout.
315
316 NOTE: This is EXPERIMENTAL.
317 """ # TODO: get rid of EXPERIMENTAL label
318 engine = opts.pop('engine')
319 return PythonScript.make_update_script_for_model(
320 engine, oldmodel, model, repository, **opts)
321
322
323 @with_engine
324 def update_db_from_model(url, repository, model, **opts):
325 """%prog update_db_from_model URL REPOSITORY_PATH MODEL
326
327 Modify the database to match the structure of the current Python
328 model. This also sets the db_version number to the latest in the
329 repository.
330
331 NOTE: This is EXPERIMENTAL.
332 """ # TODO: get rid of EXPERIMENTAL label
333 engine = opts.pop('engine')
334 schema = ControlledSchema(engine, repository)
335 schema.update_db_from_model(model)
336
337 @with_engine
338 def _migrate(url, repository, version, upgrade, err, **opts):
339 engine = opts.pop('engine')
340 url = str(engine.url)
341 schema = ControlledSchema(engine, repository)
342 version = _migrate_version(schema, version, upgrade, err)
343
344 changeset = schema.changeset(version)
345 for ver, change in changeset:
346 nextver = ver + changeset.step
347 log.info('%s -> %s... ', ver, nextver)
348
349 if opts.get('preview_sql'):
350 if isinstance(change, PythonScript):
351 log.info(change.preview_sql(url, changeset.step, **opts))
352 elif isinstance(change, SqlScript):
353 log.info(change.source())
354
355 elif opts.get('preview_py'):
356 if not isinstance(change, PythonScript):
357 raise exceptions.UsageError("Python source can be only displayed"
358 " for python migration files")
359 source_ver = max(ver, nextver)
360 module = schema.repository.version(source_ver).script().module
361 funcname = upgrade and "upgrade" or "downgrade"
362 func = getattr(module, funcname)
363 log.info(inspect.getsource(func))
364 else:
365 schema.runchange(ver, change, changeset.step)
366 log.info('done')
367
368
369 def _migrate_version(schema, version, upgrade, err):
370 if version is None:
371 return version
372 # Version is specified: ensure we're upgrading in the right direction
373 # (current version < target version for upgrading; reverse for down)
374 version = VerNum(version)
375 cur = schema.version
376 if upgrade is not None:
377 if upgrade:
378 direction = cur <= version
379 else:
380 direction = cur >= version
381 if not direction:
382 raise exceptions.KnownError(err % (cur, version))
383 return version