[3] | 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 |
---|