plain.models 0.49.1__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.
- plain/models/CHANGELOG.md +23 -0
- plain/models/aggregates.py +42 -19
- plain/models/backends/base/base.py +125 -105
- plain/models/backends/base/client.py +11 -3
- plain/models/backends/base/creation.py +22 -12
- plain/models/backends/base/features.py +10 -4
- plain/models/backends/base/introspection.py +29 -16
- plain/models/backends/base/operations.py +187 -91
- plain/models/backends/base/schema.py +267 -165
- plain/models/backends/base/validation.py +12 -3
- plain/models/backends/ddl_references.py +85 -43
- plain/models/backends/mysql/base.py +29 -26
- plain/models/backends/mysql/client.py +7 -2
- plain/models/backends/mysql/compiler.py +12 -3
- plain/models/backends/mysql/creation.py +5 -2
- plain/models/backends/mysql/features.py +24 -22
- plain/models/backends/mysql/introspection.py +22 -13
- plain/models/backends/mysql/operations.py +106 -39
- plain/models/backends/mysql/schema.py +48 -24
- plain/models/backends/mysql/validation.py +13 -6
- plain/models/backends/postgresql/base.py +41 -34
- plain/models/backends/postgresql/client.py +7 -2
- plain/models/backends/postgresql/creation.py +10 -5
- plain/models/backends/postgresql/introspection.py +15 -8
- plain/models/backends/postgresql/operations.py +109 -42
- plain/models/backends/postgresql/schema.py +85 -46
- plain/models/backends/sqlite3/_functions.py +151 -115
- plain/models/backends/sqlite3/base.py +37 -23
- plain/models/backends/sqlite3/client.py +7 -1
- plain/models/backends/sqlite3/creation.py +9 -5
- plain/models/backends/sqlite3/features.py +5 -3
- plain/models/backends/sqlite3/introspection.py +32 -16
- plain/models/backends/sqlite3/operations.py +125 -42
- plain/models/backends/sqlite3/schema.py +82 -58
- plain/models/backends/utils.py +52 -29
- plain/models/backups/cli.py +8 -6
- plain/models/backups/clients.py +16 -7
- plain/models/backups/core.py +24 -13
- plain/models/base.py +113 -74
- plain/models/cli.py +94 -63
- plain/models/config.py +1 -1
- plain/models/connections.py +23 -7
- plain/models/constraints.py +65 -47
- plain/models/database_url.py +1 -1
- plain/models/db.py +6 -2
- plain/models/deletion.py +66 -43
- plain/models/entrypoints.py +1 -1
- plain/models/enums.py +22 -11
- plain/models/exceptions.py +23 -8
- plain/models/expressions.py +440 -257
- plain/models/fields/__init__.py +253 -202
- plain/models/fields/json.py +120 -54
- plain/models/fields/mixins.py +12 -8
- plain/models/fields/related.py +284 -252
- plain/models/fields/related_descriptors.py +34 -25
- plain/models/fields/related_lookups.py +23 -11
- plain/models/fields/related_managers.py +81 -47
- plain/models/fields/reverse_related.py +58 -55
- plain/models/forms.py +89 -63
- plain/models/functions/comparison.py +71 -18
- plain/models/functions/datetime.py +79 -29
- plain/models/functions/math.py +43 -10
- plain/models/functions/mixins.py +24 -7
- plain/models/functions/text.py +104 -25
- plain/models/functions/window.py +12 -6
- plain/models/indexes.py +52 -28
- plain/models/lookups.py +228 -153
- plain/models/migrations/autodetector.py +86 -43
- plain/models/migrations/exceptions.py +7 -3
- plain/models/migrations/executor.py +33 -7
- plain/models/migrations/graph.py +79 -50
- plain/models/migrations/loader.py +45 -22
- plain/models/migrations/migration.py +23 -18
- plain/models/migrations/operations/base.py +37 -19
- plain/models/migrations/operations/fields.py +89 -42
- plain/models/migrations/operations/models.py +245 -143
- plain/models/migrations/operations/special.py +82 -25
- plain/models/migrations/optimizer.py +7 -2
- plain/models/migrations/questioner.py +58 -31
- plain/models/migrations/recorder.py +18 -11
- plain/models/migrations/serializer.py +50 -39
- plain/models/migrations/state.py +220 -133
- plain/models/migrations/utils.py +29 -13
- plain/models/migrations/writer.py +17 -14
- plain/models/options.py +63 -56
- plain/models/otel.py +16 -6
- plain/models/preflight.py +35 -12
- plain/models/query.py +323 -228
- plain/models/query_utils.py +93 -58
- plain/models/registry.py +34 -16
- plain/models/sql/compiler.py +146 -97
- plain/models/sql/datastructures.py +38 -25
- plain/models/sql/query.py +255 -169
- plain/models/sql/subqueries.py +32 -21
- plain/models/sql/where.py +54 -29
- plain/models/test/pytest.py +15 -11
- plain/models/test/utils.py +4 -2
- plain/models/transaction.py +20 -7
- plain/models/utils.py +13 -5
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/METADATA +1 -1
- plain_models-0.50.0.dist-info/RECORD +122 -0
- plain_models-0.49.1.dist-info/RECORD +0 -122
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/WHEEL +0 -0
- {plain_models-0.49.1.dist-info → plain_models-0.50.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.49.1.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(
|
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(
|
224
|
-
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
-
|
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":
|
464
|
+
return conflict_suffix_sql % {"fields": fields_str}
|
398
465
|
return super().on_conflict_suffix_sql(
|
399
466
|
fields,
|
400
467
|
on_conflict,
|