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.
Describe the bug
When a
ForeignKeyFielddeclaressource_field=...to control the FK column name,makemigrationsignores the declared value and writes the default<field_name>_idinto 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 withgenerate_schemas()getsaccount_id; the same models migrated getconsumer_id. Neither path errors.Meta.indexesandMeta.unique_togetherare not affected. They reference field names, which round-trip correctly.To Reproduce
repro/models.py:conf.py:Expected behavior
The generated migration should carry the declared
source_field='account_id', matching whatgenerate_schemas()produces.Actual behavior
repro/migrations/0001_initial.py:source_field='consumer_id'— the declared'account_id'is gone.For contrast,
generate_schemas()on the same models is correct:Root cause
After
_init_relations, the declared value has been moved off the relation field onto the backing concrete field, and the relation field'ssource_fieldhas been overwritten with the generated backing-field name:So on a
ForeignKeyFieldInstance,source_fieldno longer means "the DB column name" — it means "the name of my backing field".tortoise/migrations/writer.py::_format_create_modelrelies on that to deduplicate the backing field:but
_render_fieldthen emits that same attribute as if it were the column name. The real column name isfields_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_fieldon aForeignKeyField. Name the Python field so that<name>_idis the column you want (account = fields.ForeignKeyField(...)givesaccount_id). The generated migration is then correct.Environment
Possibly related
db_defaultis dropped during_init_relations(CREATE TABLE omits DEFAULT on FK columns) #2199 / Fixdb_defaulton FK/O2O dropped during_init_relations#2200 —db_defaultdropped on FK/O2O during_init_relations. Same family of bug (a declared FK attribute lost in_init_relations), different attribute, so the fix there does not cover this.source_fieldnot respected on FKs from an inherited mixin. Different root cause (shared field instances across subclasses) and not migration-related.Edit: an earlier version of this report said
Meta.indexesandMeta.unique_togetherwere 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.