plain.models 0.49.2__py3-none-any.whl → 0.50.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. plain/models/CHANGELOG.md +13 -0
  2. plain/models/aggregates.py +42 -19
  3. plain/models/backends/base/base.py +125 -105
  4. plain/models/backends/base/client.py +11 -3
  5. plain/models/backends/base/creation.py +22 -12
  6. plain/models/backends/base/features.py +10 -4
  7. plain/models/backends/base/introspection.py +29 -16
  8. plain/models/backends/base/operations.py +187 -91
  9. plain/models/backends/base/schema.py +267 -165
  10. plain/models/backends/base/validation.py +12 -3
  11. plain/models/backends/ddl_references.py +85 -43
  12. plain/models/backends/mysql/base.py +29 -26
  13. plain/models/backends/mysql/client.py +7 -2
  14. plain/models/backends/mysql/compiler.py +12 -3
  15. plain/models/backends/mysql/creation.py +5 -2
  16. plain/models/backends/mysql/features.py +24 -22
  17. plain/models/backends/mysql/introspection.py +22 -13
  18. plain/models/backends/mysql/operations.py +106 -39
  19. plain/models/backends/mysql/schema.py +48 -24
  20. plain/models/backends/mysql/validation.py +13 -6
  21. plain/models/backends/postgresql/base.py +41 -34
  22. plain/models/backends/postgresql/client.py +7 -2
  23. plain/models/backends/postgresql/creation.py +10 -5
  24. plain/models/backends/postgresql/introspection.py +15 -8
  25. plain/models/backends/postgresql/operations.py +109 -42
  26. plain/models/backends/postgresql/schema.py +85 -46
  27. plain/models/backends/sqlite3/_functions.py +151 -115
  28. plain/models/backends/sqlite3/base.py +37 -23
  29. plain/models/backends/sqlite3/client.py +7 -1
  30. plain/models/backends/sqlite3/creation.py +9 -5
  31. plain/models/backends/sqlite3/features.py +5 -3
  32. plain/models/backends/sqlite3/introspection.py +32 -16
  33. plain/models/backends/sqlite3/operations.py +125 -42
  34. plain/models/backends/sqlite3/schema.py +82 -58
  35. plain/models/backends/utils.py +52 -29
  36. plain/models/backups/cli.py +8 -6
  37. plain/models/backups/clients.py +16 -7
  38. plain/models/backups/core.py +24 -13
  39. plain/models/base.py +113 -74
  40. plain/models/cli.py +94 -63
  41. plain/models/config.py +1 -1
  42. plain/models/connections.py +23 -7
  43. plain/models/constraints.py +65 -47
  44. plain/models/database_url.py +1 -1
  45. plain/models/db.py +6 -2
  46. plain/models/deletion.py +66 -43
  47. plain/models/entrypoints.py +1 -1
  48. plain/models/enums.py +22 -11
  49. plain/models/exceptions.py +23 -8
  50. plain/models/expressions.py +440 -257
  51. plain/models/fields/__init__.py +253 -202
  52. plain/models/fields/json.py +120 -54
  53. plain/models/fields/mixins.py +12 -8
  54. plain/models/fields/related.py +284 -252
  55. plain/models/fields/related_descriptors.py +31 -22
  56. plain/models/fields/related_lookups.py +23 -11
  57. plain/models/fields/related_managers.py +81 -47
  58. plain/models/fields/reverse_related.py +58 -55
  59. plain/models/forms.py +89 -63
  60. plain/models/functions/comparison.py +71 -18
  61. plain/models/functions/datetime.py +79 -29
  62. plain/models/functions/math.py +43 -10
  63. plain/models/functions/mixins.py +24 -7
  64. plain/models/functions/text.py +104 -25
  65. plain/models/functions/window.py +12 -6
  66. plain/models/indexes.py +52 -28
  67. plain/models/lookups.py +228 -153
  68. plain/models/migrations/autodetector.py +86 -43
  69. plain/models/migrations/exceptions.py +7 -3
  70. plain/models/migrations/executor.py +33 -7
  71. plain/models/migrations/graph.py +79 -50
  72. plain/models/migrations/loader.py +45 -22
  73. plain/models/migrations/migration.py +23 -18
  74. plain/models/migrations/operations/base.py +37 -19
  75. plain/models/migrations/operations/fields.py +89 -42
  76. plain/models/migrations/operations/models.py +245 -143
  77. plain/models/migrations/operations/special.py +82 -25
  78. plain/models/migrations/optimizer.py +7 -2
  79. plain/models/migrations/questioner.py +58 -31
  80. plain/models/migrations/recorder.py +18 -11
  81. plain/models/migrations/serializer.py +50 -39
  82. plain/models/migrations/state.py +220 -133
  83. plain/models/migrations/utils.py +29 -13
  84. plain/models/migrations/writer.py +17 -14
  85. plain/models/options.py +63 -56
  86. plain/models/otel.py +16 -6
  87. plain/models/preflight.py +35 -12
  88. plain/models/query.py +323 -228
  89. plain/models/query_utils.py +93 -58
  90. plain/models/registry.py +34 -16
  91. plain/models/sql/compiler.py +146 -97
  92. plain/models/sql/datastructures.py +38 -25
  93. plain/models/sql/query.py +255 -169
  94. plain/models/sql/subqueries.py +32 -21
  95. plain/models/sql/where.py +54 -29
  96. plain/models/test/pytest.py +15 -11
  97. plain/models/test/utils.py +4 -2
  98. plain/models/transaction.py +20 -7
  99. plain/models/utils.py +13 -5
  100. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
  101. plain_models-0.50.0.dist-info/RECORD +122 -0
  102. plain_models-0.49.2.dist-info/RECORD +0 -122
  103. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
  104. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
  105. {plain_models-0.49.2.dist-info → plain_models-0.50.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import operator
2
4
  from functools import cached_property
3
5
 
@@ -26,19 +28,19 @@ class DatabaseFeatures(BaseDatabaseFeatures):
26
28
  supports_logical_xor = True
27
29
 
28
30
  @cached_property
29
- def minimum_database_version(self):
31
+ def minimum_database_version(self) -> tuple[int, ...]:
30
32
  if self.connection.mysql_is_mariadb:
31
33
  return (10, 4)
32
34
  else:
33
35
  return (8,)
34
36
 
35
37
  @cached_property
36
- def _mysql_storage_engine(self):
38
+ def _mysql_storage_engine(self) -> str:
37
39
  "Internal method used in Plain tests. Don't rely on this from your code"
38
40
  return self.connection.mysql_server_data["default_storage_engine"]
39
41
 
40
42
  @cached_property
41
- def allows_auto_pk_0(self):
43
+ def allows_auto_pk_0(self) -> bool:
42
44
  """
43
45
  Autoincrement primary key can be set to 0 if it doesn't generate new
44
46
  autoincrement values.
@@ -46,7 +48,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
46
48
  return "NO_AUTO_VALUE_ON_ZERO" in self.connection.sql_mode
47
49
 
48
50
  @cached_property
49
- def update_can_self_select(self):
51
+ def update_can_self_select(self) -> bool:
50
52
  return self.connection.mysql_is_mariadb and self.connection.mysql_version >= (
51
53
  10,
52
54
  3,
@@ -54,7 +56,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
54
56
  )
55
57
 
56
58
  @cached_property
57
- def can_return_columns_from_insert(self):
59
+ def can_return_columns_from_insert(self) -> bool:
58
60
  return self.connection.mysql_is_mariadb and self.connection.mysql_version >= (
59
61
  10,
60
62
  5,
@@ -66,21 +68,21 @@ class DatabaseFeatures(BaseDatabaseFeatures):
66
68
  )
67
69
 
68
70
  @cached_property
69
- def has_zoneinfo_database(self):
71
+ def has_zoneinfo_database(self) -> bool:
70
72
  return self.connection.mysql_server_data["has_zoneinfo_database"]
71
73
 
72
74
  @cached_property
73
- def is_sql_auto_is_null_enabled(self):
75
+ def is_sql_auto_is_null_enabled(self) -> bool:
74
76
  return self.connection.mysql_server_data["sql_auto_is_null"]
75
77
 
76
78
  @cached_property
77
- def supports_over_clause(self):
79
+ def supports_over_clause(self) -> bool:
78
80
  if self.connection.mysql_is_mariadb:
79
81
  return True
80
82
  return self.connection.mysql_version >= (8, 0, 2)
81
83
 
82
84
  @cached_property
83
- def supports_column_check_constraints(self):
85
+ def supports_column_check_constraints(self) -> bool:
84
86
  if self.connection.mysql_is_mariadb:
85
87
  return True
86
88
  return self.connection.mysql_version >= (8, 0, 16)
@@ -90,32 +92,32 @@ class DatabaseFeatures(BaseDatabaseFeatures):
90
92
  )
91
93
 
92
94
  @cached_property
93
- def can_introspect_check_constraints(self):
95
+ def can_introspect_check_constraints(self) -> bool:
94
96
  if self.connection.mysql_is_mariadb:
95
97
  return True
96
98
  return self.connection.mysql_version >= (8, 0, 16)
97
99
 
98
100
  @cached_property
99
- def has_select_for_update_skip_locked(self):
101
+ def has_select_for_update_skip_locked(self) -> bool:
100
102
  if self.connection.mysql_is_mariadb:
101
103
  return self.connection.mysql_version >= (10, 6)
102
104
  return self.connection.mysql_version >= (8, 0, 1)
103
105
 
104
106
  @cached_property
105
- def has_select_for_update_nowait(self):
107
+ def has_select_for_update_nowait(self) -> bool:
106
108
  if self.connection.mysql_is_mariadb:
107
109
  return True
108
110
  return self.connection.mysql_version >= (8, 0, 1)
109
111
 
110
112
  @cached_property
111
- def has_select_for_update_of(self):
113
+ def has_select_for_update_of(self) -> bool:
112
114
  return (
113
115
  not self.connection.mysql_is_mariadb
114
116
  and self.connection.mysql_version >= (8, 0, 1)
115
117
  )
116
118
 
117
119
  @cached_property
118
- def supports_explain_analyze(self):
120
+ def supports_explain_analyze(self) -> bool:
119
121
  return self.connection.mysql_is_mariadb or self.connection.mysql_version >= (
120
122
  8,
121
123
  0,
@@ -123,7 +125,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
123
125
  )
124
126
 
125
127
  @cached_property
126
- def supported_explain_formats(self):
128
+ def supported_explain_formats(self) -> set[str]:
127
129
  # Alias MySQL's TRADITIONAL to TEXT for consistency with other
128
130
  # backends.
129
131
  formats = {"JSON", "TEXT", "TRADITIONAL"}
@@ -136,7 +138,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
136
138
  return formats
137
139
 
138
140
  @cached_property
139
- def supports_transactions(self):
141
+ def supports_transactions(self) -> bool:
140
142
  """
141
143
  All storage engines except MyISAM support transactions.
142
144
  """
@@ -145,17 +147,17 @@ class DatabaseFeatures(BaseDatabaseFeatures):
145
147
  uses_savepoints = property(operator.attrgetter("supports_transactions"))
146
148
 
147
149
  @cached_property
148
- def ignores_table_name_case(self):
150
+ def ignores_table_name_case(self) -> bool:
149
151
  return self.connection.mysql_server_data["lower_case_table_names"]
150
152
 
151
153
  @cached_property
152
- def can_introspect_json_field(self):
154
+ def can_introspect_json_field(self) -> bool:
153
155
  if self.connection.mysql_is_mariadb:
154
156
  return self.can_introspect_check_constraints
155
157
  return True
156
158
 
157
159
  @cached_property
158
- def supports_index_column_ordering(self):
160
+ def supports_index_column_ordering(self) -> bool:
159
161
  if self._mysql_storage_engine != "InnoDB":
160
162
  return False
161
163
  if self.connection.mysql_is_mariadb:
@@ -163,7 +165,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
163
165
  return self.connection.mysql_version >= (8, 0, 1)
164
166
 
165
167
  @cached_property
166
- def supports_expression_indexes(self):
168
+ def supports_expression_indexes(self) -> bool:
167
169
  return (
168
170
  not self.connection.mysql_is_mariadb
169
171
  and self._mysql_storage_engine != "MyISAM"
@@ -171,7 +173,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
171
173
  )
172
174
 
173
175
  @cached_property
174
- def supports_select_intersection(self):
176
+ def supports_select_intersection(self) -> bool:
175
177
  is_mariadb = self.connection.mysql_is_mariadb
176
178
  return is_mariadb or self.connection.mysql_version >= (8, 0, 31)
177
179
 
@@ -180,7 +182,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
180
182
  )
181
183
 
182
184
  @cached_property
183
- def can_rename_index(self):
185
+ def can_rename_index(self) -> bool:
184
186
  if self.connection.mysql_is_mariadb:
185
187
  return self.connection.mysql_version >= (10, 5, 2)
186
188
  return True
@@ -1,7 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections import namedtuple
4
+ from typing import Any
2
5
 
3
6
  import sqlparse
4
- from MySQLdb.constants import FIELD_TYPE
7
+ from MySQLdb.constants import FIELD_TYPE # type: ignore[import-untyped]
5
8
 
6
9
  from plain.models.backends.base.introspection import BaseDatabaseIntrospection
7
10
  from plain.models.backends.base.introspection import FieldInfo as BaseFieldInfo
@@ -46,7 +49,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
46
49
  FIELD_TYPE.VAR_STRING: "CharField",
47
50
  }
48
51
 
49
- def get_field_type(self, data_type, description):
52
+ def get_field_type(self, data_type: Any, description: Any) -> str:
50
53
  field_type = super().get_field_type(data_type, description)
51
54
  if "auto_increment" in description.extra:
52
55
  if field_type == "BigIntegerField":
@@ -64,7 +67,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
64
67
  return "JSONField"
65
68
  return field_type
66
69
 
67
- def get_table_list(self, cursor):
70
+ def get_table_list(self, cursor: Any) -> list[TableInfo]:
68
71
  """Return a list of table and view names in the current database."""
69
72
  cursor.execute(
70
73
  """
@@ -81,12 +84,12 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
81
84
  for row in cursor.fetchall()
82
85
  ]
