]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
- docs
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 28 Nov 2011 00:44:59 +0000 (19:44 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 28 Nov 2011 00:44:59 +0000 (19:44 -0500)
- note about unicode
- dont need importlater
- use correct type_ kw arg
- log cols/tables/etc as we autogenerate

CHANGES
alembic/__init__.py
alembic/autogenerate.py
alembic/op.py
alembic/util.py
docs/build/front.rst
docs/build/tutorial.rst
tests/test_autogenerate.py

diff --git a/CHANGES b/CHANGES
index 14095f6be7229f2f6703333463573d5f974429af..3863272710306b89963cd38cfb42558e48dd5224 100644 (file)
--- a/CHANGES
+++ b/CHANGES
   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 
index 9c52e029ed98f241714b520e6bc53eec5bcd4bf2..f7507f8688c9a4dabc0ee7a1adf3083e93f698b8 100644 (file)
@@ -1,6 +1,6 @@
 from os import path
 
-__version__ = '0.1alpha'
+__version__ = '0.1.0'
 
 package_dir = path.abspath(path.dirname(__file__))
 
index 101a871e125df23d0c3de332c3d14f61da4d90dd..019f50f6cd6faf1861c2aa2087e52d153697f1ee 100644 (file)
@@ -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, 
index aba629a0c699799d848a28e4c83278b968dca823..9fb72b6d822bd8d1b5f6a41ec3a643590d668118 100644 (file)
@@ -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',
index 51615b3a39e9f4fc2cb7b298ab72952dcb737f61..82572fd1ac65dc614bb8b36221c4539919bd9f8d 100644 (file)
@@ -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
index 25248d5c0b0bafc6e083eeafda8a33d315b4c695..46955a3bab06084ea287a2008c0a41b3ab2ddb37 100644 (file)
@@ -7,12 +7,24 @@ Information about the Alembic project.
 Project Homepage
 ================
 
-Alembic is hosted on `Bitbucket <http://bitbucket.org>`_ - the lead project page is at https://bitbucket.org/zzzeek/alembic.  Source
-code is tracked here using `Mercurial <http://mercurial.selenic.com/>`_.
+Alembic is hosted on `Bitbucket <http://bitbucket.org>`_ - the lead project
+page is at https://bitbucket.org/zzzeek/alembic. Source code is tracked here
+using `Mercurial <http://mercurial.selenic.com/>`_.
 
-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 <https://groups.google.com/group/sqlalchemy-alembic>`_.
 
+.. _bugs:
+
 Bugs
 ====
 Bugs and feature enhancements to Alembic should be reported on the `Bitbucket
index 470adb49f3337963914868d6f6f206134632773d..c621769ab42e9879ff74db3f397b0cdd9605df54 100644 (file)
@@ -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 <http://www.sqlalchemy.org/docs/orm/extensions/declarative.html#synopsis>`_
 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 
index 119e9867ec9e952647497dfba258500388f2a5ec..05335cb40f9e1321c06141dc3a9d8107169b12b4 100644 (file)
@@ -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):