Skip to content

Commit 71624e7

Browse files
authored
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
1 parent 7340e6e commit 71624e7

File tree

12 files changed

+188
-72
lines changed

12 files changed

+188
-72
lines changed

tests/fields/test_db_index.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from tortoise.contrib import test
77
from tortoise.exceptions import ConfigurationError
88
from tortoise.indexes import Index
9+
from tests.testmodels import ModelWithIndexes
910

1011

1112
class CustomIndex(Index):
@@ -14,7 +15,7 @@ def __init__(self, *args, **kw):
1415
self._foo = ""
1516

1617

17-
class TestIndexHashEqualRepr(test.TestCase):
18+
class TestIndexHashEqualRepr(test.SimpleTestCase):
1819
def test_index_eq(self):
1920
assert Index(fields=("id",)) == Index(fields=("id",))
2021
assert CustomIndex(fields=("id",)) == CustomIndex(fields=("id",))
@@ -46,7 +47,7 @@ def test_index_repr(self):
4647
assert repr(Index(fields=("id",), name="MyIndex")) == "Index(fields=['id'], name='MyIndex')"
4748
assert repr(Index(Field("id"))) == f'Index({str(Field("id"))})'
4849
assert repr(Index(Field("a"), name="Id")) == f"Index({str(Field('a'))}, name='Id')"
49-
with self.assertRaises(ValueError):
50+
with self.assertRaises(ConfigurationError):
5051
Index(Field("id"), fields=("name",))
5152

5253

@@ -94,3 +95,11 @@ class TestIndexAliasUUID(TestIndexAlias):
9495
class TestIndexAliasChar(TestIndexAlias):
9596
Field = fields.CharField
9697
init_kwargs = {"max_length": 10}
98+
99+
100+
class TestModelWithIndexes(test.TestCase):
101+
def test_meta(self):
102+
self.assertEqual(ModelWithIndexes._meta.indexes, [Index(fields=("f1", "f2"))])
103+
self.assertTrue(ModelWithIndexes._meta.fields_map["id"].index)
104+
self.assertTrue(ModelWithIndexes._meta.fields_map["indexed"].index)
105+
self.assertTrue(ModelWithIndexes._meta.fields_map["unique_indexed"].unique)