83
86
 
84
- def get_table_description(self, cursor, table_name):
87
+ def get_table_description(self, cursor: Any, table_name: str) -> list[FieldInfo]:
85
88
  """
86
89
  Return a description of the table with the DB-API cursor.description
87
90
  interface."
88
91
  """
89
- json_constraints = {}
92
+ json_constraints: set[Any] = set()
90
93
  if (
91
94
  self.connection.mysql_is_mariadb
92
95
  and self.connection.features.can_introspect_json_field
@@ -148,7 +151,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
148
151
  f"SELECT * FROM {self.connection.ops.quote_name(table_name)} LIMIT 1"
149
152
  )
150
153
 
151
- def to_int(i):
154
+ def to_int(i: Any) -> Any:
152
155
  return int(i) if i is not None else i
153
156
 
154
157
  fields = []
@@ -172,14 +175,16 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
172
175
  )
173
176
  return fields
174
177
 
175
- def get_sequences(self, cursor, table_name, table_fields=()):
178
+ def get_sequences(
179
+ self, cursor: Any, table_name: str, table_fields: tuple[Any, ...] = ()
180
+ ) -> list[dict[str, Any]]:
176
181
  for field_info in self.get_table_description(cursor, table_name):
177
182
  if "auto_increment" in field_info.extra:
