diff --git a/docs/models.rst b/docs/models.rst index 94284d778..c68ee06e3 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -201,7 +201,15 @@ The ``Meta`` class .. attribute:: table :annotation: = "" - Set this to configure a manual table name, instead of a generated one + Set this to configure a manual table name, instead of a generated one. + ``db_table`` is also supported as an alias. If both ``table`` and + ``db_table`` are set, they must have the same value. + + .. attribute:: db_table + :annotation: = "" + + Alias for ``table``. This is useful for users coming from Django, where + ``db_table`` is the standard option name. .. attribute:: table_description :annotation: = "" diff --git a/tests/test_table_name.py b/tests/test_table_name.py index 62ea9e24c..b97a18b41 100644 --- a/tests/test_table_name.py +++ b/tests/test_table_name.py @@ -3,6 +3,7 @@ from tortoise import fields from tortoise.context import TortoiseContext +from tortoise.exceptions import ConfigurationError from tortoise.models import Model @@ -11,19 +12,36 @@ def table_name_generator(model_cls: type[Model]): class Tournament(Model): - id = fields.IntField(pk=True) + id = fields.IntField(primary_key=True) name = fields.TextField() created_at = fields.DatetimeField(auto_now_add=True) class CustomTable(Model): - id = fields.IntField(pk=True) + id = fields.IntField(primary_key=True) name = fields.TextField() class Meta: table = "my_custom_table" +class CustomDBTable(Model): + id = fields.IntField(primary_key=True) + name = fields.TextField() + + class Meta: + db_table = "my_custom_db_table" + + +class CustomTableAndDBTable(Model): + id = fields.IntField(primary_key=True) + name = fields.TextField() + + class Meta: + table = "my_matching_table" + db_table = "my_matching_table" + + @pytest_asyncio.fixture async def table_name_db(): """Fixture for table name generator tests with in-memory SQLite.""" @@ -46,3 +64,24 @@ async def test_glabal_name_generator(table_name_db): @pytest.mark.asyncio async def test_custom_table_name_precedence(table_name_db): assert CustomTable._meta.db_table == "my_custom_table" + + +@pytest.mark.asyncio +async def test_custom_db_table_name_precedence(table_name_db): + assert CustomDBTable._meta.db_table == "my_custom_db_table" + + +@pytest.mark.asyncio +async def test_custom_table_and_db_table_name_match(table_name_db): + assert CustomTableAndDBTable._meta.db_table == "my_matching_table" + + +def test_conflicting_table_and_db_table_names(): + with pytest.raises(ConfigurationError, match="Meta.table and Meta.db_table"): + + class ConflictingTableName(Model): + id = fields.IntField(primary_key=True) + + class Meta: + table = "first_table" + db_table = "second_table" diff --git a/tortoise/models.py b/tortoise/models.py index aa20fdbd6..60e450315 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -85,6 +85,18 @@ def prepare_default_ordering(meta: Model.Meta) -> tuple[tuple[str, Order], ...]: return parsed_ordering +def resolve_db_table(meta: Model.Meta) -> str: + table = getattr(meta, "table", "") + db_table = getattr(meta, "db_table", "") + if not db_table: + return table + if table and table != db_table: + raise ConfigurationError( + "Meta.table and Meta.db_table must have the same value, or set only one." + ) + return db_table + + class FkSetterKwargs(TypedDict): _key: str relation_field: str @@ -222,7 +234,7 @@ class MetaInfo: def __init__(self, meta: Model.Meta) -> None: self.abstract: bool = getattr(meta, "abstract", False) self.manager: Manager = getattr(meta, "manager", Manager()) - self.db_table: str = getattr(meta, "table", "") + self.db_table: str = resolve_db_table(meta) self.schema: str | None = getattr(meta, "schema", None) self.app: str | None = getattr(meta, "app", None) self.unique_together: tuple[tuple[str, ...], ...] = get_together(meta, "unique_together")