From: Mike Waites Date: Fri, 31 Aug 2018 18:56:36 +0000 (+0100) Subject: Implemented support for Table and Column Comments X-Git-Tag: rel_1_0_6~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ec8db7d875f486cde2d02942bd8409abf37e3704;p=thirdparty%2Fsqlalchemy%2Falembic.git Implemented support for Table and Column Comments Added Table and Column level comments for supported backends. `create_table`, `add_column` and `alter_column` now optionally take `comment="X"` kwarg. Support for autogenerate for Table and Column objects has also been added Fixes: #422 Change-Id: I1fd37bb7fe3d167baf7b1e7bf7ff5bfd48e7cf54 --- diff --git a/alembic/autogenerate/compare.py b/alembic/autogenerate/compare.py index 1bea2552..df1e5096 100644 --- a/alembic/autogenerate/compare.py +++ b/alembic/autogenerate/compare.py @@ -921,6 +921,32 @@ def _compare_server_default( log.info("Detected server default on column '%s.%s'", tname, cname) +@comparators.dispatch_for("column") +def _compare_column_comment( + autogen_context, + alter_column_op, + schema, + tname, + cname, + conn_col, + metadata_col, +): + + if not sqla_compat._dialect_supports_comments(autogen_context.dialect): + return + + metadata_comment = metadata_col.comment + conn_col_comment = conn_col.comment + if conn_col_comment is None and metadata_comment is None: + return False + + alter_column_op.existing_comment = conn_col_comment + + if conn_col_comment != metadata_comment: + alter_column_op.modify_comment = metadata_comment + log.info("Detected column comment '%s.%s'", tname, cname) + + @comparators.dispatch_for("table") def _compare_foreign_keys( autogen_context, @@ -1028,3 +1054,41 @@ def _compare_foreign_keys( else None ) _add_fk(const, compare_to) + + +@comparators.dispatch_for("table") +def _compare_table_comment( + autogen_context, + modify_table_ops, + schema, + tname, + conn_table, + metadata_table, +): + + if not sqla_compat._dialect_supports_comments(autogen_context.dialect): + return + + # if we're doing CREATE TABLE, comments will be created inline + # with the create_table op. + if conn_table is None or metadata_table is None: + return + + if conn_table.comment is None and metadata_table.comment is None: + return + + if metadata_table.comment is None and conn_table.comment is not None: + modify_table_ops.ops.append( + ops.DropTableCommentOp( + tname, existing_comment=conn_table.comment, schema=schema + ) + ) + elif metadata_table.comment != conn_table.comment: + modify_table_ops.ops.append( + ops.CreateTableCommentOp( + tname, + metadata_table.comment, + existing_comment=conn_table.comment, + schema=schema, + ) + ) diff --git a/alembic/autogenerate/render.py b/alembic/autogenerate/render.py index 3b6e9353..64dbaebf 100644 --- a/alembic/autogenerate/render.py +++ b/alembic/autogenerate/render.py @@ -117,6 +117,50 @@ def _render_modify_table(autogen_context, op): return ["pass"] +@renderers.dispatch_for(ops.CreateTableCommentOp) +def _render_create_table_comment(autogen_context, op): + + templ = ( + "{prefix}create_table_comment(\n" + "{indent}'{tname}',\n" + "{indent}{comment},\n" + "{indent}existing_comment={existing},\n" + "{indent}schema={schema}\n" + ")" + ) + return templ.format( + prefix=_alembic_autogenerate_prefix(autogen_context), + tname=op.table_name, + comment="'%s'" % op.comment if op.comment is not None else None, + existing="'%s'" % op.existing_comment + if op.existing_comment is not None + else None, + schema="'%s'" % op.schema if op.schema is not None else None, + indent=" ", + ) + + +@renderers.dispatch_for(ops.DropTableCommentOp) +def _render_drop_table_comment(autogen_context, op): + + templ = ( + "{prefix}drop_table_comment(\n" + "{indent}'{tname}',\n" + "{indent}existing_comment={existing},\n" + "{indent}schema={schema}\n" + ")" + ) + return templ.format( + prefix=_alembic_autogenerate_prefix(autogen_context), + tname=op.table_name, + existing="'%s'" % op.existing_comment + if op.existing_comment is not None + else None, + schema="'%s'" % op.schema if op.schema is not None else None, + indent=" ", + ) + + @renderers.dispatch_for(ops.CreateTableOp) def _add_table(autogen_context, op): table = op.to_table() @@ -150,6 +194,10 @@ def _add_table(autogen_context, op): } if op.schema: text += ",\nschema=%r" % _ident(op.schema) + + comment = sqla_compat._comment_attribute(table) + if comment: + text += ",\ncomment=%r" % _ident(comment) for k in sorted(op.kw): text += ",\n%s=%r" % (k.replace(" ", "_"), op.kw[k]) text += "\n)" @@ -357,9 +405,11 @@ def _alter_column(autogen_context, op): server_default = op.modify_server_default type_ = op.modify_type nullable = op.modify_nullable + comment = op.modify_comment autoincrement = op.kw.get("autoincrement", None) existing_type = op.existing_type existing_nullable = op.existing_nullable + existing_comment = op.existing_comment existing_server_default = op.existing_server_default schema = op.schema @@ -388,6 +438,10 @@ def _alter_column(autogen_context, op): text += ",\n%stype_=%s" % (indent, _repr_type(type_, autogen_context)) if nullable is not None: text += ",\n%snullable=%r" % (indent, nullable) + if comment is not False: + text += ",\n%scomment=%r" % (indent, comment) + if existing_comment is not None: + text += ",\n%sexisting_comment=%r" % (indent, existing_comment) if nullable is None and existing_nullable is not None: text += ",\n%sexisting_nullable=%r" % (indent, existing_nullable) if autoincrement is not None: @@ -558,6 +612,10 @@ def _render_column(column, autogen_context): if column.system: opts.append(("system", column.system)) + comment = sqla_compat._comment_attribute(column) + if comment: + opts.append(("comment", "'%s'" % comment)) + # TODO: for non-ascii colname, assign a "key" return "%(prefix)sColumn(%(name)r, %(type)s, %(kw)s)" % { "prefix": _sqlalchemy_autogenerate_prefix(autogen_context), diff --git a/alembic/ddl/base.py b/alembic/ddl/base.py index b0de1365..9bdb0bef 100644 --- a/alembic/ddl/base.py +++ b/alembic/ddl/base.py @@ -48,6 +48,7 @@ class AlterColumn(AlterTable): existing_type=None, existing_nullable=None, existing_server_default=None, + existing_comment=None, ): super(AlterColumn, self).__init__(name, schema=schema) self.column_name = column_name @@ -58,6 +59,7 @@ class AlterColumn(AlterTable): ) self.existing_nullable = existing_nullable self.existing_server_default = existing_server_default + self.existing_comment = existing_comment class ColumnNullable(AlterColumn): @@ -96,6 +98,12 @@ class DropColumn(AlterTable): self.column = column +class ColumnComment(AlterColumn): + def __init__(self, name, column_name, comment, **kw): + super(ColumnComment, self).__init__(name, column_name, **kw) + self.comment = comment + + @compiles(RenameTable) def visit_rename_table(element, compiler, **kw): return "%s RENAME TO %s" % ( diff --git a/alembic/ddl/impl.py b/alembic/ddl/impl.py index dd1be58b..5df7c040 100644 --- a/alembic/ddl/impl.py +++ b/alembic/ddl/impl.py @@ -146,6 +146,8 @@ class DefaultImpl(with_metaclass(ImplMeta)): type_=None, schema=None, autoincrement=None, + comment=False, + existing_comment=None, existing_type=None, existing_server_default=None, existing_nullable=None, @@ -166,6 +168,7 @@ class DefaultImpl(with_metaclass(ImplMeta)): existing_type=existing_type, existing_server_default=existing_server_default, existing_nullable=existing_nullable, + existing_comment=existing_comment, ) ) if server_default is not False: @@ -178,6 +181,7 @@ class DefaultImpl(with_metaclass(ImplMeta)): existing_type=existing_type, existing_server_default=existing_server_default, existing_nullable=existing_nullable, + existing_comment=existing_comment, ) ) if type_ is not None: @@ -190,8 +194,24 @@ class DefaultImpl(with_metaclass(ImplMeta)): existing_type=existing_type, existing_server_default=existing_server_default, existing_nullable=existing_nullable, + existing_comment=existing_comment, ) ) + + if comment is not False: + self._exec( + base.ColumnComment( + table_name, + column_name, + comment, + schema=schema, + existing_type=existing_type, + existing_server_default=existing_server_default, + existing_nullable=existing_nullable, + existing_comment=existing_comment, + ) + ) + # do the new name last ;) if name is not None: self._exec( @@ -235,12 +255,34 @@ class DefaultImpl(with_metaclass(ImplMeta)): for index in table.indexes: self._exec(schema.CreateIndex(index)) + with_comment = ( + sqla_compat._dialect_supports_comments(self.dialect) + and not self.dialect.inline_comments + ) + comment = sqla_compat._comment_attribute(table) + if comment and with_comment: + self.create_table_comment(table) + + for column in table.columns: + comment = sqla_compat._comment_attribute(column) + if comment and with_comment: + self.create_column_comment(column) + def drop_table(self, table): self._exec(schema.DropTable(table)) def create_index(self, index): self._exec(schema.CreateIndex(index)) + def create_table_comment(self, table): + self._exec(schema.SetTableComment(table)) + + def drop_table_comment(self, table): + self._exec(schema.DropTableComment(table)) + + def create_column_comment(self, column): + self._exec(schema.SetColumnComment(column)) + def drop_index(self, index): self._exec(schema.DropIndex(index)) diff --git a/alembic/ddl/mysql.py b/alembic/ddl/mysql.py index 29f416e6..d20aec68 100644 --- a/alembic/ddl/mysql.py +++ b/alembic/ddl/mysql.py @@ -39,6 +39,8 @@ class MySQLImpl(DefaultImpl): existing_nullable=None, autoincrement=None, existing_autoincrement=None, + comment=False, + existing_comment=None, **kw ): if name is not None: @@ -66,6 +68,7 @@ class MySQLImpl(DefaultImpl): nullable is not None or type_ is not None or autoincrement is not None + or comment is not False ): self._exec( MySQLModifyColumn( @@ -85,6 +88,9 @@ class MySQLImpl(DefaultImpl): autoincrement=autoincrement if autoincrement is not None else existing_autoincrement, + comment=comment + if comment is not False + else existing_comment, ) ) elif server_default is not False: @@ -276,6 +282,7 @@ class MySQLChangeColumn(AlterColumn): nullable=None, default=False, autoincrement=None, + comment=False, ): super(AlterColumn, self).__init__(name, schema=schema) self.column_name = column_name @@ -283,6 +290,7 @@ class MySQLChangeColumn(AlterColumn): self.newname = newname self.default = default self.autoincrement = autoincrement + self.comment = comment if type_ is None: raise util.CommandError( "All MySQL CHANGE/MODIFY COLUMN operations " @@ -328,6 +336,7 @@ def _mysql_modify_column(element, compiler, **kw): server_default=element.default, type_=element.type_, autoincrement=element.autoincrement, + comment=element.comment, ), ) @@ -344,6 +353,7 @@ def _mysql_change_column(element, compiler, **kw): server_default=element.default, type_=element.type_, autoincrement=element.autoincrement, + comment=element.comment, ), ) @@ -355,7 +365,9 @@ def _render_value(compiler, expr): return compiler.sql_compiler.process(expr) -def _mysql_colspec(compiler, nullable, server_default, type_, autoincrement): +def _mysql_colspec( + compiler, nullable, server_default, type_, autoincrement, comment +): spec = "%s %s" % ( compiler.dialect.type_compiler.process(type_), "NULL" if nullable else "NOT NULL", @@ -364,6 +376,10 @@ def _mysql_colspec(compiler, nullable, server_default, type_, autoincrement): spec += " AUTO_INCREMENT" if server_default is not False and server_default is not None: spec += " DEFAULT %s" % _render_value(compiler, server_default) + if comment: + spec += " COMMENT %s" % compiler.sql_compiler.render_literal_value( + comment, sqltypes.String() + ) return spec diff --git a/alembic/ddl/oracle.py b/alembic/ddl/oracle.py index 4bd92e3a..76cf7c5d 100644 --- a/alembic/ddl/oracle.py +++ b/alembic/ddl/oracle.py @@ -1,7 +1,9 @@ from sqlalchemy.ext.compiler import compiles +from sqlalchemy.sql import sqltypes from .base import AddColumn from .base import alter_table +from .base import ColumnComment from .base import ColumnDefault from .base import ColumnName from .base import ColumnNullable @@ -83,6 +85,22 @@ def visit_column_default(element, compiler, **kw): ) +@compiles(ColumnComment, "oracle") +def visit_column_comment(element, compiler, **kw): + ddl = "COMMENT ON COLUMN {table_name}.{column_name} IS {comment}" + + comment = compiler.sql_compiler.render_literal_value( + (element.comment if element.comment is not None else ""), + sqltypes.String(), + ) + + return ddl.format( + table_name=element.table_name, + column_name=element.column_name, + comment=comment, + ) + + def alter_column(compiler, name): return "MODIFY %s" % format_column_name(compiler, name) diff --git a/alembic/ddl/postgresql.py b/alembic/ddl/postgresql.py index d5d8e6e3..a8e332ec 100644 --- a/alembic/ddl/postgresql.py +++ b/alembic/ddl/postgresql.py @@ -14,6 +14,7 @@ from sqlalchemy.types import NULLTYPE from .base import alter_column from .base import alter_table from .base import AlterColumn +from .base import ColumnComment from .base import compiles from .base import format_table_name from .base import format_type @@ -277,6 +278,24 @@ def visit_column_type(element, compiler, **kw): ) +@compiles(ColumnComment, "postgresql") +def visit_column_comment(element, compiler, **kw): + ddl = "COMMENT ON COLUMN {table_name}.{column_name} IS {comment}" + comment = ( + compiler.sql_compiler.render_literal_value( + element.comment, sqltypes.String() + ) + if element.comment is not None + else "NULL" + ) + + return ddl.format( + table_name=element.table_name, + column_name=element.column_name, + comment=comment, + ) + + @Operations.register_operation("create_exclude_constraint") @BatchOperations.register_operation( "create_exclude_constraint", "batch_create_exclude_constraint" diff --git a/alembic/operations/__init__.py b/alembic/operations/__init__.py index 3f8f0bd9..dc2d3a49 100644 --- a/alembic/operations/__init__.py +++ b/alembic/operations/__init__.py @@ -4,5 +4,4 @@ from .base import Operations from .ops import MigrateOperation - __all__ = ["Operations", "BatchOperations", "MigrateOperation"] diff --git a/alembic/operations/ops.py b/alembic/operations/ops.py index 1b104f09..c85a25d5 100644 --- a/alembic/operations/ops.py +++ b/alembic/operations/ops.py @@ -1352,6 +1352,119 @@ class RenameTableOp(AlterTableOp): return operations.invoke(op) +@Operations.register_operation("create_table_comment") +class CreateTableCommentOp(AlterTableOp): + """Represent a COMMENT ON `table` operation. + """ + + def __init__( + self, table_name, comment, schema=None, existing_comment=None + ): + self.table_name = table_name + self.comment = comment + self.existing_comment = existing_comment + self.schema = schema + + @classmethod + def create_table_comment( + cls, + operations, + table_name, + comment, + existing_comment=None, + schema=None, + ): + """Invokes the `:func:`alembic.operations.toimpl.create_table_comment` + impl to initiate a new COMMENT ON `table` operation. + + :param table_name: string name of the target table. + :param comment: string value of the comment being registered against + the specified table. + :param existing_comment: An optional string value of a comment + already registered + on the specified table. + + """ + + op = cls( + table_name, + comment, + existing_comment=existing_comment, + schema=schema, + ) + return operations.invoke(op) + + def reverse(self): + """Reverses the COMMENT ON operation against a table. + """ + if self.existing_comment is None: + return DropTableCommentOp( + self.table_name, + existing_comment=self.comment, + schema=self.schema, + ) + else: + return CreateTableCommentOp( + self.table_name, + self.existing_comment, + existing_comment=self.comment, + schema=self.schema, + ) + + def to_table(self, migration_context=None): + schema_obj = schemaobj.SchemaObjects(migration_context) + + return schema_obj.table( + self.table_name, schema=self.schema, comment=self.comment + ) + + def to_diff_tuple(self): + return ("add_table_comment", self.to_table(), self.existing_comment) + + +@Operations.register_operation("drop_table_comment") +class DropTableCommentOp(AlterTableOp): + """Represent a COMMENT ON `table` operation. + """ + + def __init__(self, table_name, schema=None, existing_comment=None): + self.table_name = table_name + self.existing_comment = existing_comment + self.schema = schema + + @classmethod + def drop_table_comment( + cls, operations, table_name, existing_comment=None, schema=None + ): + """Invokes the `:func:`alembic.operations.toimpl.drop_table_comment` to + remove an existing comment set on a table. + + :param table_name: string name of the target table. + :param existing_comment: An optional string value of a comment already + registered + on the specified table. + + """ + + op = cls(table_name, existing_comment=existing_comment, schema=schema) + return operations.invoke(op) + + def reverse(self): + """Reverses the COMMENT ON operation against a table. + """ + return CreateTableCommentOp( + self.table_name, self.existing_comment, schema=self.schema + ) + + def to_table(self, migration_context=None): + schema_obj = schemaobj.SchemaObjects(migration_context) + + return schema_obj.table(self.table_name, schema=self.schema) + + def to_diff_tuple(self): + return ("remove_table_comment", self.to_table()) + + @Operations.register_operation("alter_column") @BatchOperations.register_operation("alter_column", "batch_alter_column") class AlterColumnOp(AlterTableOp): @@ -1365,7 +1478,9 @@ class AlterColumnOp(AlterTableOp): existing_type=None, existing_server_default=False, existing_nullable=None, + existing_comment=None, modify_nullable=None, + modify_comment=False, modify_server_default=False, modify_name=None, modify_type=None, @@ -1376,7 +1491,9 @@ class AlterColumnOp(AlterTableOp): self.existing_type = existing_type self.existing_server_default = existing_server_default self.existing_nullable = existing_nullable + self.existing_comment = existing_comment self.modify_nullable = modify_nullable + self.modify_comment = modify_comment self.modify_server_default = modify_server_default self.modify_name = modify_name self.modify_type = modify_type @@ -1398,6 +1515,7 @@ class AlterColumnOp(AlterTableOp): "existing_server_default": ( self.existing_server_default ), + "existing_comment": self.existing_comment, }, self.existing_type, self.modify_type, @@ -1416,6 +1534,7 @@ class AlterColumnOp(AlterTableOp): "existing_server_default": ( self.existing_server_default ), + "existing_comment": self.existing_comment, }, self.existing_nullable, self.modify_nullable, @@ -1432,12 +1551,32 @@ class AlterColumnOp(AlterTableOp): { "existing_nullable": self.existing_nullable, "existing_type": self.existing_type, + "existing_comment": self.existing_comment, }, self.existing_server_default, self.modify_server_default, ) ) + if self.modify_comment is not False: + col_diff.append( + ( + "modify_comment", + schema, + tname, + cname, + { + "existing_nullable": self.existing_nullable, + "existing_type": self.existing_type, + "existing_server_default": ( + self.existing_server_default + ), + }, + self.existing_comment, + self.modify_comment, + ) + ) + return col_diff def has_changes(self): @@ -1445,6 +1584,7 @@ class AlterColumnOp(AlterTableOp): self.modify_nullable is not None or self.modify_server_default is not False or self.modify_type is not None + or self.modify_comment is not False ) if hc1: return True @@ -1460,12 +1600,15 @@ class AlterColumnOp(AlterTableOp): kw["existing_type"] = self.existing_type kw["existing_nullable"] = self.existing_nullable kw["existing_server_default"] = self.existing_server_default + kw["existing_comment"] = self.existing_comment if self.modify_type is not None: kw["modify_type"] = self.modify_type if self.modify_nullable is not None: kw["modify_nullable"] = self.modify_nullable if self.modify_server_default is not False: kw["modify_server_default"] = self.modify_server_default + if self.modify_comment is not False: + kw["modify_comment"] = self.modify_comment # TODO: make this a little simpler all_keys = set( @@ -1492,12 +1635,14 @@ class AlterColumnOp(AlterTableOp): table_name, column_name, nullable=None, + comment=False, server_default=False, new_column_name=None, type_=None, existing_type=None, existing_server_default=False, existing_nullable=None, + existing_comment=None, schema=None, **kw ): @@ -1592,10 +1737,12 @@ class AlterColumnOp(AlterTableOp): existing_type=existing_type, existing_server_default=existing_server_default, existing_nullable=existing_nullable, + existing_comment=existing_comment, modify_name=new_column_name, modify_type=type_, modify_server_default=server_default, modify_nullable=nullable, + modify_comment=comment, **kw ) @@ -1607,12 +1754,14 @@ class AlterColumnOp(AlterTableOp): operations, column_name, nullable=None, + comment=False, server_default=False, new_column_name=None, type_=None, existing_type=None, existing_server_default=False, existing_nullable=None, + existing_comment=None, **kw ): """Issue an "alter column" instruction using the current @@ -1630,10 +1779,12 @@ class AlterColumnOp(AlterTableOp): existing_type=existing_type, existing_server_default=existing_server_default, existing_nullable=existing_nullable, + existing_comment=existing_comment, modify_name=new_column_name, modify_type=type_, modify_server_default=server_default, modify_nullable=nullable, + modify_comment=comment, **kw ) diff --git a/alembic/operations/toimpl.py b/alembic/operations/toimpl.py index ad8d7a23..56994235 100644 --- a/alembic/operations/toimpl.py +++ b/alembic/operations/toimpl.py @@ -2,6 +2,7 @@ from sqlalchemy import schema as sa_schema from . import ops from .base import Operations +from ..util import sqla_compat @Operations.implementation_for(ops.AlterColumnOp) @@ -21,6 +22,8 @@ def alter_column(operations, operation): server_default = operation.modify_server_default new_column_name = operation.modify_name nullable = operation.modify_nullable + comment = operation.modify_comment + existing_comment = operation.existing_comment def _count_constraint(constraint): return not isinstance(constraint, sa_schema.PrimaryKeyConstraint) and ( @@ -48,6 +51,8 @@ def alter_column(operations, operation): existing_type=existing_type, existing_server_default=existing_server_default, existing_nullable=existing_nullable, + comment=comment, + existing_comment=existing_comment, **operation.kw ) @@ -104,6 +109,18 @@ def rename_table(operations, operation): ) +@Operations.implementation_for(ops.CreateTableCommentOp) +def create_table_comment(operations, operation): + table = operation.to_table(operations.migration_context) + operations.impl.create_table_comment(table) + + +@Operations.implementation_for(ops.DropTableCommentOp) +def drop_table_comment(operations, operation): + table = operation.to_table(operations.migration_context) + operations.impl.drop_table_comment(table) + + @Operations.implementation_for(ops.AddColumnOp) def add_column(operations, operation): table_name = operation.table_name @@ -118,6 +135,14 @@ def add_column(operations, operation): for index in t.indexes: operations.impl.create_index(index) + with_comment = ( + sqla_compat._dialect_supports_comments(operations.impl.dialect) + and not operations.impl.dialect.inline_comments + ) + comment = sqla_compat._comment_attribute(column) + if comment and with_comment: + operations.impl.create_column_comment(column) + @Operations.implementation_for(ops.AddConstraintOp) def create_constraint(operations, operation): diff --git a/alembic/testing/requirements.py b/alembic/testing/requirements.py index 8c02254d..90b51ba9 100644 --- a/alembic/testing/requirements.py +++ b/alembic/testing/requirements.py @@ -1,4 +1,5 @@ from alembic import util +from alembic.util import sqla_compat from . import exclusions if util.sqla_094: @@ -135,7 +136,26 @@ class SuiteRequirements(Requirements): "SQLAlchemy 1.1.0 or greater required", ) + @property + def sqlalchemy_1216(self): + return exclusions.skip_if( + lambda config: not util.sqla_1216, + "SQLAlchemy 1.2.16 or greater required", + ) + @property def pep3147(self): return exclusions.only_if(lambda config: util.compat.has_pep3147()) + + @property + def comments(self): + return exclusions.only_if( + lambda config: sqla_compat._dialect_supports_comments( + config.db.dialect + ) + ) + + @property + def comments_api(self): + return exclusions.only_if(lambda config: util.sqla_120) diff --git a/alembic/util/__init__.py b/alembic/util/__init__.py index 18d25cdf..88b74319 100644 --- a/alembic/util/__init__.py +++ b/alembic/util/__init__.py @@ -31,6 +31,8 @@ from .sqla_compat import sqla_1014 # noqa from .sqla_compat import sqla_105 # noqa from .sqla_compat import sqla_110 # noqa from .sqla_compat import sqla_1115 # noqa +from .sqla_compat import sqla_120 # noqa +from .sqla_compat import sqla_1216 # noqa if not sqla_09: diff --git a/alembic/util/sqla_compat.py b/alembic/util/sqla_compat.py index c20f318e..82250d00 100644 --- a/alembic/util/sqla_compat.py +++ b/alembic/util/sqla_compat.py @@ -36,6 +36,8 @@ sqla_1010 = _vers >= (1, 0, 10) sqla_110 = _vers >= (1, 1, 0) sqla_1014 = _vers >= (1, 0, 14) sqla_1115 = _vers >= (1, 1, 15) +sqla_120 = _vers >= (1, 2, 0) +sqla_1216 = _vers >= (1, 2, 16) if sqla_110: @@ -214,6 +216,22 @@ def _get_index_final_name(dialect, idx): return dialect.ddl_compiler(dialect, None)._prepared_index_name(idx) +def _dialect_supports_comments(dialect): + if sqla_120: + return dialect.supports_comments + else: + return False + + +def _comment_attribute(obj): + """return the .comment attribute from a Table or Column""" + + if sqla_120: + return obj.comment + else: + return None + + def _is_mariadb(mysql_dialect): return "MariaDB" in mysql_dialect.server_version_info diff --git a/docs/build/unreleased/422.rst b/docs/build/unreleased/422.rst new file mode 100644 index 00000000..d9d1ce01 --- /dev/null +++ b/docs/build/unreleased/422.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: feature, operations + :tickets: 422 + + Added Table and Column level comments for supported backends. + `create_table`, `add_column` and `alter_column` now all optionally + take `comment="X"` kwarg. Support for autogenerate for Table + and Column objects has also been added. \ No newline at end of file diff --git a/tests/test_autogen_comments.py b/tests/test_autogen_comments.py new file mode 100644 index 00000000..97d1df66 --- /dev/null +++ b/tests/test_autogen_comments.py @@ -0,0 +1,246 @@ +import sys + +from sqlalchemy import Column +from sqlalchemy import Float +from sqlalchemy import MetaData +from sqlalchemy import String +from sqlalchemy import Table + +from alembic.testing import eq_ +from alembic.testing import mock +from alembic.testing import TestBase +from ._autogen_fixtures import AutogenFixtureTest + +py3k = sys.version_info.major >= 3 + + +class AutogenerateCommentsTest(AutogenFixtureTest, TestBase): + __backend__ = True + + __requires__ = ("comments",) + + def test_existing_table_comment_no_change(self): + m1 = MetaData() + m2 = MetaData() + + Table( + "some_table", + m1, + Column("test", String(10), primary_key=True), + comment="this is some table", + ) + + Table( + "some_table", + m2, + Column("test", String(10), primary_key=True), + comment="this is some table", + ) + + diffs = self._fixture(m1, m2) + + eq_(diffs, []) + + def test_add_table_comment(self): + m1 = MetaData() + m2 = MetaData() + + Table("some_table", m1, Column("test", String(10), primary_key=True)) + + Table( + "some_table", + m2, + Column("test", String(10), primary_key=True), + comment="this is some table", + ) + + diffs = self._fixture(m1, m2) + + eq_(diffs[0][0], "add_table_comment") + eq_(diffs[0][1].comment, "this is some table") + eq_(diffs[0][2], None) + + def test_remove_table_comment(self): + m1 = MetaData() + m2 = MetaData() + + Table( + "some_table", + m1, + Column("test", String(10), primary_key=True), + comment="this is some table", + ) + + Table("some_table", m2, Column("test", String(10), primary_key=True)) + + diffs = self._fixture(m1, m2) + + eq_(diffs[0][0], "remove_table_comment") + eq_(diffs[0][1].comment, None) + + def test_alter_table_comment(self): + m1 = MetaData() + m2 = MetaData() + + Table( + "some_table", + m1, + Column("test", String(10), primary_key=True), + comment="this is some table", + ) + + Table( + "some_table", + m2, + Column("test", String(10), primary_key=True), + comment="this is also some table", + ) + + diffs = self._fixture(m1, m2) + + eq_(diffs[0][0], "add_table_comment") + eq_(diffs[0][1].comment, "this is also some table") + eq_(diffs[0][2], "this is some table") + + def test_existing_column_comment_no_change(self): + m1 = MetaData() + m2 = MetaData() + + Table( + "some_table", + m1, + Column("test", String(10), primary_key=True), + Column("amount", Float, comment="the amount"), + ) + + Table( + "some_table", + m2, + Column("test", String(10), primary_key=True), + Column("amount", Float, comment="the amount"), + ) + + diffs = self._fixture(m1, m2) + + eq_(diffs, []) + + def test_add_column_comment(self): + m1 = MetaData() + m2 = MetaData() + + Table( + "some_table", + m1, + Column("test", String(10), primary_key=True), + Column("amount", Float), + ) + + Table( + "some_table", + m2, + Column("test", String(10), primary_key=True), + Column("amount", Float, comment="the amount"), + ) + + diffs = self._fixture(m1, m2) + eq_( + diffs, + [ + [ + ( + "modify_comment", + None, + "some_table", + "amount", + { + "existing_nullable": True, + "existing_type": mock.ANY, + "existing_server_default": False, + }, + None, + "the amount", + ) + ] + ], + ) + + def test_remove_column_comment(self): + m1 = MetaData() + m2 = MetaData() + + Table( + "some_table", + m1, + Column("test", String(10), primary_key=True), + Column("amount", Float, comment="the amount"), + ) + + Table( + "some_table", + m2, + Column("test", String(10), primary_key=True), + Column("amount", Float), + ) + + diffs = self._fixture(m1, m2) + eq_( + diffs, + [ + [ + ( + "modify_comment", + None, + "some_table", + "amount", + { + "existing_nullable": True, + "existing_type": mock.ANY, + "existing_server_default": False, + }, + "the amount", + None, + ) + ] + ], + ) + + def test_alter_column_comment(self): + m1 = MetaData() + m2 = MetaData() + + Table( + "some_table", + m1, + Column("test", String(10), primary_key=True), + Column("amount", Float, comment="the amount"), + ) + + Table( + "some_table", + m2, + Column("test", String(10), primary_key=True), + Column("amount", Float, comment="the adjusted amount"), + ) + + diffs = self._fixture(m1, m2) + + eq_( + diffs, + [ + [ + ( + "modify_comment", + None, + "some_table", + "amount", + { + "existing_nullable": True, + "existing_type": mock.ANY, + "existing_server_default": False, + }, + "the amount", + "the adjusted amount", + ) + ] + ], + ) diff --git a/tests/test_autogen_composition.py b/tests/test_autogen_composition.py index 6c31d52e..d6664b2a 100644 --- a/tests/test_autogen_composition.py +++ b/tests/test_autogen_composition.py @@ -183,7 +183,7 @@ nullable=True)) batch_op.drop_index('pw_idx') batch_op.drop_column('pw') - # ### end Alembic commands ###""" # noqa, + # ### end Alembic commands ###""", # noqa, ) eq_( @@ -219,7 +219,7 @@ nullable=True)) sa.ForeignKeyConstraint(['uid'], ['user.id'], ) ) op.drop_table('item') - # ### end Alembic commands ###""" # noqa, + # ### end Alembic commands ###""", # noqa, ) def test_imports_maintined(self): diff --git a/tests/test_autogen_render.py b/tests/test_autogen_render.py index 6465b34a..7673a493 100644 --- a/tests/test_autogen_render.py +++ b/tests/test_autogen_render.py @@ -1143,6 +1143,18 @@ class AutogenRenderTest(TestBase): "nullable=False)", ) + @config.requirements.comments_api + def test_render_col_with_comment(self): + c = Column("some_key", Integer, comment="This is a comment") + Table("some_table", MetaData(), c) + result = autogenerate.render._render_column(c, self.autogen_context) + eq_ignore_whitespace( + result, + "sa.Column('some_key', sa.Integer(), " + "nullable=True, " + "comment='This is a comment')", + ) + def test_render_col_autoinc_false_mysql(self): c = Column("some_key", Integer, primary_key=True, autoincrement=False) Table("some_table", MetaData(), c) @@ -1760,6 +1772,128 @@ class AutogenRenderTest(TestBase): op_obj, ) + @config.requirements.comments_api + def test_render_alter_column_modify_comment(self): + op_obj = ops.AlterColumnOp( + "sometable", "somecolumn", modify_comment="This is a comment" + ) + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.alter_column('sometable', 'somecolumn', " + "comment='This is a comment')", + ) + + @config.requirements.comments_api + def test_render_alter_column_existing_comment(self): + op_obj = ops.AlterColumnOp( + "sometable", "somecolumn", existing_comment="This is a comment" + ) + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.alter_column('sometable', 'somecolumn', " + "existing_comment='This is a comment')", + ) + + @config.requirements.comments_api + def test_render_col_drop_comment(self): + op_obj = ops.AlterColumnOp( + "sometable", + "somecolumn", + existing_comment="This is a comment", + modify_comment=None, + ) + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.alter_column('sometable', 'somecolumn', " + "comment=None, " + "existing_comment='This is a comment')", + ) + + @config.requirements.comments_api + def test_render_table_with_comment(self): + m = MetaData() + t = Table( + "test", + m, + Column("id", Integer, primary_key=True), + Column("q", Integer, ForeignKey("address.id")), + comment="test comment", + ) + op_obj = ops.CreateTableOp.from_table(t) + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.create_table('test'," + "sa.Column('id', sa.Integer(), nullable=False)," + "sa.Column('q', sa.Integer(), nullable=True)," + "sa.ForeignKeyConstraint(['q'], ['address.id'], )," + "sa.PrimaryKeyConstraint('id')," + "comment='test comment'" + ")", + ) + + @config.requirements.comments_api + def test_render_add_column_with_comment(self): + op_obj = ops.AddColumnOp( + "foo", Column("x", Integer, comment="This is a Column") + ) + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.add_column('foo', sa.Column('x', sa.Integer(), " + "nullable=True, comment='This is a Column'))", + ) + + @config.requirements.comments_api + def test_render_create_table_comment_op(self): + op_obj = ops.CreateTableCommentOp("table_name", "comment") + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.create_table_comment(" + " 'table_name'," + " 'comment'," + " existing_comment=None," + " schema=None" + ")", + ) + + def test_render_create_table_comment_op_with_existing_comment(self): + op_obj = ops.CreateTableCommentOp( + "table_name", "comment", existing_comment="old comment" + ) + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.create_table_comment(" + " 'table_name'," + " 'comment'," + " existing_comment='old comment'," + " schema=None" + ")", + ) + + def test_render_create_table_comment_op_with_schema(self): + op_obj = ops.CreateTableCommentOp( + "table_name", "comment", schema="SomeSchema" + ) + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.create_table_comment(" + " 'table_name'," + " 'comment'," + " existing_comment=None," + " schema='SomeSchema'" + ")", + ) + + def test_render_drop_table_comment_op(self): + op_obj = ops.DropTableCommentOp("table_name") + eq_ignore_whitespace( + autogenerate.render_op_text(self.autogen_context, op_obj), + "op.drop_table_comment(" + " 'table_name'," + " existing_comment=None," + " schema=None" + ")", + ) + class RenderNamingConventionTest(TestBase): __requires__ = ("sqlalchemy_094",) diff --git a/tests/test_batch.py b/tests/test_batch.py index 31e23c5b..aa998539 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -695,9 +695,9 @@ class BatchAPITest(TestBase): with self._fixture() as batch: batch.add_column(column) - eq_( - batch.impl.operations.impl.mock_calls, - [mock.call.add_column("tname", column, schema=None)], + assert ( + mock.call.add_column("tname", column, schema=None) + in batch.impl.operations.impl.mock_calls ) def test_create_fk(self): diff --git a/tests/test_mysql.py b/tests/test_mysql.py index 7158358f..ba0abb6b 100644 --- a/tests/test_mysql.py +++ b/tests/test_mysql.py @@ -20,6 +20,43 @@ from alembic.testing.fixtures import TestBase class MySQLOpTest(TestBase): + @config.requirements.comments_api + def test_create_table_with_comment(self): + context = op_fixture("mysql") + op.create_table( + "t2", + Column("c1", Integer, primary_key=True), + comment="This is a table comment", + ) + context.assert_contains("COMMENT='This is a table comment'") + + @config.requirements.comments_api + def test_create_table_with_column_comments(self): + context = op_fixture("mysql") + op.create_table( + "t2", + Column("c1", Integer, primary_key=True, comment="c1 comment"), + Column("c2", Integer, comment="c2 comment"), + comment="This is a table comment", + ) + + context.assert_( + "CREATE TABLE t2 " + "(c1 INTEGER NOT NULL COMMENT 'c1 comment' AUTO_INCREMENT, " + # TODO: why is there no space at the end here? is that on the + # SQLA side? + "c2 INTEGER COMMENT 'c2 comment', PRIMARY KEY (c1))" + "COMMENT='This is a table comment'" + ) + + @config.requirements.comments_api + def test_add_column_with_comment(self): + context = op_fixture("mysql") + op.add_column("t", Column("q", Integer, comment="This is a comment")) + context.assert_( + "ALTER TABLE t ADD COLUMN q INTEGER COMMENT 'This is a comment'" + ) + def test_rename_column(self): context = op_fixture("mysql") op.alter_column( @@ -192,6 +229,100 @@ class MySQLOpTest(TestBase): server_default="q", ) + @config.requirements.comments_api + def test_alter_column_add_comment(self): + context = op_fixture("mysql") + op.alter_column( + "t1", + "c1", + comment="This is a column comment", + existing_type=Boolean(), + schema="foo", + ) + + context.assert_( + "ALTER TABLE foo.t1 MODIFY c1 BOOL NULL " + "COMMENT 'This is a column comment'" + ) + + @config.requirements.comments_api + def test_alter_column_add_comment_quoting(self): + context = op_fixture("mysql") + op.alter_column( + "t1", + "c1", + comment="This is a 'column' comment", + existing_type=Boolean(), + schema="foo", + ) + + context.assert_( + "ALTER TABLE foo.t1 MODIFY c1 BOOL NULL " + "COMMENT 'This is a ''column'' comment'" + ) + + @config.requirements.comments_api + def test_alter_column_drop_comment(self): + context = op_fixture("mysql") + op.alter_column( + "t", + "c", + existing_type=Boolean(), + schema="foo", + comment=None, + existing_comment="This is a column comment", + ) + + context.assert_("ALTER TABLE foo.t MODIFY c BOOL NULL") + + @config.requirements.comments_api + def test_alter_column_existing_comment(self): + context = op_fixture("mysql") + op.alter_column( + "t1", + "c1", + nullable=False, + existing_comment="existing column comment", + existing_type=Integer, + ) + + context.assert_( + "ALTER TABLE t1 MODIFY c1 INTEGER NOT NULL " + "COMMENT 'existing column comment'" + ) + + @config.requirements.comments_api + def test_alter_column_new_comment_replaces_existing(self): + context = op_fixture("mysql") + op.alter_column( + "t1", + "c1", + nullable=False, + comment="This is a column comment", + existing_comment="existing column comment", + existing_type=Integer, + ) + + context.assert_( + "ALTER TABLE t1 MODIFY c1 INTEGER NOT NULL " + "COMMENT 'This is a column comment'" + ) + + @config.requirements.comments_api + def test_create_table_comment(self): + # this is handled by SQLAlchemy's compilers + context = op_fixture("mysql") + op.create_table_comment("t2", comment="t2 table", schema="foo") + context.assert_("ALTER TABLE foo.t2 COMMENT 't2 table'") + + @config.requirements.comments_api + @config.requirements.sqlalchemy_1216 + def test_drop_table_comment(self): + # this is handled by SQLAlchemy's compilers + context = op_fixture("mysql") + op.drop_table_comment("t2", existing_comment="t2 table", schema="foo") + context.assert_("ALTER TABLE foo.t2 COMMENT ''") + def test_drop_fk(self): context = op_fixture("mysql") op.drop_constraint("f1", "t1", "foreignkey") diff --git a/tests/test_op.py b/tests/test_op.py index ce0686cf..8a3e32fb 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -896,6 +896,22 @@ class OpTest(TestBase): op.drop_index("ik_test", tablename="t1") context.assert_("DROP INDEX ik_test ON t1") + @config.requirements.comments + def test_create_table_comment_op(self): + context = op_fixture() + + op.create_table_comment("some_table", "table comment") + + context.assert_("COMMENT ON TABLE some_table IS 'table comment'") + + @config.requirements.comments + def test_drop_table_comment_op(self): + context = op_fixture() + + op.drop_table_comment("some_table") + + context.assert_("COMMENT ON TABLE some_table IS NULL") + class SQLModeOpTest(TestBase): def test_auto_literals(self): diff --git a/tests/test_oracle.py b/tests/test_oracle.py index a28e9bec..15ed57e3 100644 --- a/tests/test_oracle.py +++ b/tests/test_oracle.py @@ -3,6 +3,7 @@ from sqlalchemy import Integer from alembic import command from alembic import op +from alembic.testing import config from alembic.testing.env import _no_sql_testing_config from alembic.testing.env import clear_staging_env from alembic.testing.env import staging_env @@ -55,6 +56,17 @@ class OpTest(TestBase): ) context.assert_("ALTER TABLE t1 ADD c1 INTEGER DEFAULT '12' NOT NULL") + @config.requirements.comments + def test_add_column_with_comment(self): + context = op_fixture("oracle") + op.add_column( + "t1", Column("c1", Integer, nullable=False, comment="c1 comment") + ) + context.assert_( + "ALTER TABLE t1 ADD c1 INTEGER NOT NULL", + "COMMENT ON COLUMN t1.c1 IS 'c1 comment'", + ) + def test_alter_column_rename_oracle(self): context = op_fixture("oracle") op.alter_column("t", "c", name="x") @@ -65,6 +77,56 @@ class OpTest(TestBase): op.alter_column("t", "c", type_=Integer) context.assert_("ALTER TABLE t MODIFY c INTEGER") + def test_alter_column_add_comment(self): + context = op_fixture("oracle") + op.alter_column("t", "c", type_=Integer, comment="c comment") + context.assert_( + "ALTER TABLE t MODIFY c INTEGER", + "COMMENT ON COLUMN t.c IS 'c comment'", + ) + + def test_alter_column_add_comment_quotes(self): + context = op_fixture("oracle") + op.alter_column("t", "c", type_=Integer, comment="c 'comment'") + context.assert_( + "ALTER TABLE t MODIFY c INTEGER", + "COMMENT ON COLUMN t.c IS 'c ''comment'''", + ) + + def test_alter_column_drop_comment(self): + context = op_fixture("oracle") + op.alter_column("t", "c", type_=Integer, comment=None) + context.assert_( + "ALTER TABLE t MODIFY c INTEGER", "COMMENT ON COLUMN t.c IS ''" + ) + + @config.requirements.comments_api + def test_create_table_comment(self): + # this is handled by SQLAlchemy's compilers + context = op_fixture("oracle") + op.create_table_comment( + 't2', + comment='t2 table', + schema='foo' + ) + context.assert_( + "COMMENT ON TABLE foo.t2 IS 't2 table'" + ) + + @config.requirements.comments_api + @config.requirements.sqlalchemy_1216 + def test_drop_table_comment(self): + # this is handled by SQLAlchemy's compilers + context = op_fixture("oracle") + op.drop_table_comment( + 't2', + existing_comment='t2 table', + schema='foo' + ) + context.assert_( + "COMMENT ON TABLE foo.t2 IS ''" + ) + def test_drop_index(self): context = op_fixture("oracle") op.drop_index("my_idx", "my_table") @@ -138,6 +200,17 @@ class OpTest(TestBase): "ALTER TABLE t RENAME COLUMN c TO c2", ) + @config.requirements.comments + def test_create_table_with_column_comments(self): + context = op_fixture("oracle") + op.create_table( + "t2", Column("c1", Integer, primary_key=True), comment="t2 comment" + ) + context.assert_( + "CREATE TABLE t2 (c1 INTEGER NOT NULL, PRIMARY KEY (c1))", + "COMMENT ON TABLE t2 IS 't2 comment'", + ) + # TODO: when we add schema support # def test_alter_column_rename_oracle_schema(self): # context = op_fixture('oracle') diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index 5ae261ba..77e3a20b 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -159,6 +159,132 @@ class PostgresqlOpTest(TestBase): 'USING gist ("SomeColumn" WITH >) WHERE ("SomeColumn" > 5)' ) + @config.requirements.comments_api + def test_add_column_with_comment(self): + context = op_fixture("postgresql") + op.add_column("t", Column("q", Integer, comment="This is a comment")) + context.assert_( + "ALTER TABLE t ADD COLUMN q INTEGER", + "COMMENT ON COLUMN t.q IS 'This is a comment'", + ) + + @config.requirements.comments_api + def test_alter_column_with_comment(self): + context = op_fixture("postgresql") + op.alter_column( + "t", + "c", + nullable=False, + existing_type=Boolean(), + schema="foo", + comment="This is a column comment", + ) + + context.assert_( + "ALTER TABLE foo.t ALTER COLUMN c SET NOT NULL", + "COMMENT ON COLUMN t.c IS 'This is a column comment'", + ) + + @config.requirements.comments_api + def test_alter_column_add_comment(self): + context = op_fixture("postgresql") + op.alter_column( + "t", + "c", + existing_type=Boolean(), + schema="foo", + comment="This is a column comment", + ) + + context.assert_("COMMENT ON COLUMN t.c IS 'This is a column comment'") + + @config.requirements.comments_api + def test_alter_column_add_comment_quoting(self): + context = op_fixture("postgresql") + op.alter_column( + "t", + "c", + existing_type=Boolean(), + schema="foo", + comment="This is a column 'comment'", + ) + + context.assert_( + "COMMENT ON COLUMN t.c IS 'This is a column ''comment'''" + ) + + @config.requirements.comments_api + def test_alter_column_drop_comment(self): + context = op_fixture("postgresql") + op.alter_column( + "t", + "c", + existing_type=Boolean(), + schema="foo", + comment=None, + existing_comment="This is a column comment", + ) + + context.assert_("COMMENT ON COLUMN t.c IS NULL") + + @config.requirements.comments_api + def test_create_table_with_comment(self): + context = op_fixture("postgresql") + op.create_table( + "t2", + Column("c1", Integer, primary_key=True), + Column("c2", Integer), + comment="t2 comment", + ) + context.assert_( + "CREATE TABLE t2 (c1 SERIAL NOT NULL, " + "c2 INTEGER, PRIMARY KEY (c1))", + "COMMENT ON TABLE t2 IS 't2 comment'", + ) + + @config.requirements.comments_api + def test_create_table_with_column_comments(self): + context = op_fixture("postgresql") + op.create_table( + "t2", + Column("c1", Integer, primary_key=True, comment="c1 comment"), + Column("c2", Integer, comment="c2 comment"), + comment="t2 comment", + ) + context.assert_( + "CREATE TABLE t2 (c1 SERIAL NOT NULL, " + "c2 INTEGER, PRIMARY KEY (c1))", + "COMMENT ON TABLE t2 IS 't2 comment'", + "COMMENT ON COLUMN t2.c1 IS 'c1 comment'", + "COMMENT ON COLUMN t2.c2 IS 'c2 comment'", + ) + + @config.requirements.comments_api + def test_create_table_comment(self): + # this is handled by SQLAlchemy's compilers + context = op_fixture("postgresql") + op.create_table_comment( + 't2', + comment='t2 table', + schema='foo' + ) + context.assert_( + "COMMENT ON TABLE foo.t2 IS 't2 table'" + ) + + @config.requirements.comments_api + def test_drop_table_comment(self): + # this is handled by SQLAlchemy's compilers + context = op_fixture("postgresql") + op.drop_table_comment( + 't2', + existing_comment='t2 table', + schema='foo' + ) + context.assert_( + "COMMENT ON TABLE foo.t2 IS NULL" + ) + class PGOfflineEnumTest(TestBase): def setUp(self): diff --git a/tests/test_revision.py b/tests/test_revision.py index d1708954..20eb309a 100644 --- a/tests/test_revision.py +++ b/tests/test_revision.py @@ -190,6 +190,7 @@ class LabeledBranchTest(DownIterateTest): Revision("e", ("d",), branch_labels=["xy1"]), Revision("f", ("e",)), ] + assert_raises_message( RevisionError, r"Branch name 'xy1' in revision (?:e|c) already " @@ -208,6 +209,7 @@ class LabeledBranchTest(DownIterateTest): Revision("c2", ("b",)), Revision("d", ("c1", "c2")), ] + map_ = RevisionMap(fn) c1 = map_.get_revision("c1") c2 = map_.get_revision("c2") diff --git a/tests/test_sqlite.py b/tests/test_sqlite.py index 110c65e5..dd81a13a 100644 --- a/tests/test_sqlite.py +++ b/tests/test_sqlite.py @@ -5,6 +5,7 @@ from sqlalchemy.sql import column from alembic import op from alembic.testing import assert_raises_message +from alembic.testing import config from alembic.testing.fixtures import op_fixture from alembic.testing.fixtures import TestBase @@ -40,3 +41,25 @@ class SQLiteTest(TestBase): "foo", "sometable", ) + + @config.requirements.comments + def test_create_table_with_comment_ignored(self): + + context = op_fixture("sqlite") + op.create_table( + "t2", + Column("c1", Integer, primary_key=True), + Column("c2", Integer), + comment="This is a table comment", + ) + context.assert_( + "CREATE TABLE t2 (c1 INTEGER NOT NULL, " + "c2 INTEGER, PRIMARY KEY (c1))" + ) + + @config.requirements.comments + def test_add_column_with_comment_ignored(self): + + context = op_fixture("sqlite") + op.add_column("t1", Column("c1", Integer, comment="c1 comment")) + context.assert_("ALTER TABLE t1 ADD COLUMN c1 INTEGER")