178
183
  # MySQL allows only one auto-increment column per table.
179
184
  return [{"table": table_name, "column": field_info.name}]
180
185
  return []
181
186
 
182
- def get_relations(self, cursor, table_name):
187
+ def get_relations(self, cursor: Any, table_name: str) -> dict[str, tuple[str, str]]:
183
188
  """
184
189
  Return a dictionary of {field_name: (field_name_other_table, other_table)}
185
190
  representing all foreign keys in the given table.
@@ -200,7 +205,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
200
205
  for field_name, other_field, other_table in cursor.fetchall()
201
206
  }
202
207
 
203
- def get_storage_engine(self, cursor, table_name):
208
+ def get_storage_engine(self, cursor: Any, table_name: str) -> str:
204
209
  """
205
210
  Retrieve the storage engine for a given table. Return the default
206
211
  storage engine if the table doesn't exist.
@@ -220,8 +225,10 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
220
225
  return self.connection.features._mysql_storage_engine
221
226
  return result[0]
222
227
 
223
- def _parse_constraint_columns(self, check_clause, columns):
224
- check_columns = OrderedSet()
228
+ def _parse_constraint_columns(
229
+ self, check_clause: str, columns: set[str]
230
+ ) -> OrderedSet:
231
+ check_columns: OrderedSet = OrderedSet()
225
232
  statement = sqlparse.parse(check_clause)[0]
