added 0.6 TODO, all api now uses engine.dispose() to handle pool correctly

This commit is contained in:
iElectric 2010-04-29 17:02:51 +02:00
parent 2e43ac8101
commit ec24bde52c
8 changed files with 96 additions and 35 deletions

14
TODO
View File

@ -6,3 +6,17 @@ make_update_script_for_model:
- even if two "models" are equal, it doesn't yield so - even if two "models" are equal, it doesn't yield so
- controlledschema.drop() drops whole migrate table, maybe there are some other repositories bound to it! - controlledschema.drop() drops whole migrate table, maybe there are some other repositories bound to it!
0.6.0
- make logging stderr and stdout aware
- update documentation
- write documentation how to test all databases
- update repository migration script
- single pool strategy for api (decorator to close engine connection)
- wrap migration into transaction
- interactive migration script resultion
- downgrade to scripttest 1.0, report bug
- port to unittest2
- readd transaction support

View File

@ -97,6 +97,7 @@ versioning API is available as the :ref:`migrate <command-line-usage>` command.
.. _`Google Code project`: http://code.google.com/p/sqlalchemy-migrate .. _`Google Code project`: http://code.google.com/p/sqlalchemy-migrate
.. _sqlalchemy: http://www.sqlalchemy.org .. _sqlalchemy: http://www.sqlalchemy.org
API Documentation API Documentation
------------------ ------------------
@ -104,6 +105,7 @@ API Documentation
api api
Changelog Changelog
--------- ---------

View File

