Mercurial > kallithea
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/lib/dbmigrate/migrate/versioning/api.py Sat Dec 11 01:54:12 2010 +0100 @@ -0,0 +1,383 @@ +""" + This module provides an external API to the versioning system. + + .. versionchanged:: 0.6.0 + :func:`migrate.versioning.api.test` and schema diff functions + changed order of positional arguments so all accept `url` and `repository` + as first arguments. + + .. versionchanged:: 0.5.4 + ``--preview_sql`` displays source file when using SQL scripts. + If Python script is used, it runs the action with mocked engine and + returns captured SQL statements. + + .. versionchanged:: 0.5.4 + Deprecated ``--echo`` parameter in favour of new + :func:`migrate.versioning.util.construct_engine` behavior. +""" + +# Dear migrate developers, +# +# please do not comment this module using sphinx syntax because its +# docstrings are presented as user help and most users cannot +# interpret sphinx annotated ReStructuredText. +# +# Thanks, +# Jan Dittberner + +import sys +import inspect +import logging + +from migrate import exceptions +from migrate.versioning import (repository, schema, version, + script as script_) # command name conflict +from migrate.versioning.util import catch_known_errors, with_engine + + +log = logging.getLogger(__name__) +command_desc = { + 'help': 'displays help on a given command', + 'create': 'create an empty repository at the specified path', + 'script': 'create an empty change Python script', + 'script_sql': 'create empty change SQL scripts for given database', + 'version': 'display the latest version available in a repository', + 'db_version': 'show the current version of the repository under version control', + 'source': 'display the Python code for a particular version in this repository', + 'version_control': 'mark a database as under this repository\'s version control', + 'upgrade': 'upgrade a database to a later version', + 'downgrade': 'downgrade a database to an earlier version', + 'drop_version_control': 'removes version control from a database', + 'manage': 'creates a Python script that runs Migrate with a set of default values', + 'test': 'performs the upgrade and downgrade command on the given database', + 'compare_model_to_db': 'compare MetaData against the current database state', + 'create_model': 'dump the current database as a Python model to stdout', + 'make_update_script_for_model': 'create a script changing the old MetaData to the new (current) MetaData', + 'update_db_from_model': 'modify the database to match the structure of the current MetaData', +} +__all__ = command_desc.keys() + +Repository = repository.Repository +ControlledSchema = schema.ControlledSchema +VerNum = version.VerNum +PythonScript = script_.PythonScript +SqlScript = script_.SqlScript + + +# deprecated +def help(cmd=None, **opts): + """%prog help COMMAND + + Displays help on a given command. + """ + if cmd is None: + raise exceptions.UsageError(None) + try: + func = globals()[cmd] + except: + raise exceptions.UsageError( + "'%s' isn't a valid command. Try 'help COMMAND'" % cmd) + ret = func.__doc__ + if sys.argv[0]: + ret = ret.replace('%prog', sys.argv[0]) + return ret + +@catch_known_errors +def create(repository, name, **opts): + """%prog create REPOSITORY_PATH NAME [--table=TABLE] + + Create an empty repository at the specified path. + + You can specify the version_table to be used; by default, it is + 'migrate_version'. This table is created in all version-controlled + databases. + """ + repo_path = Repository.create(repository, name, **opts) + + +@catch_known_errors +def script(description, repository, **opts): + """%prog script DESCRIPTION REPOSITORY_PATH + + Create an empty change script using the next unused version number + appended with the given description. + + For instance, manage.py script "Add initial tables" creates: + repository/versions/001_Add_initial_tables.py + """ + repo = Repository(repository) + repo.create_script(description, **opts) + + +@catch_known_errors +def script_sql(database, repository, **opts): + """%prog script_sql DATABASE REPOSITORY_PATH + + Create empty change SQL scripts for given DATABASE, where DATABASE + is either specific ('postgres', 'mysql', 'oracle', 'sqlite', etc.) + or generic ('default'). + + For instance, manage.py script_sql postgres creates: + repository/versions/001_postgres_upgrade.sql and + repository/versions/001_postgres_postgres.sql + """ + repo = Repository(repository) + repo.create_script_sql(database, **opts) + + +def version(repository, **opts): + """%prog version REPOSITORY_PATH + + Display the latest version available in a repository. + """ + repo = Repository(repository) + return repo.latest + + +@with_engine +def db_version(url, repository, **opts): + """%prog db_version URL REPOSITORY_PATH + + Show the current version of the repository with the given + connection string, under version control of the specified + repository. + + The url should be any valid SQLAlchemy connection string. + """ + engine = opts.pop('engine') + schema = ControlledSchema(engine, repository) + return schema.version + + +def source(version, dest=None, repository=None, **opts): + """%prog source VERSION [DESTINATION] --repository=REPOSITORY_PATH + + Display the Python code for a particular version in this + repository. Save it to the file at DESTINATION or, if omitted, + send to stdout. + """ + if repository is None: + raise exceptions.UsageError("A repository must be specified") + repo = Repository(repository) + ret = repo.version(version).script().source() + if dest is not None: + dest = open(dest, 'w') + dest.write(ret) + dest.close() + ret = None + return ret + + +def upgrade(url, repository, version=None, **opts): + """%prog upgrade URL REPOSITORY_PATH [VERSION] [--preview_py|--preview_sql] + + Upgrade a database to a later version. + + This runs the upgrade() function defined in your change scripts. + + By default, the database is updated to the latest available + version. You may specify a version instead, if you wish. + + You may preview the Python or SQL code to be executed, rather than + actually executing it, using the appropriate 'preview' option. + """ + err = "Cannot upgrade a database of version %s to version %s. "\ + "Try 'downgrade' instead." + return _migrate(url, repository, version, upgrade=True, err=err, **opts) + + +def downgrade(url, repository, version, **opts): + """%prog downgrade URL REPOSITORY_PATH VERSION [--preview_py|--preview_sql] + + Downgrade a database to an earlier version. + + This is the reverse of upgrade; this runs the downgrade() function + defined in your change scripts. + + You may preview the Python or SQL code to be executed, rather than + actually executing it, using the appropriate 'preview' option. + """ + err = "Cannot downgrade a database of version %s to version %s. "\ + "Try 'upgrade' instead." + return _migrate(url, repository, version, upgrade=False, err=err, **opts) + +@with_engine +def test(url, repository, **opts): + """%prog test URL REPOSITORY_PATH [VERSION] + + Performs the upgrade and downgrade option on the given + database. This is not a real test and may leave the database in a + bad state. You should therefore better run the test on a copy of + your database. + """ + engine = opts.pop('engine') + repos = Repository(repository) + script = repos.version(None).script() + + # Upgrade + log.info("Upgrading...") + script.run(engine, 1) + log.info("done") + + log.info("Downgrading...") + script.run(engine, -1) + log.info("done") + log.info("Success") + + +@with_engine +def version_control(url, repository, version=None, **opts): + """%prog version_control URL REPOSITORY_PATH [VERSION] + + Mark a database as under this repository's version control. + + Once a database is under version control, schema changes should + only be done via change scripts in this repository. + + This creates the table version_table in the database. + + The url should be any valid SQLAlchemy connection string. + + By default, the database begins at version 0 and is assumed to be + empty. If the database is not empty, you may specify a version at + which to begin instead. No attempt is made to verify this + version's correctness - the database schema is expected to be + identical to what it would be if the database were created from + scratch. + """ + engine = opts.pop('engine') + ControlledSchema.create(engine, repository, version) + + +@with_engine +def drop_version_control(url, repository, **opts): + """%prog drop_version_control URL REPOSITORY_PATH + + Removes version control from a database. + """ + engine = opts.pop('engine') + schema = ControlledSchema(engine, repository) + schema.drop() + + +def manage(file, **opts): + """%prog manage FILENAME [VARIABLES...] + + Creates a script that runs Migrate with a set of default values. + + For example:: + + %prog manage manage.py --repository=/path/to/repository \ +--url=sqlite:///project.db + + would create the script manage.py. The following two commands + would then have exactly the same results:: + + python manage.py version + %prog version --repository=/path/to/repository + """ + Repository.create_manage_file(file, **opts) + + +@with_engine +def compare_model_to_db(url, repository, model, **opts): + """%prog compare_model_to_db URL REPOSITORY_PATH MODEL + + Compare the current model (assumed to be a module level variable + of type sqlalchemy.MetaData) against the current database. + + NOTE: This is EXPERIMENTAL. + """ # TODO: get rid of EXPERIMENTAL label + engine = opts.pop('engine') + return ControlledSchema.compare_model_to_db(engine, model, repository) + + +@with_engine +def create_model(url, repository, **opts): + """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True] + + Dump the current database as a Python model to stdout. + + NOTE: This is EXPERIMENTAL. + """ # TODO: get rid of EXPERIMENTAL label + engine = opts.pop('engine') + declarative = opts.get('declarative', False) + return ControlledSchema.create_model(engine, repository, declarative) + + +@catch_known_errors +@with_engine +def make_update_script_for_model(url, repository, oldmodel, model, **opts): + """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH + + Create a script changing the old Python model to the new (current) + Python model, sending to stdout. + + NOTE: This is EXPERIMENTAL. + """ # TODO: get rid of EXPERIMENTAL label + engine = opts.pop('engine') + return PythonScript.make_update_script_for_model( + engine, oldmodel, model, repository, **opts) + + +@with_engine +def update_db_from_model(url, repository, model, **opts): + """%prog update_db_from_model URL REPOSITORY_PATH MODEL + + Modify the database to match the structure of the current Python + model. This also sets the db_version number to the latest in the + repository. + + NOTE: This is EXPERIMENTAL. + """ # TODO: get rid of EXPERIMENTAL label + engine = opts.pop('engine') + schema = ControlledSchema(engine, repository) + schema.update_db_from_model(model) + +@with_engine +def _migrate(url, repository, version, upgrade, err, **opts): + engine = opts.pop('engine') + url = str(engine.url) + schema = ControlledSchema(engine, repository) + version = _migrate_version(schema, version, upgrade, err) + + changeset = schema.changeset(version) + for ver, change in changeset: + nextver = ver + changeset.step + log.info('%s -> %s... ', ver, nextver) + + if opts.get('preview_sql'): + if isinstance(change, PythonScript): + log.info(change.preview_sql(url, changeset.step, **opts)) + elif isinstance(change, SqlScript): + log.info(change.source()) + + elif opts.get('preview_py'): + if not isinstance(change, PythonScript): + raise exceptions.UsageError("Python source can be only displayed" + " for python migration files") + source_ver = max(ver, nextver) + module = schema.repository.version(source_ver).script().module + funcname = upgrade and "upgrade" or "downgrade" + func = getattr(module, funcname) + log.info(inspect.getsource(func)) + else: + schema.runchange(ver, change, changeset.step) + log.info('done') + + +def _migrate_version(schema, version, upgrade, err): + if version is None: + return version + # Version is specified: ensure we're upgrading in the right direction + # (current version < target version for upgrading; reverse for down) + version = VerNum(version) + cur = schema.version + if upgrade is not None: + if upgrade: + direction = cur <= version + else: + direction = cur >= version + if not direction: + raise exceptions.KnownError(err % (cur, version)) + return version