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