@ -6,7 +6,7 @@
changed order of positional arguments so all accept `url` and `repository` changed order of positional arguments so all accept `url` and `repository`
as first arguments. as first arguments.
.. versionchanged:: 0.5.4 .. versionchanged:: 0.5.4
``--preview_sql`` displays source file when using SQL scripts. ``--preview_sql`` displays source file when using SQL scripts.
If Python script is used, it runs the action with mocked engine and If Python script is used, it runs the action with mocked engine and
returns captured SQL statements. returns captured SQL statements.
@ -32,7 +32,7 @@ import logging
from migrate.versioning import (exceptions, repository, schema, version, from migrate.versioning import (exceptions, repository, schema, version,
script as script_) # command name conflict script as script_) # command name conflict
from migrate.versioning.util import catch_known_errors, construct_engine from migrate.versioning.util import catch_known_errors, with_engine
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -134,6 +134,7 @@ def version(repository, **opts):
return repo.latest return repo.latest
@with_engine
def db_version(url, repository, **opts): def db_version(url, repository, **opts):
"""%prog db_version URL REPOSITORY_PATH """%prog db_version URL REPOSITORY_PATH
@ -143,7 +144,7 @@ def db_version(url, repository, **opts):
The url should be any valid SQLAlchemy connection string. The url should be any valid SQLAlchemy connection string.
""" """
engine = construct_engine(url, **opts) engine = opts.pop('engine')
schema = ControlledSchema(engine, repository) schema = ControlledSchema(engine, repository)
return schema.version return schema.version
@ -199,7 +200,8 @@ def downgrade(url, repository, version, **opts):
err = "Cannot downgrade a database of version %s to version %s. "\ err = "Cannot downgrade a database of version %s to version %s. "\
"Try 'upgrade' instead." "Try 'upgrade' instead."
return _migrate(url, repository, version, upgrade=False, err=err, **opts) return _migrate(url, repository, version, upgrade=False, err=err, **opts)
@with_engine
def test(url, repository, **opts): def test(url, repository, **opts):
"""%prog test URL REPOSITORY_PATH [VERSION] """%prog test URL REPOSITORY_PATH [VERSION]
@ -208,7 +210,7 @@ def test(url, repository, **opts):
bad state. You should therefore better run the test on a copy of bad state. You should therefore better run the test on a copy of
your database. your database.
""" """
engine = construct_engine(url, **opts) engine = opts.pop('engine')
repos = Repository(repository) repos = Repository(repository)
script = repos.version(None).script() script = repos.version(None).script()
@ -223,6 +225,7 @@ def test(url, repository, **opts):
log.info("Success") log.info("Success")
@with_engine
def version_control(url, repository, version=None, **opts): def version_control(url, repository, version=None, **opts):
"""%prog version_control URL REPOSITORY_PATH [VERSION] """%prog version_control URL REPOSITORY_PATH [VERSION]
@ -242,16 +245,17 @@ def version_control(url, repository, version=None, **opts):
identical to what it would be if the database were created from identical to what it would be if the database were created from
scratch. scratch.
""" """
engine = construct_engine(url, **opts) engine = opts.pop('engine')
ControlledSchema.create(engine, repository, version) ControlledSchema.create(engine, repository, version)
@with_engine
def drop_version_control(url, repository, **opts): def drop_version_control(url, repository, **opts):
"""%prog drop_version_control URL REPOSITORY_PATH """%prog drop_version_control URL REPOSITORY_PATH
Removes version control from a database. Removes version control from a database.
""" """
engine = construct_engine(url, **opts) engine = opts.pop('engine')
schema = ControlledSchema(engine, repository) schema = ControlledSchema(engine, repository)
schema.drop() schema.drop()
@ -275,6 +279,7 @@ def manage(file, **opts):
Repository.create_manage_file(file, **opts) Repository.create_manage_file(file, **opts)
@with_engine
def compare_model_to_db(url, repository, model, **opts): def compare_model_to_db(url, repository, model, **opts):
"""%prog compare_model_to_db URL REPOSITORY_PATH MODEL """%prog compare_model_to_db URL REPOSITORY_PATH MODEL
@ -283,10 +288,11 @@ def compare_model_to_db(url, repository, model, **opts):
NOTE: This is EXPERIMENTAL. NOTE: This is EXPERIMENTAL.
""" # TODO: get rid of EXPERIMENTAL label """ # TODO: get rid of EXPERIMENTAL label
engine = construct_engine(url, **opts) engine = opts.pop('engine')
return ControlledSchema.compare_model_to_db(engine, model, repository) return ControlledSchema.compare_model_to_db(engine, model, repository)
@with_engine
def create_model(url, repository, **opts): def create_model(url, repository, **opts):
"""%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True] """%prog create_model URL REPOSITORY_PATH [DECLERATIVE=True]
@ -294,12 +300,13 @@ def create_model(url, repository, **opts):
NOTE: This is EXPERIMENTAL. NOTE: This is EXPERIMENTAL.
""" # TODO: get rid of EXPERIMENTAL label """ # TODO: get rid of EXPERIMENTAL label
engine = construct_engine(url, **opts) engine = opts.pop('engine')
declarative = opts.get('declarative', False) declarative = opts.get('declarative', False)
return ControlledSchema.create_model(engine, repository, declarative) return ControlledSchema.create_model(engine, repository, declarative)
@catch_known_errors @catch_known_errors
@with_engine
def make_update_script_for_model(url, repository, oldmodel, model, **opts): def make_update_script_for_model(url, repository, oldmodel, model, **opts):
"""%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH """%prog make_update_script_for_model URL OLDMODEL MODEL REPOSITORY_PATH
@ -308,11 +315,12 @@ def make_update_script_for_model(url, repository, oldmodel, model, **opts):
NOTE: This is EXPERIMENTAL. NOTE: This is EXPERIMENTAL.
""" # TODO: get rid of EXPERIMENTAL label """ # TODO: get rid of EXPERIMENTAL label
engine = construct_engine(url, **opts) engine = opts.pop('engine')
return PythonScript.make_update_script_for_model( return PythonScript.make_update_script_for_model(
engine, oldmodel, model, repository, **opts) engine, oldmodel, model, repository, **opts)
@with_engine
def update_db_from_model(url, repository, model, **opts): def update_db_from_model(url, repository, model, **opts):
"""%prog update_db_from_model URL REPOSITORY_PATH MODEL """%prog update_db_from_model URL REPOSITORY_PATH MODEL
@ -322,13 +330,14 @@ def update_db_from_model(url, repository, model, **opts):
NOTE: This is EXPERIMENTAL. NOTE: This is EXPERIMENTAL.
""" # TODO: get rid of EXPERIMENTAL label """ # TODO: get rid of EXPERIMENTAL label
engine = construct_engine(url, **opts) engine = opts.pop('engine')
schema = ControlledSchema(engine, repository) schema = ControlledSchema(engine, repository)
schema.update_db_from_model(model) schema.update_db_from_model(model)
@with_engine
def _migrate(url, repository, version, upgrade, err, **opts): def _migrate(url, repository, version, upgrade, err, **opts):
engine = construct_engine(url, **opts) engine = opts.pop('engine')
url = str(engine.url)
schema = ControlledSchema(engine, repository) schema = ControlledSchema(engine, repository)
version = _migrate_version(schema, version, upgrade, err) version = _migrate_version(schema, version, upgrade, err)

View File

@ -11,7 +11,7 @@ from migrate.versioning import exceptions, genmodel, schemadiff
from migrate.versioning.config import operations from migrate.versioning.config import operations
from migrate.versioning.template import Template from migrate.versioning.template import Template
from migrate.versioning.script import base from migrate.versioning.script import base
from migrate.versioning.util import import_path, load_model, construct_engine from migrate.versioning.util import import_path, load_model, with_engine
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -102,18 +102,21 @@ class PythonScript(base.BaseScript):
def preview_sql(self, url, step, **args): def preview_sql(self, url, step, **args):
"""Mocks SQLAlchemy Engine to store all executed calls in a string """Mocks SQLAlchemy Engine to store all executed calls in a string
and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>` and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>`
:returns: SQL file :returns: SQL file
""" """
buf = StringIO() buf = StringIO()
args['engine_arg_strategy'] = 'mock' args['engine_arg_strategy'] = 'mock'
args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p) args['engine_arg_executor'] = lambda s, p = '': buf.write(str(s) + p)
engine = construct_engine(url, **args)
self.run(engine, step) @with_engine
def go(url, step, **kw):
engine = kw.pop('engine')
self.run(engine, step)
return buf.getvalue()
return go(url, step, **args)
return buf.getvalue()
def run(self, engine, step): def run(self, engine, step):
"""Core method of Script file. """Core method of Script file.
Exectues :func:`update` or :func:`downgrade` functions Exectues :func:`update` or :func:`downgrade` functions

