column: Column[Any],
schema: Optional[Union[quoted_name, str]] = None,
if_not_exists: Optional[bool] = None,
+ inline_references: Optional[bool] = None,
) -> None:
super().__init__(name, schema=schema)
self.column = column
self.if_not_exists = if_not_exists
+ self.inline_references = inline_references
class DropColumn(AlterTable):
return "%s %s" % (
alter_table(compiler, element.table_name, element.schema),
add_column(
- compiler, element.column, if_not_exists=element.if_not_exists, **kw
+ compiler,
+ element.column,
+ if_not_exists=element.if_not_exists,
+ inline_references=element.inline_references,
+ **kw,
),
)
compiler: DDLCompiler,
column: Column[Any],
if_not_exists: Optional[bool] = None,
+ inline_references: Optional[bool] = None,
**kw,
) -> str:
text = "ADD COLUMN %s%s" % (
if column.primary_key:
text += " PRIMARY KEY"
+ # Handle inline REFERENCES if requested
+ # Only render inline if there's exactly one foreign key AND the
+ # ForeignKeyConstraint is single-column, to avoid non-deterministic
+ # behavior with sets and to ensure proper syntax
+ if (
+ inline_references
+ and len(column.foreign_keys) == 1
+ and (fk := list(column.foreign_keys)[0])
+ and fk.constraint is not None
+ and len(fk.constraint.columns) == 1
+ ):
+ ref_col = fk.column
+ ref_table = ref_col.table
+
+ # Format with proper quoting
+ if ref_table.schema:
+ table_name = "%s.%s" % (
+ compiler.preparer.quote_schema(ref_table.schema),
+ compiler.preparer.quote(ref_table.name),
+ )
+ else:
+ table_name = compiler.preparer.quote(ref_table.name)
+
+ text += " REFERENCES %s (%s)" % (
+ table_name,
+ compiler.preparer.quote(ref_col.name),
+ )
+
const = " ".join(
compiler.process(constraint) for constraint in column.constraints
)
*,
schema: Optional[Union[str, quoted_name]] = None,
if_not_exists: Optional[bool] = None,
+ inline_references: Optional[bool] = None,
) -> None:
self._exec(
base.AddColumn(
column,
schema=schema,
if_not_exists=if_not_exists,
+ inline_references=inline_references,
)
)
*,
schema: Optional[str] = None,
if_not_exists: Optional[bool] = None,
+ inline_references: Optional[bool] = None,
) -> None:
"""Issue an "add column" instruction using the current
migration context.
The provided :class:`~sqlalchemy.schema.Column` object may include a
:class:`~sqlalchemy.schema.ForeignKey` constraint directive,
- referencing a remote table name. For this specific type of constraint,
- Alembic will automatically emit a second ALTER statement in order to
- add the single-column FOREIGN KEY constraint separately::
+ referencing a remote table name. By default, Alembic will automatically
+ emit a second ALTER statement in order to add the single-column FOREIGN
+ KEY constraint separately::
from alembic import op
from sqlalchemy import Column, INTEGER, ForeignKey
Column("account_id", INTEGER, ForeignKey("accounts.id")),
)
+ To render the FOREIGN KEY constraint inline within the ADD COLUMN
+ directive, use the ``inline_references`` parameter. This can improve
+ performance on large tables since the constraint is marked as valid
+ immediately for nullable columns::
+
+ from alembic import op
+ from sqlalchemy import Column, INTEGER, ForeignKey
+
+ op.add_column(
+ "organization",
+ Column("account_id", INTEGER, ForeignKey("accounts.id")),
+ inline_references=True,
+ )
+
The column argument passed to :meth:`.Operations.add_column` is a
:class:`~sqlalchemy.schema.Column` construct, used in the same way it's
used in SQLAlchemy. In particular, values or functions to be indicated
.. versionadded:: 1.16.0
+ :param inline_references: If True, renders FOREIGN KEY constraints
+ inline within the ADD COLUMN directive using REFERENCES syntax,
+ rather than as a separate ALTER TABLE ADD CONSTRAINT statement.
+ This is supported by PostgreSQL, Oracle, MySQL 5.7+, and
+ MariaDB 10.5+.
+
+ .. versionadded:: 1.18.2
+
"""
def alter_column(
*,
schema: Optional[str] = None,
if_not_exists: Optional[bool] = None,
+ inline_references: Optional[bool] = None,
) -> None:
"""Issue an "add column" instruction using the current
migration context.
The provided :class:`~sqlalchemy.schema.Column` object may include a
:class:`~sqlalchemy.schema.ForeignKey` constraint directive,
- referencing a remote table name. For this specific type of constraint,
- Alembic will automatically emit a second ALTER statement in order to
- add the single-column FOREIGN KEY constraint separately::
+ referencing a remote table name. By default, Alembic will automatically
+ emit a second ALTER statement in order to add the single-column FOREIGN
+ KEY constraint separately::
from alembic import op
from sqlalchemy import Column, INTEGER, ForeignKey
Column("account_id", INTEGER, ForeignKey("accounts.id")),
)
+ To render the FOREIGN KEY constraint inline within the ADD COLUMN
+ directive, use the ``inline_references`` parameter. This can improve
+ performance on large tables since the constraint is marked as valid
+ immediately for nullable columns::
+
+ from alembic import op
+ from sqlalchemy import Column, INTEGER, ForeignKey
+
+ op.add_column(
+ "organization",
+ Column("account_id", INTEGER, ForeignKey("accounts.id")),
+ inline_references=True,
+ )
+
The column argument passed to :meth:`.Operations.add_column` is a
:class:`~sqlalchemy.schema.Column` construct, used in the same way it's
used in SQLAlchemy. In particular, values or functions to be indicated
.. versionadded:: 1.16.0
+ :param inline_references: If True, renders FOREIGN KEY constraints
+ inline within the ADD COLUMN directive using REFERENCES syntax,
+ rather than as a separate ALTER TABLE ADD CONSTRAINT statement.
+ This is supported by PostgreSQL, Oracle, MySQL 5.7+, and
+ MariaDB 10.5+.
+
+ .. versionadded:: 1.18.2
+
""" # noqa: E501
...
insert_before: Optional[str] = None,
insert_after: Optional[str] = None,
if_not_exists: Optional[bool] = None,
+ inline_references: Optional[bool] = None,
) -> None:
"""Issue an "add column" instruction using the current
batch migration context.
*,
schema: Optional[str] = None,
if_not_exists: Optional[bool] = None,
+ inline_references: Optional[bool] = None,
**kw: Any,
) -> None:
super().__init__(table_name, schema=schema)
self.column = column
self.if_not_exists = if_not_exists
+ self.inline_references = inline_references
self.kw = kw
def reverse(self) -> DropColumnOp:
*,
schema: Optional[str] = None,
if_not_exists: Optional[bool] = None,
+ inline_references: Optional[bool] = None,
) -> None:
"""Issue an "add column" instruction using the current
migration context.
The provided :class:`~sqlalchemy.schema.Column` object may include a
:class:`~sqlalchemy.schema.ForeignKey` constraint directive,
- referencing a remote table name. For this specific type of constraint,
- Alembic will automatically emit a second ALTER statement in order to
- add the single-column FOREIGN KEY constraint separately::
+ referencing a remote table name. By default, Alembic will automatically
+ emit a second ALTER statement in order to add the single-column FOREIGN
+ KEY constraint separately::
from alembic import op
from sqlalchemy import Column, INTEGER, ForeignKey
Column("account_id", INTEGER, ForeignKey("accounts.id")),
)
+ To render the FOREIGN KEY constraint inline within the ADD COLUMN
+ directive, use the ``inline_references`` parameter. This can improve
+ performance on large tables since the constraint is marked as valid
+ immediately for nullable columns::
+
+ from alembic import op
+ from sqlalchemy import Column, INTEGER, ForeignKey
+
+ op.add_column(
+ "organization",
+ Column("account_id", INTEGER, ForeignKey("accounts.id")),
+ inline_references=True,
+ )
+
The column argument passed to :meth:`.Operations.add_column` is a
:class:`~sqlalchemy.schema.Column` construct, used in the same way it's
used in SQLAlchemy. In particular, values or functions to be indicated
.. versionadded:: 1.16.0
+ :param inline_references: If True, renders FOREIGN KEY constraints
+ inline within the ADD COLUMN directive using REFERENCES syntax,
+ rather than as a separate ALTER TABLE ADD CONSTRAINT statement.
+ This is supported by PostgreSQL, Oracle, MySQL 5.7+, and
+ MariaDB 10.5+.
+
+ .. versionadded:: 1.18.2
+
"""
op = cls(
column,
schema=schema,
if_not_exists=if_not_exists,
+ inline_references=inline_references,
)
return operations.invoke(op)
insert_before: Optional[str] = None,
insert_after: Optional[str] = None,
if_not_exists: Optional[bool] = None,
+ inline_references: Optional[bool] = None,
) -> None:
"""Issue an "add column" instruction using the current
batch migration context.
column,
schema=operations.impl.schema,
if_not_exists=if_not_exists,
+ inline_references=inline_references,
**kw,
)
return operations.invoke(op)
column = operation.column
schema = operation.schema
kw = operation.kw
+ inline_references = operation.inline_references
if column.table is not None:
column = _copy(column)
column,
schema=schema,
if_not_exists=operation.if_not_exists,
+ inline_references=inline_references,
**kw,
)
for constraint in t.constraints:
if not isinstance(constraint, sa_schema.PrimaryKeyConstraint):
+ # Skip ForeignKeyConstraint if it was rendered inline
+ # This only happens when inline_references=True AND there's exactly
+ # one FK AND the constraint is single-column
+ if (
+ inline_references
+ and isinstance(constraint, sa_schema.ForeignKeyConstraint)
+ and len(column.foreign_keys) == 1
+ and len(constraint.columns) == 1
+ ):
+ continue
operations.impl.add_constraint(constraint)
for index in t.indexes:
operations.impl.create_index(index)
--- /dev/null
+.. change::
+ :tags: usecase, operations
+ :tickets: 1780
+
+ Added ``inline_references`` parameter to :meth:`.Operations.add_column`
+ which allows rendering of ``REFERENCES`` clauses inline within the ``ADD
+ COLUMN`` directive rather than as a separate ``ADD CONSTRAINT`` directive.
+ This syntax is supported by PostgreSQL, Oracle, MySQL 5.7+, and MariaDB
+ 10.5+, and can provide performance benefits on large tables by avoiding
+ full table validation when adding a nullable foreign key column.
assert (
mock.call.add_column(
- "tname", column, schema=None, if_not_exists=None
+ "tname",
+ column,
+ schema=None,
+ if_not_exists=None,
+ inline_references=None,
)
in batch.impl.operations.impl.mock_calls
)
"ALTER TABLE t1 ADD FOREIGN KEY(c1) REFERENCES c2 (id)",
)
+ def test_add_column_fk_inline_references(self):
+ context = op_fixture()
+ op.add_column(
+ "t1",
+ Column("c1", Integer, ForeignKey("c2.id"), nullable=False),
+ inline_references=True,
+ )
+ context.assert_(
+ "ALTER TABLE t1 ADD COLUMN c1 INTEGER NOT NULL REFERENCES c2 (id)"
+ )
+
def test_add_column_schema_fk(self):
context = op_fixture()
op.add_column(
"ALTER TABLE foo.t1 ADD FOREIGN KEY(c1) REFERENCES c2 (id)",
)
+ def test_add_column_schema_fk_inline_references(self):
+ context = op_fixture()
+ op.add_column(
+ "t1",
+ Column("c1", Integer, ForeignKey("c2.id"), nullable=False),
+ schema="foo",
+ inline_references=True,
+ )
+ context.assert_(
+ "ALTER TABLE foo.t1 ADD COLUMN c1 INTEGER NOT NULL "
+ "REFERENCES c2 (id)"
+ )
+
def test_add_column_schema_type(self):
"""Test that a schema type generates its constraints...."""
context = op_fixture()
"ALTER TABLE t1 ADD FOREIGN KEY(c1) REFERENCES t1 (c2)",
)
+ def test_add_column_fk_self_referential_inline_references(self):
+ context = op_fixture()
+ op.add_column(
+ "t1",
+ Column("c1", Integer, ForeignKey("t1.c2"), nullable=False),
+ inline_references=True,
+ )
+ context.assert_(
+ "ALTER TABLE t1 ADD COLUMN c1 INTEGER NOT NULL REFERENCES t1 (c2)"
+ )
+
def test_add_column_schema_fk_self_referential(self):
context = op_fixture()
op.add_column(
"ALTER TABLE t1 ADD FOREIGN KEY(c1) REFERENCES remote.t2 (c2)",
)
+ def test_add_column_fk_schema_inline_references(self):
+ context = op_fixture()
+ op.add_column(
+ "t1",
+ Column("c1", Integer, ForeignKey("remote.t2.c2"), nullable=False),
+ inline_references=True,
+ )
+ context.assert_(
+ "ALTER TABLE t1 ADD COLUMN c1 INTEGER NOT NULL "
+ "REFERENCES remote.t2 (c2)"
+ )
+
+ @combinations(
+ ("MixedCase.id", 'REFERENCES "MixedCase" (id)'),
+ ("t2.MixedCase", 'REFERENCES t2 ("MixedCase")'),
+ (
+ "MixedCaseTable.MixedCaseColumn",
+ 'REFERENCES "MixedCaseTable" ("MixedCaseColumn")',
+ ),
+ ("MixedSchema.t2.id", 'REFERENCES "MixedSchema".t2 (id)'),
+ (
+ "MixedSchema.MixedTable.MixedCol",
+ 'REFERENCES "MixedSchema"."MixedTable" ("MixedCol")',
+ ),
+ argnames="fk_ref,expected_ref",
+ )
+ def test_add_column_fk_inline_references_quoting(
+ self, fk_ref, expected_ref
+ ):
+ context = op_fixture()
+ op.add_column(
+ "t1",
+ Column("c1", Integer, ForeignKey(fk_ref), nullable=False),
+ inline_references=True,
+ )
+ context.assert_(
+ f"ALTER TABLE t1 ADD COLUMN c1 INTEGER NOT NULL {expected_ref}"
+ )
+
+ def test_add_column_multiple_fk_inline_references(self):
+ """Test that inline_references is ignored when a column has multiple
+ ForeignKey objects to avoid non-deterministic behavior."""
+ context = op_fixture()
+ # A column with two ForeignKey objects
+ col = Column(
+ "c1",
+ Integer,
+ ForeignKey("t2.id"),
+ ForeignKey("t3.id"),
+ nullable=False,
+ )
+ op.add_column("t1", col, inline_references=True)
+
+ # Should NOT render inline REFERENCES because there are 2 FKs
+ # Falls back to separate ADD CONSTRAINT statements
+ # Order of FK constraints is non-deterministic due to set ordering
+ context.assert_contains(
+ "ALTER TABLE t1 ADD COLUMN c1 INTEGER NOT NULL"
+ )
+ context.assert_contains(
+ "ALTER TABLE t1 ADD FOREIGN KEY(c1) REFERENCES t2 (id)"
+ )
+ context.assert_contains(
+ "ALTER TABLE t1 ADD FOREIGN KEY(c1) REFERENCES t3 (id)"
+ )
+
def test_add_column_schema_fk_schema(self):
context = op_fixture()
op.add_column(