Skip to content

Commit cd4f404

Browse files
david-fedzzzeek
authored andcommitted
Fixed rendering of index expressions in MySQL
Fixed Python-side autogenerate rendering of index expressions in MySQL dialect by aligning it with SQLAlchemy's MySQL index expression rules. Pull request courtesy david-fed. Fixes: #1492 Closes: #1695 Pull-request: #1695 Pull-request-sha: 8f9ed8f Change-Id: I3b838b4b7a44e3d5a279ba30624c1552f99959d7
1 parent a9f860b commit cd4f404

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

alembic/ddl/mysql.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
from sqlalchemy import schema
1313
from sqlalchemy import types as sqltypes
14+
from sqlalchemy.sql import elements
15+
from sqlalchemy.sql import functions
16+
from sqlalchemy.sql import operators
1417

1518
from .base import alter_table
1619
from .base import AlterColumn
@@ -31,6 +34,7 @@
3134

3235
from sqlalchemy.dialects.mysql.base import MySQLDDLCompiler
3336
from sqlalchemy.sql.ddl import DropConstraint
37+
from sqlalchemy.sql.elements import ClauseElement
3438
from sqlalchemy.sql.schema import Constraint
3539
from sqlalchemy.sql.type_api import TypeEngine
3640

@@ -47,6 +51,31 @@ class MySQLImpl(DefaultImpl):
4751
)
4852
type_arg_extract = [r"character set ([\w\-_]+)", r"collate ([\w\-_]+)"]
4953

54+
def render_ddl_sql_expr(
55+
self,
56+
expr: ClauseElement,
57+
is_server_default: bool = False,
58+
is_index: bool = False,
59+
**kw: Any,
60+
) -> str:
61+
# apply Grouping to index expressions;
62+
# see https://github.com/sqlalchemy/sqlalchemy/blob/
63+
# 36da2eaf3e23269f2cf28420ae73674beafd0661/
64+
# lib/sqlalchemy/dialects/mysql/base.py#L2191
65+
if is_index and (
66+
isinstance(expr, elements.BinaryExpression)
67+
or (
68+
isinstance(expr, elements.UnaryExpression)
69+
and expr.modifier not in (operators.desc_op, operators.asc_op)
70+
)
71+
or isinstance(expr, functions.FunctionElement)
72+
):
73+
expr = elements.Grouping(expr)
74+
75+
return super().render_ddl_sql_expr(
76+
expr, is_server_default=is_server_default, is_index=is_index, **kw
77+
)
78+
5079
def alter_column(
5180
self,
5281
table_name: str,

docs/build/unreleased/1492.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.. change::
2+
:tags: bug, mysql
3+
:tickets: 1492
4+
5+
Fixed Python-side autogenerate rendering of index expressions in MySQL
6+
dialect by aligning it with SQLAlchemy's MySQL index expression rules. Pull
7+
request courtesy david-fed.

tests/test_mysql.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sqlalchemy import Float
77
from sqlalchemy import func
88
from sqlalchemy import Identity
9+
from sqlalchemy import Index
910
from sqlalchemy import inspect
1011
from sqlalchemy import Integer
1112
from sqlalchemy import MetaData
@@ -15,6 +16,7 @@
1516
from sqlalchemy import TIMESTAMP
1617
from sqlalchemy.dialects.mysql import VARCHAR
1718

19+
from alembic import autogenerate
1820
from alembic import op
1921
from alembic import util
2022
from alembic.autogenerate import api
@@ -24,6 +26,7 @@
2426
from alembic.testing import assert_raises_message
2527
from alembic.testing import combinations
2628
from alembic.testing import config
29+
from alembic.testing import eq_ignore_whitespace
2730
from alembic.testing.env import clear_staging_env
2831
from alembic.testing.env import staging_env
2932
from alembic.testing.fixtures import AlterColRoundTripFixture
@@ -692,3 +695,79 @@ def test_compare_boolean_same(self):
692695

693696
def test_compare_boolean_diff(self):
694697
self._compare_default_roundtrip(Boolean(), "1", "0")
698+
699+
700+
class MySQLAutogenRenderTest(TestBase):
701+
def setUp(self):
702+
ctx_opts = {
703+
"sqlalchemy_module_prefix": "sa.",
704+
"alembic_module_prefix": "op.",
705+
"target_metadata": MetaData(),
706+
}
707+
context = MigrationContext.configure(
708+
dialect_name="mysql", opts=ctx_opts
709+
)
710+
711+
self.autogen_context = api.AutogenContext(context)
712+
713+
def test_render_add_index_expr_binary(self):
714+
m = MetaData()
715+
t = Table(
716+
"t",
717+
m,
718+
Column("x", Integer, primary_key=True),
719+
Column("y", Integer),
720+
)
721+
idx = Index("foo_idx", t.c.x > 5)
722+
723+
eq_ignore_whitespace(
724+
autogenerate.render_op_text(
725+
self.autogen_context, ops.CreateIndexOp.from_index(idx)
726+
),
727+
"op.create_index('foo_idx', 't', "
728+
"[sa.literal_column('(x > 5)')], unique=False)",
729+
)
730+
731+
def test_render_add_index_expr_unary(self):
732+
m = MetaData()
733+
t = Table(
734+
"t",
735+
m,
736+
Column("x", Integer, primary_key=True),
737+
Column("y", Integer),
738+
)
739+
idx1 = Index("foo_idx", -t.c.x)
740+
idx2 = Index("foo_idx", t.c.x.desc())
741+
742+
eq_ignore_whitespace(
743+
autogenerate.render_op_text(
744+
self.autogen_context, ops.CreateIndexOp.from_index(idx1)
745+
),
746+
"op.create_index('foo_idx', 't', "
747+
"[sa.literal_column('(-x)')], unique=False)",
748+
)
749+
eq_ignore_whitespace(
750+
autogenerate.render_op_text(
751+
self.autogen_context, ops.CreateIndexOp.from_index(idx2)
752+
),
753+
"op.create_index('foo_idx', 't', "
754+
"[sa.literal_column('x DESC')], unique=False)",
755+
)
756+
757+
def test_render_add_index_expr_func(self):
758+
m = MetaData()
759+
t = Table(
760+
"t",
761+
m,
762+
Column("x", Integer, primary_key=True),
763+
Column("y", Integer, nullable=True),
764+
)
765+
idx = Index("foo_idx", t.c.x, func.coalesce(t.c.y, 0))
766+
767+
eq_ignore_whitespace(
768+
autogenerate.render_op_text(
769+
self.autogen_context, ops.CreateIndexOp.from_index(idx)
770+
),
771+
"op.create_index('foo_idx', 't', "
772+
"['x', sa.literal_column('(coalesce(y, 0))')], unique=False)",
773+
)

0 commit comments

Comments
 (0)