From: Mike Bayer Date: Mon, 28 Nov 2011 00:44:59 +0000 (-0500) Subject: - docs X-Git-Tag: rel_0_1_0~36 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=be3ebd2d98955f986c4e101090998c387a5076b8;p=thirdparty%2Fsqlalchemy%2Falembic.git - docs - note about unicode - dont need importlater - use correct type_ kw arg - log cols/tables/etc as we autogenerate --- diff --git a/CHANGES b/CHANGES index 14095f6b..38632727 100644 --- a/CHANGES +++ b/CHANGES @@ -78,6 +78,13 @@ impacts the usefulness of the command, not overall stability. +- Support for non-ASCII table, column and constraint + names is mostly nonexistent. This is also a + straightforward feature add as SQLAlchemy itself + supports unicode identifiers; Alembic itself will + likely need fixes to logging, column identification + by key, etc. for full support here. + - Support for tables in remote schemas, i.e. "schemaname.tablename", is very poor. Missing "schema" behaviors should be diff --git a/alembic/__init__.py b/alembic/__init__.py index 9c52e029..f7507f86 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -1,6 +1,6 @@ from os import path -__version__ = '0.1alpha' +__version__ = '0.1.0' package_dir = path.abspath(path.dirname(__file__)) diff --git a/alembic/autogenerate.py b/alembic/autogenerate.py index 101a871e..019f50f6 100644 --- a/alembic/autogenerate.py +++ b/alembic/autogenerate.py @@ -7,6 +7,9 @@ from sqlalchemy.engine.reflection import Inspector from sqlalchemy import types as sqltypes, schema import re +import logging +log = logging.getLogger(__name__) + ################################################### # top level @@ -45,16 +48,16 @@ def _produce_net_changes(connection, metadata, diffs): difference(['alembic_version']) metadata_table_names = set(metadata.tables) - diffs.extend( - ("add_table", metadata.tables[tname]) - for tname in metadata_table_names.difference(conn_table_names) - ) + for tname in metadata_table_names.difference(conn_table_names): + diffs.append(("add_table", metadata.tables[tname])) + log.info("Detected added table %r", tname) removal_metadata = schema.MetaData() for tname in conn_table_names.difference(metadata_table_names): t = schema.Table(tname, removal_metadata) inspector.reflecttable(t, None) diffs.append(("remove_table", t)) + log.info("Detected removed table %r", tname) existing_tables = conn_table_names.intersection(metadata_table_names) @@ -87,19 +90,22 @@ def _compare_columns(tname, conn_table, metadata_table, diffs): conn_col_names = set(conn_table) metadata_col_names = set(metadata_cols_by_name) - diffs.extend( - ("add_column", tname, metadata_cols_by_name[cname]) - for cname in metadata_col_names.difference(conn_col_names) - ) - diffs.extend( - ("remove_column", tname, schema.Column( - cname, - conn_table[cname]['type'], - nullable=conn_table[cname]['nullable'], - server_default=conn_table[cname]['default'] - )) - for cname in conn_col_names.difference(metadata_col_names) - ) + for cname in metadata_col_names.difference(conn_col_names): + diffs.append( + ("add_column", tname, metadata_cols_by_name[cname]) + ) + log.info("Detected added column '%s.%s'", tname, cname) + + for cname in conn_col_names.difference(metadata_col_names): + diffs.append( + ("remove_column", tname, schema.Column( + cname, + conn_table[cname]['type'], + nullable=conn_table[cname]['nullable'], + server_default=conn_table[cname]['default'] + )) + ) + log.info("Detected removed column '%s.%s'", tname, cname) for colname in metadata_col_names.intersection(conn_col_names): metadata_col = metadata_table.c[colname] @@ -118,10 +124,15 @@ def _compare_columns(tname, conn_table, metadata_table, diffs): def _compare_nullable(tname, cname, conn_col_nullable, metadata_col_nullable, diffs): if conn_col_nullable is not metadata_col_nullable: - diffs.extend([ + diffs.append( ("modify_nullable", tname, cname, conn_col_nullable, metadata_col_nullable), - ]) + ) + log.info("Detected %s on column '%s.%s'", + "NULL" if metadata_col_nullable else "NOT NULL", + tname, + cname + ) def _compare_type(tname, cname, conn_type, metadata_type, diffs): if conn_type._compare_type_affinity(metadata_type): @@ -132,9 +143,12 @@ def _compare_type(tname, cname, conn_type, metadata_type, diffs): isdiff = True if isdiff: - diffs.extend([ + diffs.append( ("modify_type", tname, cname, conn_type, metadata_type), - ]) + ) + log.info("Detected type change from %r to %r on '%s.%s'", + conn_type, metadata_type, tname, cname + ) def _string_compare(t1, t2): return \ @@ -224,7 +238,7 @@ def _drop_column(tname, column): def _modify_type(tname, cname, type_, old_type): return "alter_column(%(tname)r, %(cname)r, "\ - "type=%(prefix)s%(type)r, old_type=%(prefix)s%(old_type)r)" % { + "type_=%(prefix)s%(type)r, old_type=%(prefix)s%(old_type)r)" % { 'prefix':_autogenerate_prefix(), 'tname':tname, 'cname':cname, diff --git a/alembic/op.py b/alembic/op.py index aba629a0..9fb72b6d 100644 --- a/alembic/op.py +++ b/alembic/op.py @@ -4,8 +4,6 @@ from alembic.context import get_impl, get_context from sqlalchemy.types import NULLTYPE from sqlalchemy import schema, sql -util.importlater.resolve_all() - __all__ = sorted([ 'alter_column', 'add_column', diff --git a/alembic/util.py b/alembic/util.py index 51615b3a..82572fd1 100644 --- a/alembic/util.py +++ b/alembic/util.py @@ -99,79 +99,3 @@ class memoized_property(object): obj.__dict__[self.__name__] = result = self.fget(obj) return result -class importlater(object): - """Deferred import object. - - e.g.:: - - somesubmod = importlater("mypackage.somemodule", "somesubmod") - - is equivalent to:: - - from mypackage.somemodule import somesubmod - - except evaluted upon attribute access to "somesubmod". - - importlater() currently requires that resolve_all() be - called, typically at the bottom of a package's __init__.py. - This is so that __import__ still called only at - module import time, and not potentially within - a non-main thread later on. - - """ - - _unresolved = set() - - def __init__(self, path, addtl=None): - self._il_path = path - self._il_addtl = addtl - importlater._unresolved.add(self) - - @classmethod - def resolve_all(cls): - for m in list(importlater._unresolved): - m._resolve() - - @property - def _full_path(self): - if self._il_addtl: - return self._il_path + "." + self._il_addtl - else: - return self._il_path - - @memoized_property - def module(self): - if self in importlater._unresolved: - raise ImportError( - "importlater.resolve_all() hasn't been called") - - m = self._initial_import - if self._il_addtl: - m = getattr(m, self._il_addtl) - else: - for token in self._il_path.split(".")[1:]: - m = getattr(m, token) - return m - - def _resolve(self): - importlater._unresolved.discard(self) - if self._il_addtl: - self._initial_import = __import__( - self._il_path, globals(), locals(), - [self._il_addtl]) - else: - self._initial_import = __import__(self._il_path) - - def __getattr__(self, key): - if key == 'module': - raise ImportError("Could not resolve module %s" - % self._full_path) - try: - attr = getattr(self.module, key) - except AttributeError: - raise AttributeError( - "Module %s has no attribute '%s'" % - (self._full_path, key) - ) - self.__dict__[key] = attr - return attr diff --git a/docs/build/front.rst b/docs/build/front.rst index 25248d5c..46955a3b 100644 --- a/docs/build/front.rst +++ b/docs/build/front.rst @@ -7,12 +7,24 @@ Information about the Alembic project. Project Homepage ================ -Alembic is hosted on `Bitbucket `_ - the lead project page is at https://bitbucket.org/zzzeek/alembic. Source -code is tracked here using `Mercurial `_. +Alembic is hosted on `Bitbucket `_ - the lead project +page is at https://bitbucket.org/zzzeek/alembic. Source code is tracked here +using `Mercurial `_. -Releases and project status are available on Pypi at http://pypi.python.org/pypi/alembic. +Releases and project status are available on Pypi at +http://pypi.python.org/pypi/alembic. -The most recent published version of this documentation should be at http://packages.python.org/alembic/. +The most recent published version of this documentation should be at +http://packages.python.org/alembic/. + +Project Status +============== + +Note that Alembic is still in alpha status. Users should take +care to report bugs and missing features (see :ref:`bugs`) on an as-needed +basis. It should be expected that the development version may be required +for proper implementation of recently repaired issues in between releases; +the latest tip is always available at https://bitbucket.org/zzzeek/alembic/get/tip.tar.gz. .. _installation: @@ -40,6 +52,8 @@ projects. User issues, discussion of potential bugs and features should be posted to the Alembic Google Group at `sqlalchemy-alembic `_. +.. _bugs: + Bugs ==== Bugs and feature enhancements to Alembic should be reported on the `Bitbucket diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index 470adb49..c621769a 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -204,6 +204,7 @@ A new file ``1975ea83b712.py`` is generated. Looking inside the file:: down_revision = None from alembic.op import * + import sqlalchemy as sa def upgrade(): pass @@ -305,6 +306,7 @@ Let's edit this file and add a new column to the ``account`` table:: down_revision = '1975ea83b712' from alembic.op import * + import sqlalchemy as sa from sqlalchemy import DateTime, Column def upgrade(): @@ -364,34 +366,56 @@ Back to nothing - and up again:: Auto Generating Migrations =========================== -.. note:: this functionality is not yet implemented. Specific details here - are subject to change. - Alembic can view the status of the database and compare against the table metadata in the application, generating the "obvious" migrations based on a comparison. This -is achieved using the ``--autogenerate`` option to the ``alembic`` command. +is achieved using the ``--autogenerate`` option to the ``alembic revision`` command, +which places so-called *candidate* migrations into our new migrations file. We +review and modify these by hand as needed, then proceed normally. To use autogenerate, we first need to modify our ``env.py`` so that it gets access to a table metadata object that contains the target. Suppose our application has a `declarative base `_ in ``myapp.mymodel``. This base contains a :class:`~sqlalchemy.schema.MetaData` object which contains :class:`~sqlalchemy.schema.Table` objects defining our database. We make sure this -is loaded in ``env.py`` and then passed to :func:`.context.configure_connection` via -``use_metadata``:: +is loaded in ``env.py`` and then passed to :func:`.context.configure` via the +``autogenerate_metadata`` argument. The ``env.py`` sample script already has a +variable declaration near the top for our convenience, where we replace ``None`` +with our :class:`~sqlalchemy.schema.MetaData`. Starting with:: - from myapp.mymodel import Base + # add your model's MetaData object here + # for 'autogenerate' support + # from myapp import mymodel + # autogenerate_metadata = mymodel.Base.metadata + autogenerate_metadata = None - connection = engine.connect() - context.configure_connection(connection, use_metadata=Base.metadata) - trans = connection.begin() - try: - context.run_migrations() - trans.commit() - except: - trans.rollback() - raise +we change to:: -We then create an upgrade file in the usual way adding ``--autogenerate``. Suppose + from myapp.mymodel import Base + autogenerate_metadata = Base.metadata + +If we look later in the script, down in ``run_migrations_online()``, +we can see the directive passed to :func:`.context.configure`:: + + def run_migrations_online(): + engine = engine_from_config( + config.get_section(config.config_ini_section), prefix='sqlalchemy.') + + connection = engine.connect() + context.configure( + connection=connection, + autogenerate_metadata=autogenerate_metadata + ) + + trans = connection.begin() + try: + context.run_migrations() + trans.commit() + except: + trans.rollback() + raise + +We can then use the ``alembic revision`` command in conjunction with the +``--autogenerate`` option. Suppose our :class:`~sqlalchemy.schema.MetaData` contained a definition for the ``account`` table, and the database did not. We'd get output like:: @@ -414,20 +438,24 @@ is already present:: down_revision = None from alembic.op import * - import sqlalchemy as sa def upgrade(): + ### commands auto generated by Alembic - please adjust! ### create_table( - 'account', - sa.Column('id', sa.INTEGER, primary_key=True), - sa.Column('name', sa.VARCHAR(50), nullable=False), - sa.Column('description', sa.VARCHAR(200)), - sa.Column('last_transaction_date', sa.DATETIME) + 'account', + sa.Column('id', sa.Integer()), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.VARCHAR(200)), + sa.Column('last_transaction_date', sa.DateTime()), + sa.PrimaryKeyConstraint('id') + ### end Alembic commands ### ) def downgrade(): + ### commands auto generated by Alembic - please adjust! ### drop_table("account") + ### end Alembic commands ### The migration hasn't actually run yet, of course. We do that via the usual ``upgrade`` command. We should also go into our migration file and alter it as needed, including diff --git a/tests/test_autogenerate.py b/tests/test_autogenerate.py index 119e9867..05335cb4 100644 --- a/tests/test_autogenerate.py +++ b/tests/test_autogenerate.py @@ -120,7 +120,7 @@ class AutogenerateDiffTest(TestCase): drop_table(u'extra') drop_column('user', u'pw') alter_column('user', 'name', nullable=False) - alter_column('order', u'amount', type=sa.Numeric(precision=10, scale=2), old_type=sa.NUMERIC(precision=8, scale=2)) + alter_column('order', u'amount', type_=sa.Numeric(precision=10, scale=2), old_type=sa.NUMERIC(precision=8, scale=2)) alter_column('order', u'amount', nullable=True) add_column('address', sa.Column('street', sa.String(length=50), nullable=True)) ### end Alembic commands ###""") @@ -133,7 +133,7 @@ class AutogenerateDiffTest(TestCase): ) add_column('user', sa.Column(u'pw', sa.VARCHAR(length=50), nullable=True)) alter_column('user', 'name', nullable=True) - alter_column('order', u'amount', type=sa.NUMERIC(precision=8, scale=2), old_type=sa.Numeric(precision=10, scale=2)) + alter_column('order', u'amount', type_=sa.NUMERIC(precision=8, scale=2), old_type=sa.Numeric(precision=10, scale=2)) alter_column('order', u'amount', nullable=False) drop_column('address', 'street') ### end Alembic commands ###""") @@ -193,7 +193,7 @@ class AutogenRenderTest(TestCase): autogenerate._modify_type( "sometable", "somecolumn", CHAR(10), CHAR(20)), "alter_column('sometable', 'somecolumn', " - "type=sa.CHAR(length=10), old_type=sa.CHAR(length=20))" + "type_=sa.CHAR(length=10), old_type=sa.CHAR(length=20))" ) def test_render_modify_nullable(self):