tests/schema/test_generate_schema.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -724,10 +724,10 @@ async def test_index_safe(self):
724724
"""CREATE TABLE IF NOT EXISTS `index` (
725725
`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
726726
`full_text` LONGTEXT NOT NULL,
727-
`geometry` GEOMETRY NOT NULL
728-
) CHARACTER SET utf8mb4;
729-
CREATE FULLTEXT INDEX IF NOT EXISTS `idx_index_full_te_3caba4` ON `index` (`full_text`) WITH PARSER ngram;
730-
CREATE SPATIAL INDEX IF NOT EXISTS `idx_index_geometr_0b4dfb` ON `index` (`geometry`);""",
727+
`geometry` GEOMETRY NOT NULL,
728+
FULLTEXT KEY `idx_index_full_te_3caba4` (`full_text`) WITH PARSER ngram,
729+
SPATIAL KEY `idx_index_geometr_0b4dfb` (`geometry`)
730+
) CHARACTER SET utf8mb4;""",
731731
)
732732

733733
async def test_index_unsafe(self):
@@ -738,10 +738,10 @@ async def test_index_unsafe(self):
738738
"""CREATE TABLE `index` (
739739
`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
740740
`full_text` LONGTEXT NOT NULL,
741-
`geometry` GEOMETRY NOT NULL
742-
) CHARACTER SET utf8mb4;
743-
CREATE FULLTEXT INDEX `idx_index_full_te_3caba4` ON `index` (`full_text`) WITH PARSER ngram;
744-
CREATE SPATIAL INDEX `idx_index_geometr_0b4dfb` ON `index` (`geometry`);""",
741+
`geometry` GEOMETRY NOT NULL,
742+
FULLTEXT KEY `idx_index_full_te_3caba4` (`full_text`) WITH PARSER ngram,
743+
SPATIAL KEY `idx_index_geometr_0b4dfb` (`geometry`)
744+
) CHARACTER SET utf8mb4;""",
745745
)
746746

747747
async def test_m2m_no_auto_create(self):
@@ -1102,7 +1102,7 @@ async def test_index_unsafe(self):
11021102
CREATE INDEX "idx_index_gist_c807bf" ON "index" USING GIST ("gist");
11031103
CREATE INDEX "idx_index_sp_gist_2c0bad" ON "index" USING SPGIST ("sp_gist");
11041104
CREATE INDEX "idx_index_hash_cfe6b5" ON "index" USING HASH ("hash");
1105-
CREATE INDEX "idx_index_partial_c5be6a" ON "index" USING ("partial") WHERE id = 1;""",
1105+
CREATE INDEX "idx_index_partial_c5be6a" ON "index" ("partial") WHERE id = 1;""",
11061106
)
11071107

11081108
async def test_index_safe(self):
@@ -1126,7 +1126,7 @@ async def test_index_safe(self):
11261126
CREATE INDEX IF NOT EXISTS "idx_index_gist_c807bf" ON "index" USING GIST ("gist");
11271127
CREATE INDEX IF NOT EXISTS "idx_index_sp_gist_2c0bad" ON "index" USING SPGIST ("sp_gist");
11281128
CREATE INDEX IF NOT EXISTS "idx_index_hash_cfe6b5" ON "index" USING HASH ("hash");
1129-
CREATE INDEX IF NOT EXISTS "idx_index_partial_c5be6a" ON "index" USING ("partial") WHERE id = 1;""",
1129+
CREATE INDEX IF NOT EXISTS "idx_index_partial_c5be6a" ON "index" ("partial") WHERE id = 1;""",
11301130
)
11311131

11321132
async def test_m2m_no_auto_create(self):

tests/testmodels.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from tortoise import fields
1818
from tortoise.exceptions import ValidationError
1919
from tortoise.fields import NO_ACTION
20+
from tortoise.indexes import Index
2021
from tortoise.manager import Manager
2122
from tortoise.models import Model
2223
from tortoise.queryset import QuerySet
@@ -1050,3 +1051,19 @@ class BenchmarkManyFields(Model):
10501051
col_text4 = fields.TextField(null=True)
10511052
col_decimal4 = fields.DecimalField(12, 8, null=True)
10521053
col_json4 = fields.JSONField[dict](null=True)
1054+
1055+
1056+
class ModelWithIndexes(Model):
1057+
id = fields.IntField(primary_key=True)
1058+
indexed = fields.CharField(max_length=16, index=True)
1059+
unique_indexed = fields.CharField(max_length=16, unique=True)
1060+
f1 = fields.CharField(max_length=16)
1061+
f2 = fields.CharField(max_length=16)
1062+
u1 = fields.IntField()
1063+
u2 = fields.IntField()
1064+
1065+
class Meta:
1066+
indexes = [
1067+
Index(fields=["f1", "f2"]),
1068+
]
1069+
unique_together = [("u1", "u2")]

tests/utils/test_describe_model.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from tests.testmodels import (
66
Event,
77
JSONFields,
8+
ModelWithIndexes,
89
Reporter,
910
SourceFields,
1011
StraightFields,
@@ -1561,3 +1562,19 @@ def test_describe_model_json_native(self):
15611562
"m2m_fields": [],
15621563
},
15631564
)
1565+
1566+
def test_describe_indexes_serializable(self):
1567+
val = ModelWithIndexes.describe()
1568+
1569+
self.assertEqual(
1570+
val["indexes"],
1571+
[{"fields": ["f1", "f2"], "expressions": [], "name": None, "type": "", "extra": ""}],
1572+
)
1573+
1574+
def test_describe_indexes_not_serializable(self):
1575+
val = ModelWithIndexes.describe(serializable=False)
1576+
1577+
self.assertEqual(
1578+
val["indexes"],
1579+
ModelWithIndexes._meta.indexes,
1580+
)

tortoise/backends/base/schema_generator.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import re
22
from hashlib import sha256
3-
from typing import TYPE_CHECKING, Any, List, Set, Type, Union, cast
3+
from typing import TYPE_CHECKING, Any, List, Optional, Set, Type, Union, cast
4+
5+
from pypika_tortoise.context import DEFAULT_SQL_CONTEXT
46

57
from tortoise.exceptions import ConfigurationError
68
from tortoise.fields import JSONField, TextField, UUIDField
@@ -23,8 +25,10 @@ class BaseSchemaGenerator:
2325
DIALECT = "sql"
2426
TABLE_CREATE_TEMPLATE = 'CREATE TABLE {exists}"{table_name}" ({fields}){extra}{comment};'
2527
FIELD_TEMPLATE = '"{name}" {type}{nullable}{unique}{primary}{default}{comment}'
26-
INDEX_CREATE_TEMPLATE = 'CREATE INDEX {exists}"{index_name}" ON "{table_name}" ({fields});'
27-
UNIQUE_INDEX_CREATE_TEMPLATE = INDEX_CREATE_TEMPLATE.replace(" INDEX", " UNIQUE INDEX")
28+
INDEX_CREATE_TEMPLATE = (
29+
'CREATE {index_type}INDEX {exists}"{index_name}" ON "{table_name}" ({fields}){extra};'
30+
)
31+
UNIQUE_INDEX_CREATE_TEMPLATE = INDEX_CREATE_TEMPLATE.replace("INDEX", "UNIQUE INDEX")
2832
UNIQUE_CONSTRAINT_CREATE_TEMPLATE = 'CONSTRAINT "{index_name}" UNIQUE ({fields})'
2933
GENERATED_PK_TEMPLATE = '"{field_name}" {generated_sql}{comment}'
3034
FK_TEMPLATE = ' REFERENCES "{table}" ("{field}") ON DELETE {on_delete}{comment}'
@@ -167,21 +171,33 @@ def _generate_fk_name(
167171
)
168172
return index_name
169173

170-
def _get_index_sql(self, model: "Type[Model]", field_names: List[str], safe: bool) -> str:
174+
def _get_index_sql(
175+
self,
176+
model: "Type[Model]",
177+
field_names: List[str],
178+
safe: bool,
179+
index_name: Optional[str] = None,
180+
index_type: Optional[str] = None,
181+
extra: Optional[str] = None,
182+
) -> str:
171183
return self.INDEX_CREATE_TEMPLATE.format(
172184
exists="IF NOT EXISTS " if safe else "",
173-
index_name=self._generate_index_name("idx", model, field_names),
185+
index_name=index_name or self._generate_index_name("idx", model, field_names),
186+
index_type=f"{index_type} " if index_type else "",
174187
table_name=model._meta.db_table,
175188
fields=", ".join([self.quote(f) for f in field_names]),
189+
extra=f"{extra}" if extra else "",
176190
)
177191

178192
def _get_unique_index_sql(self, exists: str, table_name: str, field_names: List[str]) -> str:
179193
index_name = self._generate_index_name("uidx", table_name, field_names)
180194
return self.UNIQUE_INDEX_CREATE_TEMPLATE.format(
181195
exists=exists,
182196
index_name=index_name,
197+
index_type="",
183198
table_name=table_name,
184199
fields=", ".join([self.quote(f) for f in field_names]),
200+
extra="",
185201
)
186202

187203
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:
324340
self._get_unique_constraint_sql(model, unique_together_to_create)
325341
)
326342

327-
# Indexes.
328343
_indexes = [
329344
self._get_index_sql(model, [field_name], safe=safe) for field_name in fields_with_index
330345
]
331346

332347
if model._meta.indexes:
333-
for indexes_list in model._meta.indexes:
334-
if not isinstance(indexes_list, Index):
335-
indexes_to_create = []
336-
for field in indexes_list:
348+
for index in model._meta.indexes:
349+
if not isinstance(index, Index):
350+
fields = []
351+
for field in index:
337352
field_object = model._meta.fields_map[field]
338-
indexes_to_create.append(field_object.source_field or field)
353+
fields.append(field_object.source_field or field)
339354

340-
_indexes.append(self._get_index_sql(model, indexes_to_create, safe=safe))
355+
_indexes.append(self._get_index_sql(model, fields, safe=safe))
341356
else:
342-
_indexes.append(indexes_list.get_sql(self, model, safe))
357+
if index.fields:
358+
fields = [f for f in index.fields]
359+
elif index.expressions:
360+
fields = [
361+
f"({expression.get_sql(DEFAULT_SQL_CONTEXT)})"
362+
for expression in index.expressions
363+
]
364+
else:
365+
raise ConfigurationError(
366+
"At least one field or expression is required to define an index."
367+
)
368+
369+
_indexes.append(
370+
self._get_index_sql(
371+
model, fields, safe=safe, index_type=index.INDEX_TYPE, extra=index.extra
372+
)
373+
)
343374

344375
field_indexes_sqls = [val for val in list(dict.fromkeys(_indexes)) if val]
345376

tortoise/backends/base_postgres/schema_generator.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1-
from typing import TYPE_CHECKING, Any, List
1+
from typing import TYPE_CHECKING, Any, List, Optional, Type
22

33
from tortoise.backends.base.schema_generator import BaseSchemaGenerator
44
from tortoise.converters import encoders
5+
from tortoise.models import Model
56

67
if TYPE_CHECKING: # pragma: nocoverage
78
from .client import BasePostgresClient
89

910

1011
class BasePostgresSchemaGenerator(BaseSchemaGenerator):
1112
DIALECT = "postgres"
13+
INDEX_CREATE_TEMPLATE = (
14+
'CREATE INDEX {exists}"{index_name}" ON "{table_name}" {index_type}({fields}){extra};'
15+
)
16+
UNIQUE_INDEX_CREATE_TEMPLATE = INDEX_CREATE_TEMPLATE.replace("INDEX", "UNIQUE INDEX")
1217
TABLE_COMMENT_TEMPLATE = "COMMENT ON TABLE \"{table}\" IS '{comment}';"
1318
COLUMN_COMMENT_TEMPLATE = 'COMMENT ON COLUMN "{table}"."{column}" IS \'{comment}\';'
1419
GENERATED_PK_TEMPLATE = '"{field_name}" {generated_sql}'
@@ -61,3 +66,19 @@ def _escape_default_value(self, default: Any):
6166
if isinstance(default, bool):
6267
return default
6368
return encoders.get(type(default))(default) # type: ignore
69+
70+
def _get_index_sql(
71+
self,
72+
model: "Type[Model]",
73+
field_names: List[str],
74+
safe: bool,
75+
index_name: Optional[str] = None,
76+
index_type: Optional[str] = None,
77+
extra: Optional[str] = None,
78+
) -> str:
79+
if index_type:
80+
index_type = f"USING {index_type}"
81+
82+
return super()._get_index_sql(
83+
model, field_names, safe, index_name=index_name, index_type=index_type, extra=extra
84+
)

tortoise/backends/mssql/schema_generator.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, Any, List, Type
1+
from typing import TYPE_CHECKING, Any, List, Optional, Type
22

33
from tortoise.backends.base.schema_generator import BaseSchemaGenerator
44
from tortoise.converters import encoders
@@ -59,8 +59,18 @@ def _column_default_generator(
5959
def _escape_default_value(self, default: Any):
6060
return encoders.get(type(default))(default) # type: ignore
6161

62-
def _get_index_sql(self, model: "Type[Model]", field_names: List[str], safe: bool) -> str:
63-
return super(MSSQLSchemaGenerator, self)._get_index_sql(model, field_names, False)
62+
def _get_index_sql(
63+
self,
64+
model: "Type[Model]",
65+
field_names: List[str],
66+
safe: bool,
67+
index_name: Optional[str] = None,
68+
index_type: Optional[str] = None,
69+
extra: Optional[str] = None,
70+
) -> str:
71+
return super(MSSQLSchemaGenerator, self)._get_index_sql(
72+
model, field_names, False, index_name=index_name, index_type=index_type, extra=extra
73+
)
6474

6575
def _get_table_sql(self, model: "Type[Model]", safe: bool = True) -> dict:
6676
return super(MSSQLSchemaGenerator, self)._get_table_sql(model, False)

tortoise/backends/mysql/schema_generator.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, Any, List, Type
1+
from typing import TYPE_CHECKING, Any, List, Optional, Type
22

33
from tortoise.backends.base.schema_generator import BaseSchemaGenerator
44
from tortoise.converters import encoders
@@ -11,7 +11,7 @@
1111
class MySQLSchemaGenerator(BaseSchemaGenerator):
1212
DIALECT = "mysql"
1313
TABLE_CREATE_TEMPLATE = "CREATE TABLE {exists}`{table_name}` ({fields}){extra}{comment};"
14-
INDEX_CREATE_TEMPLATE = "KEY `{index_name}` ({fields})"
14+
INDEX_CREATE_TEMPLATE = "{index_type}KEY `{index_name}` ({fields}){extra}"
1515
UNIQUE_CONSTRAINT_CREATE_TEMPLATE = "UNIQUE KEY `{index_name}` ({fields})"
1616
UNIQUE_INDEX_CREATE_TEMPLATE = UNIQUE_CONSTRAINT_CREATE_TEMPLATE
1717
FIELD_TEMPLATE = "`{name}` {type}{nullable}{unique}{primary}{comment}{default}"
@@ -68,9 +68,19 @@ def _column_default_generator(
6868
def _escape_default_value(self, default: Any):
6969
return encoders.get(type(default))(default) # type: ignore
7070

71-
def _get_index_sql(self, model: "Type[Model]", field_names: List[str], safe: bool) -> str:
71+
def _get_index_sql(
72+
self,
73+
model: "Type[Model]",
74+
field_names: List[str],
75+
safe: bool,
76+
index_name: Optional[str] = None,
77+
index_type: Optional[str] = None,
78+
extra: Optional[str] = None,
79+
) -> str:
7280
"""Get index SQLs, but keep them for ourselves"""
73-
index_create_sql = super()._get_index_sql(model, field_names, safe)
81+
index_create_sql = super()._get_index_sql(
82+
model, field_names, safe, index_name=index_name, index_type=index_type, extra=extra
83+
)
7484
self._field_indexes.append(index_create_sql)
7585
return ""
7686

tortoise/backends/oracle/schema_generator.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, Any, List, Type
1+
from typing import TYPE_CHECKING, Any, List, Optional, Type
22

33
from tortoise.backends.base.schema_generator import BaseSchemaGenerator
44
from tortoise.converters import encoders
@@ -85,8 +85,18 @@ def _column_default_generator(
8585
def _escape_default_value(self, default: Any):
8686
return encoders.get(type(default))(default) # type: ignore
8787

88-
def _get_index_sql(self, model: "Type[Model]", field_names: List[str], safe: bool) -> str:
89-
return super(OracleSchemaGenerator, self)._get_index_sql(model, field_names, False)
88+
def _get_index_sql(
89+
self,
90+
model: "Type[Model]",
91+
field_names: List[str],
92+
safe: bool,
93+
index_name: Optional[str] = None,
94+
index_type: Optional[str] = None,
95+
extra: Optional[str] = None,
96+
) -> str:
97+
return super(OracleSchemaGenerator, self)._get_index_sql(
98+
model, field_names, False, index_name=index_name, index_type=index_type, extra=extra
99+
)
90100

91101
def _get_table_sql(self, model: "Type[Model]", safe: bool = True) -> dict:
92102
return super(OracleSchemaGenerator, self)._get_table_sql(model, False)

tortoise/contrib/postgres/indexes.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33

44
class PostgreSQLIndex(PartialIndex):
5-
INDEX_CREATE_TEMPLATE = (
6-
"CREATE INDEX {exists}{index_name} ON {table_name} USING{index_type}({fields}){extra};"
7-
)
5+
pass
86

97

108
class BloomIndex(PostgreSQLIndex):

0 commit comments

Comments
 (0)