Skip to content

makemigrations writes the wrong source_field for ForeignKeyField (value is overwritten during _init_relations) #2283

Description

@aksgupta98

Describe the bug

When a ForeignKeyField declares source_field=... to control the FK column name, makemigrations ignores the declared value and writes the default <field_name>_id into the migration file instead.

The result is that generate_schemas() and migrations produce different column names from the same models. A developer who creates their database with generate_schemas() gets account_id; the same models migrated get consumer_id. Neither path errors.

Meta.indexes and Meta.unique_together are not affected. They reference field names, which round-trip correctly.

To Reproduce

repro/models.py:

from tortoise import fields
from tortoise.models import Model


class Parent(Model):
    account_id = fields.CharField(max_length=15, primary_key=True)

    class Meta:
        table = "parent"


class Child(Model):
    id = fields.IntField(primary_key=True)
    # Ask for the FK column to be named "account_id" instead of the default "consumer_id".
    consumer = fields.ForeignKeyField(
        "models.Parent", related_name="children", source_field="account_id"
    )

    class Meta:
        table = "child"
        indexes = (("consumer_id",),)

conf.py:

TORTOISE_ORM = {
    "connections": {"default": "sqlite://repro.sqlite3"},
    "apps": {"models": {"models": ["repro.models"], "default_connection": "default"}},
}
python -m tortoise -c conf.TORTOISE_ORM init
python -m tortoise -c conf.TORTOISE_ORM makemigrations

Expected behavior

The generated migration should carry the declared source_field='account_id', matching what generate_schemas() produces.

Actual behavior

repro/migrations/0001_initial.py:

ops.CreateModel(
    name='Child',
    fields=[
        ('id', fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
        ('consumer', fields.ForeignKeyField('models.Parent', source_field='consumer_id', db_constraint=True, to_field='account_id', related_name='children', on_delete=OnDelete.CASCADE)),
    ],
    options={'table': 'child', 'app': 'models', 'indexes': [Index(fields=['consumer_id'])], 'pk_attr': 'id'},
    bases=['Model'],
)

source_field='consumer_id' — the declared 'account_id' is gone.

For contrast, generate_schemas() on the same models is correct:

>>> [r["name"] for r in await conn.execute_query_dict("PRAGMA table_info(child)")]
['id', 'account_id']

Root cause

After _init_relations, the declared value has been moved off the relation field onto the backing concrete field, and the relation field's source_field has been overwritten with the generated backing-field name:

>>> Tortoise.init_models(["repro.models"], "models")
>>> for name, f in Child._meta.fields_map.items():
...     print(name, type(f).__name__, repr(getattr(f, "source_field", None)))
id           IntField                 None
consumer     ForeignKeyFieldInstance  'consumer_id'   # <- overwritten, was 'account_id'
consumer_id  CharField                'account_id'    # <- the declared value lives here
>>> Child._meta.db_fields
{'id', 'account_id'}

So on a ForeignKeyFieldInstance, source_field no longer means "the DB column name" — it means "the name of my backing field". tortoise/migrations/writer.py::_format_create_model relies on that to deduplicate the backing field:

source_fields = {
    field.source_field
    for _, field in operation.fields
    if field is not None and hasattr(field, "source_field") and field.source_field
}
...
if name in source_fields:
    continue

but _render_field then emits that same attribute as if it were the column name. The real column name is fields_map[f"{name}_id"].source_field.

Why it matters

The two schema-creation paths silently disagree. A project that uses generate_schemas() in development and migrations in deployed environments ends up with different column names in each, with no error from either side. Any hand-written SQL, backfill or raw query then works in one environment and fails in the other.

Workaround

Do not use source_field on a ForeignKeyField. Name the Python field so that <name>_id is the column you want (account = fields.ForeignKeyField(...) gives account_id). The generated migration is then correct.

Environment

  • tortoise-orm 1.1.7 and 1.1.8 — reproduced on both, byte-identical output
  • Python 3.14
  • Backend: SQLite (repro) and asyncpg/PostgreSQL (original report); not backend-specific

Possibly related


Edit: an earlier version of this report said Meta.indexes and Meta.unique_together were also serialized with the wrong name. That was wrong, and I have corrected it above. Those options take field names, and the field name is what should be written, so they round-trip correctly. Only the FK column name is affected.

A fix is open as #2284.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions