From 6f8d9af59aa431aca3be851066c729c52b12a7a2 Mon Sep 17 00:00:00 2001 From: Jan Katins Date: Mon, 6 Mar 2023 16:18:17 -0500 Subject: improve autogen rendering for PG ExcludeConstraint Fixed issue regarding PostgreSQL :class:`.ExcludeConstraint`, where constraint elements which made use of :func:`.literal_column` could not be rendered for autogenerate. Additionally, using SQLAlchemy 2.0.5 or greater, :func:`.text()` constructs are also supported within PostgreSQL :class:`.ExcludeConstraint` objects for autogenerate render. Pull request courtesy Jan Katins. Fixes: #1184 Closes: #1185 Pull-request: https://github.com/sqlalchemy/alembic/pull/1185 Pull-request-sha: 68360ce9aa746a85407c5e1e04b8021123d98504 Change-Id: I302f3f4007a186ffac13b344ff6769dd302f28f5 --- alembic/ddl/postgresql.py | 15 +++++++---- docs/build/unreleased/1184.rst | 10 +++++++ tests/test_postgresql.py | 59 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 docs/build/unreleased/1184.rst diff --git a/alembic/ddl/postgresql.py b/alembic/ddl/postgresql.py index 994d7cb..4ffc2eb 100644 --- a/alembic/ddl/postgresql.py +++ b/alembic/ddl/postgresql.py @@ -22,6 +22,7 @@ from sqlalchemy.dialects.postgresql import ExcludeConstraint from sqlalchemy.dialects.postgresql import INTEGER from sqlalchemy.schema import CreateIndex from sqlalchemy.sql.elements import ColumnClause +from sqlalchemy.sql.elements import TextClause from sqlalchemy.types import NULLTYPE from .base import alter_column @@ -650,7 +651,7 @@ def _exclude_constraint( args = [ "(%s, %r)" % (_render_potential_column(sqltext, autogen_context), opstring) - for sqltext, name, opstring in constraint._render_exprs # type:ignore[attr-defined] # noqa + for sqltext, name, opstring in constraint._render_exprs ] if constraint.where is not None: args.append( @@ -667,17 +668,21 @@ def _exclude_constraint( def _render_potential_column( - value: Union[ColumnClause, Column], autogen_context: AutogenContext + value: Union[ColumnClause, Column, TextClause], + autogen_context: AutogenContext, ) -> str: if isinstance(value, ColumnClause): - template = "%(prefix)scolumn(%(name)r)" + if value.is_literal: + # like literal_column("int8range(from, to)") in ExcludeConstraint + template = "%(prefix)sliteral_column(%(name)r)" + else: + template = "%(prefix)scolumn(%(name)r)" return template % { "prefix": render._sqlalchemy_autogenerate_prefix(autogen_context), "name": value.name, } - else: return render._render_potential_expr( - value, autogen_context, wrap_in_text=False + value, autogen_context, wrap_in_text=isinstance(value, TextClause) ) diff --git a/docs/build/unreleased/1184.rst b/docs/build/unreleased/1184.rst new file mode 100644 index 0000000..3d26f3e --- /dev/null +++ b/docs/build/unreleased/1184.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, postgresql + :tickets: 1184 + + Fixed issue regarding PostgreSQL :class:`.ExcludeConstraint`, where + constraint elements which made use of :func:`.literal_column` could not be + rendered for autogenerate. Additionally, using SQLAlchemy 2.0.5 or greater, + :func:`.text()` constructs are also supported within PostgreSQL + :class:`.ExcludeConstraint` objects for autogenerate render. Pull request + courtesy Jan Katins. \ No newline at end of file diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index a8c284d..18f50ce 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -28,6 +28,7 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.sql import column from sqlalchemy.sql import false from sqlalchemy.sql import table +from sqlalchemy.sql.expression import literal_column from alembic import autogenerate from alembic import command @@ -1156,6 +1157,64 @@ class PostgresqlAutogenRenderTest(TestBase): "name='TExclX'))", ) + def test_inline_exclude_constraint_literal_column(self): + """test for #1184""" + + autogen_context = self.autogen_context + + m = MetaData() + t = Table( + "TTable", + m, + Column("id", String()), + ExcludeConstraint( + (literal_column("id + 2"), "="), name="TExclID", using="gist" + ), + ) + + op_obj = ops.CreateTableOp.from_table(t) + + eq_ignore_whitespace( + autogenerate.render_op_text(autogen_context, op_obj), + "op.create_table('TTable',sa.Column('id', sa.String(), " + "nullable=True)," + "postgresql.ExcludeConstraint((sa.literal_column('id + 2'), '='), " + "using='gist', " + "name='TExclID'))", + ) + + @config.requirements.sqlalchemy_2 + def test_inline_exclude_constraint_text(self): + """test for #1184. + + Requires SQLAlchemy 2.0.5 due to issue + https://github.com/sqlalchemy/sqlalchemy/issues/9401 + + """ + + autogen_context = self.autogen_context + + m = MetaData() + t = Table( + "TTable", + m, + Column("id", String()), + ExcludeConstraint( + (text("id + 2"), "="), name="TExclID", using="gist" + ), + ) + + op_obj = ops.CreateTableOp.from_table(t) + + eq_ignore_whitespace( + autogenerate.render_op_text(autogen_context, op_obj), + "op.create_table('TTable',sa.Column('id', sa.String(), " + "nullable=True)," + "postgresql.ExcludeConstraint((sa.text('id + 2'), '='), " + "using='gist', " + "name='TExclID'))", + ) + def test_json_type(self): eq_ignore_whitespace( autogenerate.render._repr_type(JSON(), self.autogen_context), -- cgit v1.2.1