]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Implemented support for Table and Column Comments
authorMike Waites <mikey.waites@gmail.com>
Fri, 31 Aug 2018 18:56:36 +0000 (19:56 +0100)
committermike bayer <mike_mp@zzzcomputing.com>
Thu, 10 Jan 2019 02:09:03 +0000 (02:09 +0000)
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

24 files changed:
alembic/autogenerate/compare.py
alembic/autogenerate/render.py
alembic/ddl/base.py
alembic/ddl/impl.py
alembic/ddl/mysql.py
alembic/ddl/oracle.py
alembic/ddl/postgresql.py
alembic/operations/__init__.py
alembic/operations/ops.py
alembic/operations/toimpl.py
alembic/testing/requirements.py
alembic/util/__init__.py
alembic/util/sqla_compat.py
docs/build/unreleased/422.rst [new file with mode: 0644]
tests/test_autogen_comments.py [new file with mode: 0644]
tests/test_autogen_composition.py
tests/test_autogen_render.py
tests/test_batch.py
tests/test_mysql.py
tests/test_op.py
tests/test_oracle.py
tests/test_postgresql.py
tests/test_revision.py
tests/test_sqlite.py

index 1bea25528406f335efd9144fea1f98e5c39192d0..df1e5096d3a3addf67ba1cd4b952cf5d71e22072 100644 (file)
@@ -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,
+            )
+        )
index 3b6e9353efd786ab3b4f7411e081058b150214d5..64dbaebf00a579fc4bdf8c5a008d5505bcaec4ee 100644 (file)
@@ -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),
index b0de13650f71a8bb8e6826bd5ea134982c58fbc2..9bdb0bef37bcde5ee18137ebbb3f0d165ba16175 100644 (file)
@@ -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" % (
index dd1be58bd90744512891d235bc7276814f0149e5..5df7c040d0123acfb9554835f722336c4906143c 100644 (file)
@@ -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))
 
index 29f416e638ad57974cebd4d51bc2ba7d50373ec7..d20aec6845deb765c2f2a9e0d89d66e93d2e805a 100644 (file)
@@ -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
 
index 4bd92e3a379c057df8abbd0367a5645675034189..76cf7c5db133d0807e719c47937366fd5ab4a410 100644 (file)
@@ -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)
 
index d5d8e6e3d107da47b484ccb98d2cd6029c82600e..a8e332ec911f8df39d79da19c451ba59bf8004e0 100644 (file)
@@ -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"
index 3f8f0bd9905c599bdef005e76c21bd233d462ec9..dc2d3a492eeedf052dc111db70ea1936ae0b5867 100644 (file)
@@ -4,5 +4,4 @@ from .base import Operations
 from .ops import MigrateOperation
 
 
-
 __all__ = ["Operations", "BatchOperations", "MigrateOperation"]
index 1b104f0989f70cea453b870d82f97c7e8d52802d..c85a25d5ff2b03736eadd8252d4288cd8dba8cdb 100644 (file)
@@ -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
         )
 
index ad8d7a239171d1a36573b60a3f26561517c42b39..569942354691d16edad0fbe64f6b95ba471e0dce 100644 (file)
@@ -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):
index 8c02254d952cdc68317c71d227eda4ccf6ec6487..90b51ba998214c8ae2bb2a49209e9eccbeb546f1 100644 (file)
@@ -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)
index 18d25cdfa8aef56b5141c06233ae6ed5aba8dd86..88b743195aad9b62dfa656f6e4ebd0a2c5ced186 100644 (file)
@@ -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:
index c20f318e73375e0180c6b5deccd12e497f92d0ca..82250d00f970195ece8d3f3f03f3e284188f853a 100644 (file)
@@ -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 (file)
index 0000000..d9d1ce0
--- /dev/null
@@ -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 (file)
index 0000000..97d1df6
--- /dev/null
@@ -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",
+                    )
+                ]
+            ],
+        )
index 6c31d52e6a08fed9bf3f773974aa54e66e6b2794..d6664b2adf80c0b916740e58d1a3d2f50172baea 100644 (file)
@@ -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):
index 6465b34a71fb2b1acde7e1126f3b732dec61be96..7673a493088c70e120d771375d14d58c7a391102 100644 (file)
@@ -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",)
index 31e23c5ba6764704d14dbf9fba61d268e474952c..aa998539fd7dd56ec8fd2eca19029831f4f383f5 100644 (file)
@@ -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):
index 7158358f2b12b9feb794e1d0e3eca08762c3692a..ba0abb6b14e89166d31f3308f9ec1e3bfc05b1a2 100644 (file)
@@ -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")
index ce0686cfdb50e5d833c55f64fe359adcfd9c80f6..8a3e32fbca935d3b779ad37acdcbe058668c9a2b 100644 (file)
@@ -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):
index a28e9becbba23a52512aa7c00f75a3137478e1ba..15ed57e36b2bf025bc42b8f2e4be3420358cdf39 100644 (file)
@@ -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')
index 5ae261baf02962d7c280601f531766a6d48568ae..77e3a20b7c17f66609e25b930bc3422cdda52e52 100644 (file)
@@ -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):
index d170895487a3a32ded4e92145e83024cf6940334..20eb309a602dc537f39b695670de13d2505c46e0 100644 (file)
@@ -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")
index 110c65e591d211eb37208e915cd63122ae31af69..dd81a13a343828241cce1afa1566fc3f3bdd5d90 100644 (file)
@@ -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")