226
233
  tokens = (token for token in statement.flatten() if not token.is_whitespace)
227
234
  for token in tokens:
@@ -233,12 +240,14 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
233
240
  check_columns.add(token.value[1:-1])
234
241
  return check_columns
235
242
 
236
- def get_constraints(self, cursor, table_name):
243
+ def get_constraints(
244
+ self, cursor: Any, table_name: str
245
+ ) -> dict[str, dict[str, Any]]:
237
246
  """
238
247
  Retrieve any constraints or keys (unique, pk, fk, check, index) across
239
248
  one or more columns.
240
249
  """
241
- constraints = {}
250
+ constraints: dict[str, dict[str, Any]] = {}
242
251
  # Get the actual constraint names and columns
243
252
  name_query = """
244
253
  SELECT kc.`constraint_name`, kc.`column_name`,
@@ -1,4 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
1
4
  import uuid
5
+ from typing import TYPE_CHECKING, Any
2
6
 
3
7
  from plain.models.backends.base.operations import BaseDatabaseOperations
4
8
  from plain.models.backends.utils import split_tzname_delta
@@ -9,6 +13,9 @@ from plain.utils import timezone
9
13
  from plain.utils.encoding import force_str
10
14
  from plain.utils.regex_helper import _lazy_re_compile
11
15
 
16
+ if TYPE_CHECKING:
17
+ from plain.models.backends.base.base import BaseDatabaseWrapper
18
+
12
19
 
13
20
  class DatabaseOperations(BaseDatabaseOperations):
14
21
  compiler_module = "plain.models.backends.mysql.compiler"
@@ -39,7 +46,9 @@ class DatabaseOperations(BaseDatabaseOperations):
39
46
  # EXTRACT format cannot be passed in parameters.
40
47
  _extract_format_re = _lazy_re_compile(r"[A-Z_]+")
41
48
 
42
- def date_extract_sql(self, lookup_type, sql, params):
49
+ def date_extract_sql(
50
+ self, lookup_type: str, sql: str, params: list[Any] | tuple[Any, ...]
51
+ ) -> tuple[str, list[Any] | tuple[Any, ...]]:
43
52
  # https://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
44
53
  if lookup_type == "week_day":
45
54
  # DAYOFWEEK() returns an integer, 1-7, Sunday=1.
@@ -63,7 +72,13 @@ class DatabaseOperations(BaseDatabaseOperations):
63
72
  raise ValueError(f"Invalid loookup type: {lookup_type!r}")
64
73
  return f"EXTRACT({lookup_type} FROM {sql})", params
65
74
 
66
- def date_trunc_sql(self, lookup_type, sql, params, tzname=None):
75
+ def date_trunc_sql(
76
+ self,
77
+ lookup_type: str,
78
+ sql: str,
79
+ params: list[Any] | tuple[Any, ...],
80
+ tzname: str | None = None,
81
+ ) -> tuple[str, list[Any] | tuple[Any, ...]]:
67
82
  sql, params = self._convert_sql_to_tz(sql, params, tzname)
68
83
  fields = {
69
84
  "year": "%Y-01-01",
@@ -83,11 +98,13 @@ class DatabaseOperations(BaseDatabaseOperations):
83
98
  else:
84
99
  return f"DATE({sql})", params
85
100
 
86
- def _prepare_tzname_delta(self, tzname):
101
+ def _prepare_tzname_delta(self, tzname: str) -> str:
87
102
  tzname, sign, offset = split_tzname_delta(tzname)
88
103
  return f"{sign}{offset}" if offset else tzname
89
104
 
90
- def _convert_sql_to_tz(self, sql, params, tzname):
105
+ def _convert_sql_to_tz(
106
+ self, sql: str, params: list[Any] | tuple[Any, ...], tzname: str | None
107
+ ) -> tuple[str, list[Any] | tuple[Any, ...]]:
91
108
  if tzname and self.connection.timezone_name != tzname:
92
109
  return f"CONVERT_TZ({sql}, %s, %s)", (
93
110
  *params,
@@ -96,19 +113,35 @@ class DatabaseOperations(BaseDatabaseOperations):
96
113
  )
97
114
  return sql, params
98
115
 
99
- def datetime_cast_date_sql(self, sql, params, tzname):
116
+ def datetime_cast_date_sql(
117
+ self, sql: str, params: list[Any] | tuple[Any, ...], tzname: str | None
118
+ ) -> tuple[str, list[Any] | tuple[Any, ...]]:
100
119
  sql, params = self._convert_sql_to_tz(sql, params, tzname)
101
120
  return f"DATE({sql})", params
102
121
 
103
- def datetime_cast_time_sql(self, sql, params, tzname):
122
+ def datetime_cast_time_sql(
123
+ self, sql: str, params: list[Any] | tuple[Any, ...], tzname: str | None
124
+ ) -> tuple[str, list[Any] | tuple[Any, ...]]:
104
125
  sql, params = self._convert_sql_to_tz(sql, params, tzname)
105
126
  return f"TIME({sql})", params
106
127
 
107
- def datetime_extract_sql(self, lookup_type, sql, params, tzname):
128
+ def datetime_extract_sql(
129
+ self,
130
+ lookup_type: str,
131
+ sql: str,
132
+ params: list[Any] | tuple[Any, ...],
133
+ tzname: str | None,
134
+ ) -> tuple[str, list[Any] | tuple[Any, ...]]:
108
135
  sql, params = self._convert_sql_to_tz(sql, params, tzname)
109
136
  return self.date_extract_sql(lookup_type, sql, params)
110
137
 
111
- def datetime_trunc_sql(self, lookup_type, sql, params, tzname):
138
+ def datetime_trunc_sql(
139
+ self,
140
+ lookup_type: str,
141
+ sql: str,
142
+ params: list[Any] | tuple[Any, ...],
143
+ tzname: str | None,
144
+ ) -> tuple[str, list[Any] | tuple[Any, ...]]:
112
145
  sql, params = self._convert_sql_to_tz(sql, params, tzname)
113
146
  fields = ["year", "month", "day", "hour", "minute", "second"]
114
147
  format = ("%Y-", "%m", "-%d", " %H:", "%i", ":%s")
@@ -133,7 +166,13 @@ class DatabaseOperations(BaseDatabaseOperations):
133
166
  return f"CAST(DATE_FORMAT({sql}, %s) AS DATETIME)", (*params, format_str)
134
167
  return sql, params
135
168
 
136
- def time_trunc_sql(self, lookup_type, sql, params, tzname=None):
169
+ def time_trunc_sql(
170
+ self,
171
+ lookup_type: str,
172
+ sql: str,
173
+ params: list[Any] | tuple[Any, ...],
174
+ tzname: str | None = None,
175
+ ) -> tuple[str, list[Any] | tuple[Any, ...]]:
137
176
  sql, params = self._convert_sql_to_tz(sql, params, tzname)
138
177
  fields = {
139
178
  "hour": "%H:00:00",
@@ -146,17 +185,17 @@ class DatabaseOperations(BaseDatabaseOperations):
146
185
  else:
147
186
  return f"TIME({sql})", params
148
187
 
149
- def fetch_returned_insert_rows(self, cursor):
188
+ def fetch_returned_insert_rows(self, cursor: Any) -> list[Any]:
150
189
  """
