api.py
383 lines
| 12.8 KiB
| text/x-python
|
PythonLexer
r833 | """ | |||
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 | ||||
r835 | from rhodecode.lib.dbmigrate.migrate import exceptions | |||
from rhodecode.lib.dbmigrate.migrate.versioning import (repository, schema, version, | ||||
r833 | script as script_) # command name conflict | |||
r835 | from rhodecode.lib.dbmigrate.migrate.versioning.util import catch_known_errors, with_engine | |||
r833 | ||||
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 | ||||