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
from os import path
-__version__ = '0.1alpha'
+__version__ = '0.1.0'
package_dir = path.abspath(path.dirname(__file__))
from sqlalchemy import types as sqltypes, schema
import re
+import logging
+log = logging.getLogger(__name__)
+
###################################################
# top level
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)
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]
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):
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 \
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,
from sqlalchemy.types import NULLTYPE
from sqlalchemy import schema, sql
-util.importlater.resolve_all()
-
__all__ = sorted([
'alter_column',
'add_column',
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
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:
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
down_revision = None
from alembic.op import *
+ import sqlalchemy as sa
def upgrade():
pass
down_revision = '1975ea83b712'
from alembic.op import *
+ import sqlalchemy as sa
from sqlalchemy import DateTime, Column
def upgrade():
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::
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
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 ###""")
)
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 ###""")
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):