View File

@ -1,18 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""".. currentmodule:: migrate.versioning.util"""
import warnings import warnings
import logging
from decorator import decorator from decorator import decorator
from pkg_resources import EntryPoint from pkg_resources import EntryPoint
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.pool import StaticPool
from migrate.versioning import exceptions from migrate.versioning import exceptions
from migrate.versioning.util.keyedinstance import KeyedInstance from migrate.versioning.util.keyedinstance import KeyedInstance
from migrate.versioning.util.importpath import import_path from migrate.versioning.util.importpath import import_path
log = logging.getLogger(__name__)
def load_model(dotted_name): def load_model(dotted_name):
"""Import module and use module-level variable". """Import module and use module-level variable".
@ -123,14 +128,39 @@ def construct_engine(engine, **opts):
'engine_arg_echo=True or engine_dict={"echo": True}', 'engine_arg_echo=True or engine_dict={"echo": True}',
DeprecationWarning) DeprecationWarning)
kwargs['echo'] = echo kwargs['echo'] = echo
# parse keyword arguments # parse keyword arguments
for key, value in opts.iteritems(): for key, value in opts.iteritems():
if key.startswith('engine_arg_'): if key.startswith('engine_arg_'):
kwargs[key[11:]] = guess_obj_type(value) kwargs[key[11:]] = guess_obj_type(value)
log.debug('Constructing engine')
# TODO: return create_engine(engine, poolclass=StaticPool, **kwargs)
# seems like 0.5.x branch does not work with engine.dispose and staticpool
return create_engine(engine, **kwargs) return create_engine(engine, **kwargs)
@decorator
def with_engine(f, *a, **kw):
"""Decorator for :mod:`migrate.versioning.api` functions
to safely close resources after function usage.
Passes engine parameters to :func:`construct_engine` and
resulting parameter is available as kw['engine'].
Engine is disposed after wrapped function is executed.
.. versionadded: 0.6.0
"""
url = a[0]
engine = construct_engine(url, **kw)
try:
return f(*a, engine=engine, **kw)
finally:
if isinstance(engine, Engine):
log.debug('Disposing SQLAlchemy engine %s', engine)
engine.dispose()
class Memoize: class Memoize:
"""Memoize(fn) - an instance which acts like fn but memoizes its arguments """Memoize(fn) - an instance which acts like fn but memoizes its arguments

