From: Mike Bayer Date: Mon, 28 Nov 2011 00:05:39 +0000 (-0500) Subject: - support for schema types in modify type X-Git-Tag: rel_0_1_0~37 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f8fc2cab6995cf6639e0cc1998cfe691716746ab;p=thirdparty%2Fsqlalchemy%2Falembic.git - support for schema types in modify type - add known status to CHANGES - google group - sa. prefix on modify type in autogenerate - rename_table --- diff --git a/CHANGES b/CHANGES index a8b55684..14095f6b 100644 --- a/CHANGES +++ b/CHANGES @@ -1 +1,86 @@ -0.1 initial release. +0.1.0 +===== +- Initial release. Status of features: + +- Alembic is used in at least one production + environment, but should still be considered + alpha-level as of this release, + particularly in that many features are expected + to be missing / unimplemented. + The author asks that you *please* report all + issues, missing features, workarounds etc. + to the bugtracker, at + https://bitbucket.org/zzzeek/alembic/issues/new . + +- Postgresql and MS SQL Server environments + have been tested for several weeks in a production + environment. In particular, some involved workarounds + were implemented for automated dropping of columns + with SQL Server, which makes it extremely difficult + due to constraints, defaults being separate. + + Other database environments not included among + those two have *not* been tested, *at all*. This + includes MySQL, Firebird, Oracle, Sybase. Adding + support for these backends is *very easy*, and + many directives may work already if they conform + to standard forms. Please report all missing/ + incorrect behaviors to the bugtracker! Patches + are welcome here but are optional - please just + indicate the exact format expected by the target + database. + +- SQLite, as a backend, has almost no support for + schema alterations to existing databases. The author + would strongly recommend that SQLite not be used in + a migration context - just dump your SQLite database + into an intermediary format, then dump it back + into a new schema. For dev environments, the + dev installer should be building the whole DB from + scratch. Or just use Postgresql, which is a much + better database for non-trivial schemas. + Requests for full ALTER support on SQLite should be + reported to SQLite's bug tracker at + http://www.sqlite.org/src/wiki?name=Bug+Reports. + Requests for "please have SQLite rename the table + to a temptable then copy the data into a new table" + will be closed. Note that Alembic will at some + point offer an extensible API so that you can + implement commands like this yourself. + +- Well-tested directives include add/drop table, add/drop + column, including support for so-called "schema" + types, types which generate additional CHECK + constraints, i.e. Boolean, Enum. Other directives not + included here have *not* been strongly tested + in production, i.e. rename table, etc. + +- Both "online" and "offline" migrations have been strongly + production tested against Postgresql and SQL Server. + +- Modify column type/boolean is not as fully covered. + "Schema" types do add/drop the associated constraint + but this has not been widely tested, only in unit tests. + +- Many migrations are still outright missing, i.e. + create/add sequences, etc. As a workaround, + execute() can be used for those which are missing, + though posting of tickets for new features/missing + behaviors is strongly encouraged. + +- Autogenerate feature is implemented in a rudimentary + fashion. It's covered by unit and integration tests + and has had some basic rudimentary testing. The feature + has *not* been used in a production setting so is likely + missing lot of desirable behaviors. The + autogenerate feature only generates "sample" commands + which must be hand-tailored in any case, so this only + impacts the usefulness of the command, not overall + stability. + +- Support for tables in remote schemas, + i.e. "schemaname.tablename", is very poor. + Missing "schema" behaviors should be + reported as tickets, though in the author's + experience, migrations typically proceed only + within the default schema. diff --git a/alembic/autogenerate.py b/alembic/autogenerate.py index 173b8730..101a871e 100644 --- a/alembic/autogenerate.py +++ b/alembic/autogenerate.py @@ -190,10 +190,10 @@ def _invoke_command(updown, args): else: if updown == "upgrade": return cmd_callables[0]( - cmd_args[0], cmd_args[1], cmd_args[3]) + cmd_args[0], cmd_args[1], cmd_args[3], cmd_args[2]) else: return cmd_callables[0]( - cmd_args[0], cmd_args[1], cmd_args[2]) + cmd_args[0], cmd_args[1], cmd_args[2], cmd_args[3]) ################################################### # render python @@ -222,15 +222,17 @@ def _add_column(tname, column): def _drop_column(tname, column): return "drop_column(%r, %r)" % (tname, column.name) -def _modify_type(tname, cname, type_): - return "alter_column(%(tname)r, %(cname)r, type=%(prefix)s%(type)r)" % { +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)" % { 'prefix':_autogenerate_prefix(), 'tname':tname, 'cname':cname, - 'type':type_ + 'type':type_, + 'old_type':old_type } -def _modify_nullable(tname, cname, nullable): +def _modify_nullable(tname, cname, nullable, previous): return "alter_column(%r, %r, nullable=%r)" % ( tname, cname, nullable ) diff --git a/alembic/ddl/base.py b/alembic/ddl/base.py index da47306d..58e3006f 100644 --- a/alembic/ddl/base.py +++ b/alembic/ddl/base.py @@ -13,6 +13,11 @@ class AlterTable(DDLElement): self.table_name = table_name self.schema = schema +class RenameTable(AlterTable): + def __init__(self, old_table_name, new_table_name, schema=None): + super(RenameTable, self).__init__(old_table_name, schema=schema) + self.new_table_name = new_table_name + class AlterColumn(AlterTable): def __init__(self, name, column_name, schema=None): super(AlterColumn, self).__init__(name, schema=schema) @@ -48,6 +53,14 @@ class DropColumn(AlterTable): super(DropColumn, self).__init__(name, schema=schema) self.column = column + +@compiles(RenameTable) +def visit_rename_table(element, compiler, **kw): + return "%s RENAME TO %s" % ( + alter_table(compiler, element.table_name, element.schema), + format_table_name(compiler, element.new_table_name, element.schema) + ) + @compiles(AddColumn) def visit_add_column(element, compiler, **kw): return "%s %s" % ( @@ -70,6 +83,14 @@ def visit_column_nullable(element, compiler, **kw): "NULL" if element.nullable else "SET NOT NULL" ) +@compiles(ColumnType) +def visit_column_type(element, compiler, **kw): + return "%s %s %s" % ( + alter_table(compiler, element.table_name, element.schema), + alter_column(compiler, element.column_name), + "TYPE %s" % compiler.dialect.type_compiler.process(element.type_) + ) + @compiles(ColumnName) def visit_column_name(element, compiler, **kw): return "%s RENAME %s TO %s" % ( diff --git a/alembic/ddl/impl.py b/alembic/ddl/impl.py index 2cfaca7e..b82433e8 100644 --- a/alembic/ddl/impl.py +++ b/alembic/ddl/impl.py @@ -101,6 +101,10 @@ class DefaultImpl(object): def drop_constraint(self, const): self._exec(schema.DropConstraint(const)) + def rename_table(self, old_table_name, new_table_name, schema=None): + self._exec(base.RenameTable(old_table_name, + new_table_name, schema=schema)) + def create_table(self, table): self._exec(schema.CreateTable(table)) for index in table.indexes: diff --git a/alembic/op.py b/alembic/op.py index e1c8a937..aba629a0 100644 --- a/alembic/op.py +++ b/alembic/op.py @@ -18,6 +18,7 @@ __all__ = sorted([ 'create_index', 'inline_literal', 'bulk_insert', + 'rename_table', 'create_unique_constraint', 'get_context', 'get_bind', @@ -85,14 +86,59 @@ def _ensure_table_for_fk(metadata, fk): if not rel_t.c.contains_column(cname): rel_t.append_column(schema.Column(cname, NULLTYPE)) +def rename_table(old_table_name, new_table_name, schema=None): + """Emit an ALTER TABLE to rename a table. + + :param old_table_name: old name. + :param new_table_name: new name. + :param schema: Optional, name of schema to operate within. + + """ + get_impl().rename_table( + old_table_name, + new_table_name, + schema=schema + ) def alter_column(table_name, column_name, nullable=None, server_default=False, name=None, - type_=None + type_=None, + old_type=None, ): - """Issue an "alter column" instruction using the current change context.""" + """Issue an "alter column" instruction using the + current change context. + + :param table_name: string name of the target table. + :param column_name: string name of the target column. + :param nullable: Optional; specify ``True`` or ``False`` + to alter the column's nullability. + :param server_default: Optional; specify a string + SQL expression, :func:`~sqlalchemy.sql.expression.text`, + or :class:`~sqlalchemy.schema.DefaultClause` to alter + the column's default value. + :param name: Optional; new string name for the column + to alter the column's name. + :param type_: Optional; a :class:`~sqlalchemy.types.TypeEngine` + type object to specify a change to the column's type. + For SQLAlchemy types that also indicate a constraint (i.e. + :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`), + the constraint is also generated. + :param old_type: Optional; a :class:`~sqlalchemy.types.TypeEngine` + type object to specify the previous type. Currently this is used + if the "old" type is a SQLAlchemy type that also specifies a + constraint (i.e. + :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`), + so that the constraint can be dropped. + + """ + + if old_type: + t = _table(table_name, schema.Column(column_name, old_type)) + for constraint in t.constraints: + if not isinstance(constraint, schema.PrimaryKeyConstraint): + get_impl().drop_constraint(constraint) get_impl().alter_column(table_name, column_name, nullable=nullable, @@ -101,6 +147,12 @@ def alter_column(table_name, column_name, type_=type_ ) + if type_: + t = _table(table_name, schema.Column(column_name, type_)) + for constraint in t.constraints: + if not isinstance(constraint, schema.PrimaryKeyConstraint): + get_impl().add_constraint(constraint) + def add_column(table_name, column): """Issue an "add column" instruction using the current change context. diff --git a/docs/build/front.rst b/docs/build/front.rst index 854e4759..25248d5c 100644 --- a/docs/build/front.rst +++ b/docs/build/front.rst @@ -36,8 +36,9 @@ Community Alembic is developed by `Mike Bayer `_, and is loosely associated with the `SQLAlchemy `_ and `Pylons `_ projects. -As Alembic's usage increases, it is anticipated that the SQLAlchemy mailing list and IRC channel -will become the primary channels for support. + +User issues, discussion of potential bugs and features should be posted +to the Alembic Google Group at `sqlalchemy-alembic `_. Bugs ==== diff --git a/tests/test_autogenerate.py b/tests/test_autogenerate.py index 7b30a111..119e9867 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)) + 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)) + 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 ###""") @@ -191,13 +191,14 @@ class AutogenRenderTest(TestCase): def test_render_modify_type(self): eq_( autogenerate._modify_type( - "sometable", "somecolumn", CHAR(10)), - "alter_column('sometable', 'somecolumn', type=sa.CHAR(length=10))" + "sometable", "somecolumn", CHAR(10), CHAR(20)), + "alter_column('sometable', 'somecolumn', " + "type=sa.CHAR(length=10), old_type=sa.CHAR(length=20))" ) def test_render_modify_nullable(self): eq_( autogenerate._modify_nullable( - "sometable", "somecolumn", True), + "sometable", "somecolumn", True, "X"), "alter_column('sometable', 'somecolumn', nullable=True)" ) diff --git a/tests/test_op.py b/tests/test_op.py index c9f5e77c..cf8e6fbd 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -7,6 +7,16 @@ from sqlalchemy import Integer, Column, ForeignKey, \ Boolean from sqlalchemy.sql import table +def test_rename_table(): + context = _op_fixture() + op.rename_table('t1', 't2') + context.assert_("ALTER TABLE t1 RENAME TO t2") + +def test_rename_table_schema(): + context = _op_fixture() + op.rename_table('t1', 't2', schema="foo") + context.assert_("ALTER TABLE foo.t1 RENAME TO foo.t2") + def test_add_column(): context = _op_fixture() op.add_column('t1', Column('c1', Integer, nullable=False)) @@ -80,6 +90,37 @@ def test_alter_column_rename(): "ALTER TABLE t RENAME c TO x" ) +def test_alter_column_type(): + context = _op_fixture() + op.alter_column("t", "c", type_=String(50)) + context.assert_( + 'ALTER TABLE t ALTER COLUMN c TYPE VARCHAR(50)' + ) + +def test_alter_column_schema_type_unnamed(): + context = _op_fixture('mssql') + op.alter_column("t", "c", type_=Boolean()) + context.assert_( + 'ALTER TABLE t ALTER COLUMN c TYPE BIT', + 'ALTER TABLE t ADD CHECK (c IN (0, 1))' + ) + +def test_alter_column_schema_type_named(): + context = _op_fixture('mssql') + op.alter_column("t", "c", type_=Boolean(name="xyz")) + context.assert_( + 'ALTER TABLE t ALTER COLUMN c TYPE BIT', + 'ALTER TABLE t ADD CONSTRAINT xyz CHECK (c IN (0, 1))' + ) + +def test_alter_column_schema_type_old_type(): + context = _op_fixture('mssql') + op.alter_column("t", "c", type_=String(10), old_type=Boolean(name="xyz")) + context.assert_( + 'ALTER TABLE t DROP CONSTRAINT xyz', + 'ALTER TABLE t ALTER COLUMN c TYPE VARCHAR(10)' + ) + def test_add_foreign_key(): context = _op_fixture() op.create_foreign_key('fk_test', 't1', 't2',