From 7340e6e1b7f391719f496d95809d301cf5915163 Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Mon, 20 Jan 2025 15:59:28 +0800 Subject: [PATCH 1/2] refactor: reduce duplicated code in tests (#1846) * refactor: reduce duplicated code in tests * fix codacy issues --- tests/test_two_databases.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/tests/test_two_databases.py b/tests/test_two_databases.py index 29acf2f68..0aec81be9 100644 --- a/tests/test_two_databases.py +++ b/tests/test_two_databases.py @@ -26,35 +26,30 @@ async def asyncTearDown(self) -> None: await Tortoise._drop_databases() await super().asyncTearDown() + def build_select_sql(self) -> str: + if isinstance(self.db, OracleClient): + return 'SELECT * FROM "eventtwo"' + return "SELECT * FROM eventtwo" + async def test_two_databases(self): tournament = await Tournament.create(name="Tournament") await EventTwo.create(name="Event", tournament_id=tournament.id) + select_sql = self.build_select_sql() with self.assertRaises(OperationalError): - if isinstance(self.db, OracleClient): - await self.db.execute_query('SELECT * FROM "eventtwo"') - else: - await self.db.execute_query("SELECT * FROM eventtwo") - if isinstance(self.db, OracleClient): - _, results = await self.second_db.execute_query('SELECT * FROM "eventtwo"') - else: - _, results = await self.second_db.execute_query("SELECT * FROM eventtwo") + await self.db.execute_query(select_sql) + _, results = await self.second_db.execute_query(select_sql) self.assertEqual(dict(results[0]), {"id": 1, "name": "Event", "tournament_id": 1}) async def test_two_databases_relation(self): tournament = await Tournament.create(name="Tournament") event = await EventTwo.create(name="Event", tournament_id=tournament.id) + select_sql = self.build_select_sql() with self.assertRaises(OperationalError): - if isinstance(self.db, OracleClient): - await self.db.execute_query('SELECT * FROM "eventtwo"') - else: - await self.db.execute_query("SELECT * FROM eventtwo") + await self.db.execute_query(select_sql) - if isinstance(self.db, OracleClient): - _, results = await self.second_db.execute_query('SELECT * FROM "eventtwo"') - else: - _, results = await self.second_db.execute_query("SELECT * FROM eventtwo") + _, results = await self.second_db.execute_query(select_sql) self.assertEqual(dict(results[0]), {"id": 1, "name": "Event", "tournament_id": 1}) teams = [] From 71624e72ff14dd4789312e79a12524ec19fa94cd Mon Sep 17 00:00:00 2001 From: He Date: Mon, 20 Jan 2025 09:00:06 +0100 Subject: [PATCH 2/2] Fix index creation in Tortoise.generate_schemas() for MySQL and Postgres (#1847) * Fix index creation in Tortoise.generate_schemas() for MySQL and Postgres * Check for fields and expressions exclusivity in schema_generator * Fix Model.describe when Index is used * Fix myisam issue with indexes --- tests/fields/test_db_index.py | 13 ++++- tests/schema/test_generate_schema.py | 20 +++---- tests/testmodels.py | 17 ++++++ tests/utils/test_describe_model.py | 17 ++++++ tortoise/backends/base/schema_generator.py | 57 ++++++++++++++----- .../base_postgres/schema_generator.py | 23 +++++++- tortoise/backends/mssql/schema_generator.py | 16 +++++- tortoise/backends/mysql/schema_generator.py | 18 ++++-- tortoise/backends/oracle/schema_generator.py | 16 +++++- tortoise/contrib/postgres/indexes.py | 4 +- tortoise/indexes.py | 46 +++++---------- tortoise/models.py | 13 ++++- 12 files changed, 188 insertions(+), 72 deletions(-) diff --git a/tests/fields/test_db_index.py b/tests/fields/test_db_index.py index 9a7e1c7d2..8d61779f1 100644 --- a/tests/fields/test_db_index.py +++ b/tests/fields/test_db_index.py @@ -6,6 +6,7 @@ from tortoise.contrib import test from tortoise.exceptions import ConfigurationError from tortoise.indexes import Index +from tests.testmodels import ModelWithIndexes class CustomIndex(Index): @@ -14,7 +15,7 @@ def __init__(self, *args, **kw): self._foo = "" -class TestIndexHashEqualRepr(test.TestCase): +class TestIndexHashEqualRepr(test.SimpleTestCase): def test_index_eq(self): assert Index(fields=("id",)) == Index(fields=("id",)) assert CustomIndex(fields=("id",)) == CustomIndex(fields=("id",)) @@ -46,7 +47,7 @@ def test_index_repr(self): assert repr(Index(fields=("id",), name="MyIndex")) == "Index(fields=['id'], name='MyIndex')" assert repr(Index(Field("id"))) == f'Index({str(Field("id"))})' assert repr(Index(Field("a"), name="Id")) == f"Index({str(Field('a'))}, name='Id')" - with self.assertRaises(ValueError): + with self.assertRaises(ConfigurationError): Index(Field("id"), fields=("name",)) @@ -94,3 +95,11 @@ class TestIndexAliasUUID(TestIndexAlias): class TestIndexAliasChar(TestIndexAlias): Field = fields.CharField init_kwargs = {"max_length": 10} + + +class TestModelWithIndexes(test.TestCase): + def test_meta(self): + self.assertEqual(ModelWithIndexes._meta.indexes, [Index(fields=("f1", "f2"))]) + self.assertTrue(ModelWithIndexes._meta.fields_map["id"].index) + self.assertTrue(ModelWithIndexes._meta.fields_map["indexed"].index) + self.assertTrue(ModelWithIndexes._meta.fields_map["unique_indexed"].unique) diff --git a/tests/schema/test_generate_schema.py b/tests/schema/test_generate_schema.py index 40272d8e2..0b45385e5 100644 --- a/tests/schema/test_generate_schema.py +++ b/tests/schema/test_generate_schema.py @@ -724,10 +724,10 @@ async def test_index_safe(self): """CREATE TABLE IF NOT EXISTS `index` ( `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `full_text` LONGTEXT NOT NULL, - `geometry` GEOMETRY NOT NULL -) CHARACTER SET utf8mb4; -CREATE FULLTEXT INDEX IF NOT EXISTS `idx_index_full_te_3caba4` ON `index` (`full_text`) WITH PARSER ngram; -CREATE SPATIAL INDEX IF NOT EXISTS `idx_index_geometr_0b4dfb` ON `index` (`geometry`);""", + `geometry` GEOMETRY NOT NULL, + FULLTEXT KEY `idx_index_full_te_3caba4` (`full_text`) WITH PARSER ngram, + SPATIAL KEY `idx_index_geometr_0b4dfb` (`geometry`) +) CHARACTER SET utf8mb4;""", ) async def test_index_unsafe(self): @@ -738,10 +738,10 @@ async def test_index_unsafe(self): """CREATE TABLE `index` ( `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, `full_text` LONGTEXT NOT NULL, - `geometry` GEOMETRY NOT NULL -) CHARACTER SET utf8mb4; -CREATE FULLTEXT INDEX `idx_index_full_te_3caba4` ON `index` (`full_text`) WITH PARSER ngram; -CREATE SPATIAL INDEX `idx_index_geometr_0b4dfb` ON `index` (`geometry`);""", + `geometry` GEOMETRY NOT NULL, + FULLTEXT KEY `idx_index_full_te_3caba4` (`full_text`) WITH PARSER ngram, + SPATIAL KEY `idx_index_geometr_0b4dfb` (`geometry`) +) CHARACTER SET utf8mb4;""", ) async def test_m2m_no_auto_create(self): @@ -1102,7 +1102,7 @@ async def test_index_unsafe(self): CREATE INDEX "idx_index_gist_c807bf" ON "index" USING GIST ("gist"); CREATE INDEX "idx_index_sp_gist_2c0bad" ON "index" USING SPGIST ("sp_gist"); CREATE INDEX "idx_index_hash_cfe6b5" ON "index" USING HASH ("hash"); -CREATE INDEX "idx_index_partial_c5be6a" ON "index" USING ("partial") WHERE id = 1;""", +CREATE INDEX "idx_index_partial_c5be6a" ON "index" ("partial") WHERE id = 1;""", ) async def test_index_safe(self): @@ -1126,7 +1126,7 @@ async def test_index_safe(self): CREATE INDEX IF NOT EXISTS "idx_index_gist_c807bf" ON "index" USING GIST ("gist"); CREATE INDEX IF NOT EXISTS "idx_index_sp_gist_2c0bad" ON "index" USING SPGIST ("sp_gist"); CREATE INDEX IF NOT EXISTS "idx_index_hash_cfe6b5" ON "index" USING HASH ("hash"); -CREATE INDEX IF NOT EXISTS "idx_index_partial_c5be6a" ON "index" USING ("partial") WHERE id = 1;""", +CREATE INDEX IF NOT EXISTS "idx_index_partial_c5be6a" ON "index" ("partial") WHERE id = 1;""", ) async def test_m2m_no_auto_create(self): diff --git a/tests/testmodels.py b/tests/testmodels.py index 1e2c7dcf6..afd4b82fe 100644 --- a/tests/testmodels.py +++ b/tests/testmodels.py @@ -17,6 +17,7 @@ from tortoise import fields from tortoise.exceptions import ValidationError from tortoise.fields import NO_ACTION +from tortoise.indexes import Index from tortoise.manager import Manager from tortoise.models import Model from tortoise.queryset import QuerySet @@ -1050,3 +1051,19 @@ class BenchmarkManyFields(Model): col_text4 = fields.TextField(null=True) col_decimal4 = fields.DecimalField(12, 8, null=True) col_json4 = fields.JSONField[dict](null=True) + + +class ModelWithIndexes(Model): + id = fields.IntField(primary_key=True) + indexed = fields.CharField(max_length=16, index=True) + unique_indexed = fields.CharField(max_length=16, unique=True) + f1 = fields.CharField(max_length=16) + f2 = fields.CharField(max_length=16) + u1 = fields.IntField() + u2 = fields.IntField() + + class Meta: + indexes = [ + Index(fields=["f1", "f2"]), + ] + unique_together = [("u1", "u2")] diff --git a/tests/utils/test_describe_model.py b/tests/utils/test_describe_model.py index 998d6e794..d1be42986 100644 --- a/tests/utils/test_describe_model.py +++ b/tests/utils/test_describe_model.py @@ -5,6 +5,7 @@ from tests.testmodels import ( Event, JSONFields, + ModelWithIndexes, Reporter, SourceFields, StraightFields, @@ -1561,3 +1562,19 @@ def test_describe_model_json_native(self): "m2m_fields": [], }, ) + + def test_describe_indexes_serializable(self): + val = ModelWithIndexes.describe() + + self.assertEqual( + val["indexes"], + [{"fields": ["f1", "f2"], "expressions": [], "name": None, "type": "", "extra": ""}], + ) + + def test_describe_indexes_not_serializable(self): + val = ModelWithIndexes.describe(serializable=False) + + self.assertEqual( + val["indexes"], + ModelWithIndexes._meta.indexes, + ) diff --git a/tortoise/backends/base/schema_generator.py b/tortoise/backends/base/schema_generator.py index dee3de9b0..1f56e7cf6 100644 --- a/tortoise/backends/base/schema_generator.py +++ b/tortoise/backends/base/schema_generator.py @@ -1,6 +1,8 @@ import re from hashlib import sha256 -from typing import TYPE_CHECKING, Any, List, Set, Type, Union, cast +from typing import TYPE_CHECKING, Any, List, Optional, Set, Type, Union, cast + +from pypika_tortoise.context import DEFAULT_SQL_CONTEXT from tortoise.exceptions import ConfigurationError from tortoise.fields import JSONField, TextField, UUIDField @@ -23,8 +25,10 @@ class BaseSchemaGenerator: DIALECT = "sql" TABLE_CREATE_TEMPLATE = 'CREATE TABLE {exists}"{table_name}" ({fields}){extra}{comment};' FIELD_TEMPLATE = '"{name}" {type}{nullable}{unique}{primary}{default}{comment}' - INDEX_CREATE_TEMPLATE = 'CREATE INDEX {exists}"{index_name}" ON "{table_name}" ({fields});' - UNIQUE_INDEX_CREATE_TEMPLATE = INDEX_CREATE_TEMPLATE.replace(" INDEX", " UNIQUE INDEX") + INDEX_CREATE_TEMPLATE = ( + 'CREATE {index_type}INDEX {exists}"{index_name}" ON "{table_name}" ({fields}){extra};' + ) + UNIQUE_INDEX_CREATE_TEMPLATE = INDEX_CREATE_TEMPLATE.replace("INDEX", "UNIQUE INDEX") UNIQUE_CONSTRAINT_CREATE_TEMPLATE = 'CONSTRAINT "{index_name}" UNIQUE ({fields})' GENERATED_PK_TEMPLATE = '"{field_name}" {generated_sql}{comment}' FK_TEMPLATE = ' REFERENCES "{table}" ("{field}") ON DELETE {on_delete}{comment}' @@ -167,12 +171,22 @@ def _generate_fk_name( ) return index_name - def _get_index_sql(self, model: "Type[Model]", field_names: List[str], safe: bool) -> str: + def _get_index_sql( + self, + model: "Type[Model]", + field_names: List[str], + safe: bool, + index_name: Optional[str] = None, + index_type: Optional[str] = None, + extra: Optional[str] = None, + ) -> str: return self.INDEX_CREATE_TEMPLATE.format( exists="IF NOT EXISTS " if safe else "", - index_name=self._generate_index_name("idx", model, field_names), + index_name=index_name or self._generate_index_name("idx", model, field_names), + index_type=f"{index_type} " if index_type else "", table_name=model._meta.db_table, fields=", ".join([self.quote(f) for f in field_names]), + extra=f"{extra}" if extra else "", ) def _get_unique_index_sql(self, exists: str, table_name: str, field_names: List[str]) -> str: @@ -180,8 +194,10 @@ def _get_unique_index_sql(self, exists: str, table_name: str, field_names: List[ return self.UNIQUE_INDEX_CREATE_TEMPLATE.format( exists=exists, index_name=index_name, + index_type="", table_name=table_name, fields=", ".join([self.quote(f) for f in field_names]), + extra="", ) def _get_unique_constraint_sql(self, model: "Type[Model]", field_names: List[str]) -> str: @@ -324,22 +340,37 @@ def _get_table_sql(self, model: "Type[Model]", safe: bool = True) -> dict: self._get_unique_constraint_sql(model, unique_together_to_create) ) - # Indexes. _indexes = [ self._get_index_sql(model, [field_name], safe=safe) for field_name in fields_with_index ] if model._meta.indexes: - for indexes_list in model._meta.indexes: - if not isinstance(indexes_list, Index): - indexes_to_create = [] - for field in indexes_list: + for index in model._meta.indexes: + if not isinstance(index, Index): + fields = [] + for field in index: field_object = model._meta.fields_map[field] - indexes_to_create.append(field_object.source_field or field) + fields.append(field_object.source_field or field) - _indexes.append(self._get_index_sql(model, indexes_to_create, safe=safe)) + _indexes.append(self._get_index_sql(model, fields, safe=safe)) else: - _indexes.append(indexes_list.get_sql(self, model, safe)) + if index.fields: + fields = [f for f in index.fields] + elif index.expressions: + fields = [ + f"({expression.get_sql(DEFAULT_SQL_CONTEXT)})" + for expression in index.expressions + ] + else: + raise ConfigurationError( + "At least one field or expression is required to define an index." + ) + + _indexes.append( + self._get_index_sql( + model, fields, safe=safe, index_type=index.INDEX_TYPE, extra=index.extra + ) + ) field_indexes_sqls = [val for val in list(dict.fromkeys(_indexes)) if val] diff --git a/tortoise/backends/base_postgres/schema_generator.py b/tortoise/backends/base_postgres/schema_generator.py index 556892396..05ea3b634 100644 --- a/tortoise/backends/base_postgres/schema_generator.py +++ b/tortoise/backends/base_postgres/schema_generator.py @@ -1,7 +1,8 @@ -from typing import TYPE_CHECKING, Any, List +from typing import TYPE_CHECKING, Any, List, Optional, Type from tortoise.backends.base.schema_generator import BaseSchemaGenerator from tortoise.converters import encoders +from tortoise.models import Model if TYPE_CHECKING: # pragma: nocoverage from .client import BasePostgresClient @@ -9,6 +10,10 @@ class BasePostgresSchemaGenerator(BaseSchemaGenerator): DIALECT = "postgres" + INDEX_CREATE_TEMPLATE = ( + 'CREATE INDEX {exists}"{index_name}" ON "{table_name}" {index_type}({fields}){extra};' + ) + UNIQUE_INDEX_CREATE_TEMPLATE = INDEX_CREATE_TEMPLATE.replace("INDEX", "UNIQUE INDEX") TABLE_COMMENT_TEMPLATE = "COMMENT ON TABLE \"{table}\" IS '{comment}';" COLUMN_COMMENT_TEMPLATE = 'COMMENT ON COLUMN "{table}"."{column}" IS \'{comment}\';' GENERATED_PK_TEMPLATE = '"{field_name}" {generated_sql}' @@ -61,3 +66,19 @@ def _escape_default_value(self, default: Any): if isinstance(default, bool): return default return encoders.get(type(default))(default) # type: ignore + + def _get_index_sql( + self, + model: "Type[Model]", + field_names: List[str], + safe: bool, + index_name: Optional[str] = None, + index_type: Optional[str] = None, + extra: Optional[str] = None, + ) -> str: + if index_type: + index_type = f"USING {index_type}" + + return super()._get_index_sql( + model, field_names, safe, index_name=index_name, index_type=index_type, extra=extra + ) diff --git a/tortoise/backends/mssql/schema_generator.py b/tortoise/backends/mssql/schema_generator.py index 0bcb0a217..da89e9812 100644 --- a/tortoise/backends/mssql/schema_generator.py +++ b/tortoise/backends/mssql/schema_generator.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, List, Type +from typing import TYPE_CHECKING, Any, List, Optional, Type from tortoise.backends.base.schema_generator import BaseSchemaGenerator from tortoise.converters import encoders @@ -59,8 +59,18 @@ def _column_default_generator( def _escape_default_value(self, default: Any): return encoders.get(type(default))(default) # type: ignore - def _get_index_sql(self, model: "Type[Model]", field_names: List[str], safe: bool) -> str: - return super(MSSQLSchemaGenerator, self)._get_index_sql(model, field_names, False) + def _get_index_sql( + self, + model: "Type[Model]", + field_names: List[str], + safe: bool, + index_name: Optional[str] = None, + index_type: Optional[str] = None, + extra: Optional[str] = None, + ) -> str: + return super(MSSQLSchemaGenerator, self)._get_index_sql( + model, field_names, False, index_name=index_name, index_type=index_type, extra=extra + ) def _get_table_sql(self, model: "Type[Model]", safe: bool = True) -> dict: return super(MSSQLSchemaGenerator, self)._get_table_sql(model, False) diff --git a/tortoise/backends/mysql/schema_generator.py b/tortoise/backends/mysql/schema_generator.py index 0e72facd5..7ef898715 100644 --- a/tortoise/backends/mysql/schema_generator.py +++ b/tortoise/backends/mysql/schema_generator.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, List, Type +from typing import TYPE_CHECKING, Any, List, Optional, Type from tortoise.backends.base.schema_generator import BaseSchemaGenerator from tortoise.converters import encoders @@ -11,7 +11,7 @@ class MySQLSchemaGenerator(BaseSchemaGenerator): DIALECT = "mysql" TABLE_CREATE_TEMPLATE = "CREATE TABLE {exists}`{table_name}` ({fields}){extra}{comment};" - INDEX_CREATE_TEMPLATE = "KEY `{index_name}` ({fields})" + INDEX_CREATE_TEMPLATE = "{index_type}KEY `{index_name}` ({fields}){extra}" UNIQUE_CONSTRAINT_CREATE_TEMPLATE = "UNIQUE KEY `{index_name}` ({fields})" UNIQUE_INDEX_CREATE_TEMPLATE = UNIQUE_CONSTRAINT_CREATE_TEMPLATE FIELD_TEMPLATE = "`{name}` {type}{nullable}{unique}{primary}{comment}{default}" @@ -68,9 +68,19 @@ def _column_default_generator( def _escape_default_value(self, default: Any): return encoders.get(type(default))(default) # type: ignore - def _get_index_sql(self, model: "Type[Model]", field_names: List[str], safe: bool) -> str: + def _get_index_sql( + self, + model: "Type[Model]", + field_names: List[str], + safe: bool, + index_name: Optional[str] = None, + index_type: Optional[str] = None, + extra: Optional[str] = None, + ) -> str: """Get index SQLs, but keep them for ourselves""" - index_create_sql = super()._get_index_sql(model, field_names, safe) + index_create_sql = super()._get_index_sql( + model, field_names, safe, index_name=index_name, index_type=index_type, extra=extra + ) self._field_indexes.append(index_create_sql) return "" diff --git a/tortoise/backends/oracle/schema_generator.py b/tortoise/backends/oracle/schema_generator.py index d66908d5c..ed99f3771 100644 --- a/tortoise/backends/oracle/schema_generator.py +++ b/tortoise/backends/oracle/schema_generator.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, List, Type +from typing import TYPE_CHECKING, Any, List, Optional, Type from tortoise.backends.base.schema_generator import BaseSchemaGenerator from tortoise.converters import encoders @@ -85,8 +85,18 @@ def _column_default_generator( def _escape_default_value(self, default: Any): return encoders.get(type(default))(default) # type: ignore - def _get_index_sql(self, model: "Type[Model]", field_names: List[str], safe: bool) -> str: - return super(OracleSchemaGenerator, self)._get_index_sql(model, field_names, False) + def _get_index_sql( + self, + model: "Type[Model]", + field_names: List[str], + safe: bool, + index_name: Optional[str] = None, + index_type: Optional[str] = None, + extra: Optional[str] = None, + ) -> str: + return super(OracleSchemaGenerator, self)._get_index_sql( + model, field_names, False, index_name=index_name, index_type=index_type, extra=extra + ) def _get_table_sql(self, model: "Type[Model]", safe: bool = True) -> dict: return super(OracleSchemaGenerator, self)._get_table_sql(model, False) diff --git a/tortoise/contrib/postgres/indexes.py b/tortoise/contrib/postgres/indexes.py index 3346d4a68..499ce11ef 100644 --- a/tortoise/contrib/postgres/indexes.py +++ b/tortoise/contrib/postgres/indexes.py @@ -2,9 +2,7 @@ class PostgreSQLIndex(PartialIndex): - INDEX_CREATE_TEMPLATE = ( - "CREATE INDEX {exists}{index_name} ON {table_name} USING{index_type}({fields}){extra};" - ) + pass class BloomIndex(PostgreSQLIndex): diff --git a/tortoise/indexes.py b/tortoise/indexes.py index b3be63da5..bba96e1aa 100644 --- a/tortoise/indexes.py +++ b/tortoise/indexes.py @@ -1,19 +1,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Type +from typing import Any from pypika_tortoise.terms import Term, ValueWrapper -if TYPE_CHECKING: - from tortoise import Model - from tortoise.backends.base.schema_generator import BaseSchemaGenerator +from tortoise.exceptions import ConfigurationError class Index: INDEX_TYPE = "" - INDEX_CREATE_TEMPLATE = ( - "CREATE{index_type}INDEX {exists}{index_name} ON {table_name} ({fields}){extra};" - ) def __init__( self, @@ -31,15 +26,26 @@ def __init__( """ self.fields = list(fields or []) if not expressions and not fields: - raise ValueError("At least one field or expression is required to define an " "index.") + raise ConfigurationError( + "At least one field or expression is required to define an " "index." + ) if expressions and fields: - raise ValueError( + raise ConfigurationError( "Index.fields and expressions are mutually exclusive.", ) self.name = name self.expressions = expressions self.extra = "" + def describe(self) -> dict: + return { + "fields": self.fields, + "expressions": [str(expression) for expression in self.expressions], + "name": self.name, + "type": self.INDEX_TYPE, + "extra": self.extra, + } + def __repr__(self) -> str: argument = "" if self.expressions: @@ -50,28 +56,6 @@ def __repr__(self) -> str: argument += f", {name=}" return self.__class__.__name__ + "(" + argument + ")" - def get_sql( - self, schema_generator: "BaseSchemaGenerator", model: "Type[Model]", safe: bool - ) -> str: - if self.fields: - fields = ", ".join(schema_generator.quote(f) for f in self.fields) - else: - ctx = schema_generator.client.query_class.SQL_CONTEXT - expressions = [f"({expression.get_sql(ctx)})" for expression in self.expressions] - fields = ", ".join(expressions) - - return self.INDEX_CREATE_TEMPLATE.format( - exists="IF NOT EXISTS " if safe else "", - index_name=schema_generator.quote(self.index_name(schema_generator, model)), - index_type=f" {self.INDEX_TYPE} ", - table_name=schema_generator.quote(model._meta.db_table), - fields=fields, - extra=self.extra, - ) - - def index_name(self, schema_generator: "BaseSchemaGenerator", model: "Type[Model]") -> str: - return self.name or schema_generator._generate_index_name("idx", model, self.fields) - def __hash__(self) -> int: return hash((tuple(self.fields), self.name, tuple(self.expressions))) diff --git a/tortoise/models.py b/tortoise/models.py index 63bfa34f7..a3903f062 100644 --- a/tortoise/models.py +++ b/tortoise/models.py @@ -229,7 +229,7 @@ def __init__(self, meta: "Model.Meta") -> None: self.schema: Optional[str] = getattr(meta, "schema", None) self.app: Optional[str] = getattr(meta, "app", None) self.unique_together: Tuple[Tuple[str, ...], ...] = get_together(meta, "unique_together") - self.indexes: Tuple[Tuple[str, ...], ...] = get_together(meta, "indexes") + self.indexes: Tuple[Union[Tuple[str, ...], Index], ...] = get_together(meta, "indexes") self._default_ordering: Tuple[Tuple[str, Order], ...] = prepare_default_ordering(meta) self._ordering_validated: bool = False self.fields: Set[str] = set() @@ -1466,6 +1466,15 @@ def _check_together(cls, together: str) -> None: " to ManyToMany field." ) + @classmethod + def _describe_index( + cls, index: Union[Index, Tuple[str, ...]], serializable: bool + ) -> Union[Index, Tuple[str, ...], dict]: + if isinstance(index, Index): + return index.describe() if serializable else index + + return index + @classmethod def describe(cls, serializable: bool = True) -> dict: """ @@ -1511,7 +1520,7 @@ def describe(cls, serializable: bool = True) -> dict: "description": cls._meta.table_description or None, "docstring": inspect.cleandoc(cls.__doc__ or "") or None, "unique_together": cls._meta.unique_together or [], - "indexes": cls._meta.indexes or [], + "indexes": [cls._describe_index(index, serializable) for index in cls._meta.indexes], "pk_field": cls._meta.fields_map[cls._meta.pk_attr].describe(serializable), "data_fields": [ field.describe(serializable)