View File

@ -11,3 +11,4 @@ sqlite:///__tmp__
postgres://scott:tiger@localhost/test_migrate postgres://scott:tiger@localhost/test_migrate
mysql://scott:tiger@localhost/test_migrate mysql://scott:tiger@localhost/test_migrate
oracle://scott:tiger@localhost oracle://scott:tiger@localhost
# TODO: add firebird

View File

@ -72,9 +72,11 @@ def usedb(supported=None, not_supported=None):
@decorator @decorator
def dec(f, self, *a, **kw): def dec(f, self, *a, **kw):
for url in my_urls: for url in my_urls:
self._setup(url) try:
f(self, *a, **kw) self._setup(url)
self._teardown() f(self, *a, **kw)
finally:
self._teardown()
return dec return dec
@ -96,20 +98,19 @@ class DB(Base):
def _teardown(self): def _teardown(self):
self._disconnect() self._disconnect()
def _connect(self, url): def _connect(self, url):
self.url = url self.url = url
self.engine = create_engine(url, echo=True, poolclass=StaticPool) # TODO: seems like 0.5.x branch does not work with engine.dispose and staticpool
#self.engine = create_engine(url, echo=True, poolclass=StaticPool)
self.engine = create_engine(url, echo=True)
self.meta = MetaData(bind=self.engine) self.meta = MetaData(bind=self.engine)
if self.level < self.CONNECT: if self.level < self.CONNECT:
return return
#self.conn = self.engine.connect() #self.session = create_session(bind=self.engine)
self.session = create_session(bind=self.engine)
if self.level < self.TXN: if self.level < self.TXN:
return return
self.txn = self.session.begin() #self.txn = self.session.begin()
#self.txn.add(self.engine)
def _disconnect(self): def _disconnect(self):
if hasattr(self, 'txn'): if hasattr(self, 'txn'):
@ -118,6 +119,7 @@ class DB(Base):
self.session.close() self.session.close()
#if hasattr(self,'conn'): #if hasattr(self,'conn'):
# self.conn.close() # self.conn.close()
self.engine.dispose()
def _supported(self, url): def _supported(self, url):
db = url.split(':',1)[0] db = url.split(':',1)[0]
@ -152,3 +154,5 @@ class DB(Base):
name = self.table.name name = self.table.name
self.meta.clear() self.meta.clear()
self.table = Table(name, self.meta, autoload=True) self.table = Table(name, self.meta, autoload=True)
# TODO: document engine.dispose and write tests

View File

@ -2,9 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import shutil
import sys import sys
import types
from scripttest import TestFileEnvironment from scripttest import TestFileEnvironment
@ -18,7 +16,7 @@ class Shell(Pathed):
super(Shell, self).setUp() super(Shell, self).setUp()
self.env = TestFileEnvironment( self.env = TestFileEnvironment(
base_path=os.path.join(self.temp_usable_dir, 'env'), base_path=os.path.join(self.temp_usable_dir, 'env'),
script_path=[os.path.dirname(sys.executable)], script_path=[os.path.dirname(sys.executable)] # PATH to migrate development script folder
) )
def run_version(self, repos_path): def run_version(self, repos_path):