151
190
  Given a cursor object that has just performed an INSERT...RETURNING
152
191
  statement into a table, return the tuple of returned data.
153
192
  """
154
193
  return cursor.fetchall()
155
194
 
156
- def format_for_duration_arithmetic(self, sql):
195
+ def format_for_duration_arithmetic(self, sql: str) -> str:
157
196
  return f"INTERVAL {sql} MICROSECOND"
158
197
 
159
- def force_no_ordering(self):
198
+ def force_no_ordering(self) -> list[tuple[None, tuple[str, list[Any], bool]]]:
160
199
  """
161
200
  "ORDER BY NULL" prevents MySQL from implicitly ordering by grouped
162
201
  columns. If no ordering would otherwise be applied, we don't want any
@@ -164,26 +203,31 @@ class DatabaseOperations(BaseDatabaseOperations):
164
203
  """
165
204
  return [(None, ("NULL", [], False))]
166
205
 
167
- def adapt_decimalfield_value(self, value, max_digits=None, decimal_places=None):
206
+ def adapt_decimalfield_value(
207
+ self,
208
+ value: Any,
209
+ max_digits: int | None = None,
210
+ decimal_places: int | None = None,
211
+ ) -> Any:
168
212
  return value
169
213
 
170
- def last_executed_query(self, cursor, sql, params):
214
+ def last_executed_query(self, cursor: Any, sql: str, params: Any) -> str | None:
171
215
  # With MySQLdb, cursor objects have an (undocumented) "_executed"
172
216
  # attribute where the exact query sent to the database is saved.
173
217
  # See MySQLdb/cursors.py in the source distribution.
174
218
  # MySQLdb returns string, PyMySQL bytes.
175
219
  return force_str(getattr(cursor, "_executed", None), errors="replace")
176
220
 
177
- def no_limit_value(self):
221
+ def no_limit_value(self) -> int:
178
222
  # 2**64 - 1, as recommended by the MySQL documentation
179
223
  return 18446744073709551615
180
224
 
181
- def quote_name(self, name):
225
+ def quote_name(self, name: str) -> str:
182
226
  if name.startswith("`") and name.endswith("`"):
183
227
  return name # Quoting once is enough.
184
228
  return f"`{name}`"
185
229
 
186
- def return_insert_columns(self, fields):
230
+ def return_insert_columns(self, fields: list[Any]) -> tuple[str, tuple[Any, ...]]:
187
231
  # MySQL and MariaDB < 10.5.0 don't support an INSERT...RETURNING
188
232
  # statement.
189
233
  if not fields:
@@ -194,7 +238,7 @@ class DatabaseOperations(BaseDatabaseOperations):
194
238
  ]
195
239
  return "RETURNING {}".format(", ".join(columns)), ()
196
240
 
197
- def validate_autopk_value(self, value):
241
+ def validate_autopk_value(self, value: int) -> int:
198
242
  # Zero in AUTO_INCREMENT field does not work without the
199
243
  # NO_AUTO_VALUE_ON_ZERO SQL mode.
200
244
  if value == 0 and not self.connection.features.allows_auto_pk_0:
@@ -203,7 +247,9 @@ class DatabaseOperations(BaseDatabaseOperations):
203
247
  )
204
248
  return value
205
249
 
206
- def adapt_datetimefield_value(self, value):
250
+ def adapt_datetimefield_value(
251
+ self, value: datetime.datetime | Any | None
252
+ ) -> str | Any | None:
207
253
  if value is None:
208
254
  return None
209
255
 
@@ -216,7 +262,9 @@ class DatabaseOperations(BaseDatabaseOperations):
216
262
  value = timezone.make_naive(value, self.connection.timezone)
217
263
  return str(value)
218
264
 
219
- def adapt_timefield_value(self, value):
265
+ def adapt_timefield_value(
266
+ self, value: datetime.time | Any | None
267
+ ) -> str | Any | None:
220
268
  if value is None:
221
269
  return None
222
270
 
@@ -225,23 +273,25 @@ class DatabaseOperations(BaseDatabaseOperations):
225
273
  return value
226
274
 
227
275
  # MySQL doesn't support tz-aware times
228
- if timezone.is_aware(value):
276
+ if timezone.is_aware(value): # type: ignore[arg-type]
229
277
  raise ValueError("MySQL backend does not support timezone-aware times.")
230
278
 
231
279
  return value.isoformat(timespec="microseconds")
232
280
 
233
- def max_name_length(self):
281
+ def max_name_length(self) -> int:
234
282
  return 64
235
283
 
236
- def pk_default_value(self):
284
+ def pk_default_value(self) -> str:
237
285
  return "NULL"
238
286
 
239
- def bulk_insert_sql(self, fields, placeholder_rows):
287
+ def bulk_insert_sql(
288
+ self, fields: list[Any], placeholder_rows: list[list[str]]
289
+ ) -> str:
240
290
  placeholder_rows_sql = (", ".join(row) for row in placeholder_rows)
241
291
  values_sql = ", ".join(f"({sql})" for sql in placeholder_rows_sql)
242
292
  return "VALUES " + values_sql
243
293
 
244
- def combine_expression(self, connector, sub_expressions):
294
+ def combine_expression(self, connector: str, sub_expressions: list[str]) -> str:
245
295
  if connector == "^":
246
296
  return "POW({})".format(",".join(sub_expressions))
247
297
  # Convert the result to a signed integer since MySQL's binary operators
@@ -254,7 +304,7 @@ class DatabaseOperations(BaseDatabaseOperations):
254
304
  return f"FLOOR({lhs} / POW(2, {rhs}))"
255
305
  return super().combine_expression(connector, sub_expressions)
256
306
 
257
- def get_db_converters(self, expression):
307
+ def get_db_converters(self, expression: Any) -> list[Any]:
258
308
  converters = super().get_db_converters(expression)
259
309
  internal_type = expression.output_field.get_internal_type()
260
310
  if internal_type == "BooleanField":
@@ -265,27 +315,38 @@ class DatabaseOperations(BaseDatabaseOperations):
265
315
  converters.append(self.convert_uuidfield_value)
266
316
  return converters
267
317
 
268
- def convert_booleanfield_value(self, value, expression, connection):
318
+ def convert_booleanfield_value(
319
+ self, value: Any, expression: Any, connection: BaseDatabaseWrapper
320
+ ) -> Any:
269
321
  if value in (0, 1):
270
322
  value = bool(value)
271
323
  return value
272
324
 
273
- def convert_datetimefield_value(self, value, expression, connection):
325
+ def convert_datetimefield_value(
326
+ self, value: Any, expression: Any, connection: BaseDatabaseWrapper
327
+ ) -> datetime.datetime | None:
274
328
  if value is not None:
275
329
  value = timezone.make_aware(value, self.connection.timezone)
276
330
  return value
277
331
 
278
- def convert_uuidfield_value(self, value, expression, connection):
332
+ def convert_uuidfield_value(
333
+ self, value: Any, expression: Any, connection: BaseDatabaseWrapper
334
+ ) -> uuid.UUID | None:
279
335
  if value is not None:
280
336
  value = uuid.UUID(value)
281
337
  return value
282
338
 
283
- def binary_placeholder_sql(self, value):
339
+ def binary_placeholder_sql(self, value: Any) -> str:
284
340
  return (
285
341
  "_binary %s" if value is not None and not hasattr(value, "as_sql") else "%s"
286
342
  )
287
343
 
288
- def subtract_temporals(self, internal_type, lhs, rhs):
344
+ def subtract_temporals(
345
+ self,
346
+ internal_type: str,
347
+ lhs: tuple[str, list[Any] | tuple[Any, ...]],
348
+ rhs: tuple[str, list[Any] | tuple[Any, ...]],
349
+ ) -> tuple[str, tuple[Any, ...]]:
289
350
  lhs_sql, lhs_params = lhs
290
351
  rhs_sql, rhs_params = rhs
291
352
  if internal_type == "TimeField":
@@ -306,7 +367,7 @@ class DatabaseOperations(BaseDatabaseOperations):
306
367
  params = (*rhs_params, *lhs_params)
307
368
  return f"TIMESTAMPDIFF(MICROSECOND, {rhs_sql}, {lhs_sql})", params
308
369
 
309
- def explain_query_prefix(self, format=None, **options):
370
+ def explain_query_prefix(self, format: str | None = None, **options: Any) -> str:
310
371
  # Alias MySQL's TRADITIONAL to TEXT for consistency with other backends.
311
372
  if format and format.upper() == "TEXT":
312
373
  format = "TRADITIONAL"
@@ -327,7 +388,7 @@ class DatabaseOperations(BaseDatabaseOperations):
327
388
  prefix += f" FORMAT={format}"
328
389
  return prefix
329
390
 
330
- def regex_lookup(self, lookup_type):
391
+ def regex_lookup(self, lookup_type: str) -> str:
331
392
  # REGEXP_LIKE doesn't exist in MariaDB.
332
393
  if self.connection.mysql_is_mariadb:
333
394
  if lookup_type == "regex":
@@ -337,12 +398,12 @@ class DatabaseOperations(BaseDatabaseOperations):
337
398
  match_option = "c" if lookup_type == "regex" else "i"
338
399
  return f"REGEXP_LIKE(%s, %s, '{match_option}')"
339
400
 
340
- def insert_statement(self, on_conflict=None):
401
+ def insert_statement(self, on_conflict: Any = None) -> str:
341
402
  if on_conflict == OnConflict.IGNORE:
342
403
  return "INSERT IGNORE INTO"
343
404
  return super().insert_statement(on_conflict=on_conflict)
344
405
 
345
- def lookup_cast(self, lookup_type, internal_type=None):
406
+ def lookup_cast(self, lookup_type: str, internal_type: str | None = None) -> str:
346
407
  lookup = "%s"
347
408
  if internal_type == "JSONField":
348
409
  if self.connection.mysql_is_mariadb or lookup_type in (
@@ -359,7 +420,7 @@ class DatabaseOperations(BaseDatabaseOperations):
359
420
  lookup = "JSON_UNQUOTE(%s)"
360
421
  return lookup
361
422
 
362
- def conditional_expression_supported_in_where_clause(self, expression):
423
+ def conditional_expression_supported_in_where_clause(self, expression: Any) -> bool:
363
424
  # MySQL ignores indexes with boolean fields unless they're compared
364
425
  # directly to a boolean value.
365
426
  if isinstance(expression, Exists | Lookup):
@@ -372,7 +433,13 @@ class DatabaseOperations(BaseDatabaseOperations):
372
433
  return False
373
434
  return super().conditional_expression_supported_in_where_clause(expression)
374
435
 
375
- def on_conflict_suffix_sql(self, fields, on_conflict, update_fields, unique_fields):
436
+ def on_conflict_suffix_sql(
437
+ self,
438
+ fields: list[Any],
439
+ on_conflict: Any,
440
+ update_fields: list[Any],
441
+ unique_fields: list[Any],
442
+ ) -> str:
376
443
  if on_conflict == OnConflict.UPDATE:
377
444
  conflict_suffix_sql = "ON DUPLICATE KEY UPDATE %(fields)s"
378
445
  # The use of VALUES() is deprecated in MySQL 8.0.20+. Instead, use
@@ -388,13 +455,13 @@ class DatabaseOperations(BaseDatabaseOperations):
388
455
  else:
389
456
  field_sql = "%(field)s = VALUE(%(field)s)"
390
457
 
391
- fields = ", ".join(
458
+ fields_str = ", ".join(
392
459
  [
393
460
  field_sql % {"field": field}
394
461
  for field in map(self.quote_name, update_fields)
395
462
  ]
396
463
  )
397
- return conflict_suffix_sql % {"fields": fields}
464
+ return conflict_suffix_sql % {"fields": fields_str}
398
465
  return super().on_conflict_suffix_sql(
399
466
  fields,
400
467
  on_conflict,