1import operator
2
3from plain.models.backends.base.features import BaseDatabaseFeatures
4from plain.utils.functional import cached_property
5
6
7class DatabaseFeatures(BaseDatabaseFeatures):
8 empty_fetchmany_value = ()
9 allows_group_by_selected_pks = True
10 related_fields_match_type = True
11 # MySQL doesn't support sliced subqueries with IN/ALL/ANY/SOME.
12 allow_sliced_subqueries_with_in = False
13 has_select_for_update = True
14 supports_forward_references = False
15 supports_regex_backreferencing = False
16 supports_date_lookup_using_string = False
17 supports_timezones = False
18 requires_explicit_null_ordering_when_grouping = True
19 atomic_transactions = False
20 can_clone_databases = True
21 supports_comments = True
22 supports_comments_inline = True
23 supports_temporal_subtraction = True
24 supports_slicing_ordering_in_compound = True
25 supports_index_on_text_field = False
26 supports_update_conflicts = True
27 create_test_procedure_without_params_sql = """
28 CREATE PROCEDURE test_procedure ()
29 BEGIN
30 DECLARE V_I INTEGER;
31 SET V_I = 1;
32 END;
33 """
34 create_test_procedure_with_int_param_sql = """
35 CREATE PROCEDURE test_procedure (P_I INTEGER)
36 BEGIN
37 DECLARE V_I INTEGER;
38 SET V_I = P_I;
39 END;
40 """
41 create_test_table_with_composite_primary_key = """
42 CREATE TABLE test_table_composite_pk (
43 column_1 INTEGER NOT NULL,
44 column_2 INTEGER NOT NULL,
45 PRIMARY KEY(column_1, column_2)
46 )
47 """
48 # Neither MySQL nor MariaDB support partial indexes.
49 supports_partial_indexes = False
50 # COLLATE must be wrapped in parentheses because MySQL treats COLLATE as an
51 # indexed expression.
52 collate_as_index_expression = True
53
54 supports_order_by_nulls_modifier = False
55 order_by_nulls_first = True
56 supports_logical_xor = True
57
58 @cached_property
59 def minimum_database_version(self):
60 if self.connection.mysql_is_mariadb:
61 return (10, 4)
62 else:
63 return (8,)
64
65 @cached_property
66 def test_collations(self):
67 charset = "utf8"
68 if (
69 self.connection.mysql_is_mariadb
70 and self.connection.mysql_version >= (10, 6)
71 ) or (
72 not self.connection.mysql_is_mariadb
73 and self.connection.mysql_version >= (8, 0, 30)
74 ):
75 # utf8 is an alias for utf8mb3 in MariaDB 10.6+ and MySQL 8.0.30+.
76 charset = "utf8mb3"
77 return {
78 "ci": f"{charset}_general_ci",
79 "non_default": f"{charset}_esperanto_ci",
80 "swedish_ci": f"{charset}_swedish_ci",
81 }
82
83 test_now_utc_template = "UTC_TIMESTAMP(6)"
84
85 @cached_property
86 def _mysql_storage_engine(self):
87 "Internal method used in Plain tests. Don't rely on this from your code"
88 return self.connection.mysql_server_data["default_storage_engine"]
89
90 @cached_property
91 def allows_auto_pk_0(self):
92 """
93 Autoincrement primary key can be set to 0 if it doesn't generate new
94 autoincrement values.
95 """
96 return "NO_AUTO_VALUE_ON_ZERO" in self.connection.sql_mode
97
98 @cached_property
99 def update_can_self_select(self):
100 return self.connection.mysql_is_mariadb and self.connection.mysql_version >= (
101 10,
102 3,
103 2,
104 )
105
106 @cached_property
107 def can_introspect_foreign_keys(self):
108 "Confirm support for introspected foreign keys"
109 return self._mysql_storage_engine != "MyISAM"
110
111 @cached_property
112 def introspected_field_types(self):
113 return {
114 **super().introspected_field_types,
115 "BinaryField": "TextField",
116 "BooleanField": "IntegerField",
117 "DurationField": "BigIntegerField",
118 "GenericIPAddressField": "CharField",
119 }
120
121 @cached_property
122 def can_return_columns_from_insert(self):
123 return self.connection.mysql_is_mariadb and self.connection.mysql_version >= (
124 10,
125 5,
126 0,
127 )
128
129 can_return_rows_from_bulk_insert = property(
130 operator.attrgetter("can_return_columns_from_insert")
131 )
132
133 @cached_property
134 def has_zoneinfo_database(self):
135 return self.connection.mysql_server_data["has_zoneinfo_database"]
136
137 @cached_property
138 def is_sql_auto_is_null_enabled(self):
139 return self.connection.mysql_server_data["sql_auto_is_null"]
140
141 @cached_property
142 def supports_over_clause(self):
143 if self.connection.mysql_is_mariadb:
144 return True
145 return self.connection.mysql_version >= (8, 0, 2)
146
147 supports_frame_range_fixed_distance = property(
148 operator.attrgetter("supports_over_clause")
149 )
150
151 @cached_property
152 def supports_column_check_constraints(self):
153 if self.connection.mysql_is_mariadb:
154 return True
155 return self.connection.mysql_version >= (8, 0, 16)
156
157 supports_table_check_constraints = property(
158 operator.attrgetter("supports_column_check_constraints")
159 )
160
161 @cached_property
162 def can_introspect_check_constraints(self):
163 if self.connection.mysql_is_mariadb:
164 return True
165 return self.connection.mysql_version >= (8, 0, 16)
166
167 @cached_property
168 def has_select_for_update_skip_locked(self):
169 if self.connection.mysql_is_mariadb:
170 return self.connection.mysql_version >= (10, 6)
171 return self.connection.mysql_version >= (8, 0, 1)
172
173 @cached_property
174 def has_select_for_update_nowait(self):
175 if self.connection.mysql_is_mariadb:
176 return True
177 return self.connection.mysql_version >= (8, 0, 1)
178
179 @cached_property
180 def has_select_for_update_of(self):
181 return (
182 not self.connection.mysql_is_mariadb
183 and self.connection.mysql_version >= (8, 0, 1)
184 )
185
186 @cached_property
187 def supports_explain_analyze(self):
188 return self.connection.mysql_is_mariadb or self.connection.mysql_version >= (
189 8,
190 0,
191 18,
192 )
193
194 @cached_property
195 def supported_explain_formats(self):
196 # Alias MySQL's TRADITIONAL to TEXT for consistency with other
197 # backends.
198 formats = {"JSON", "TEXT", "TRADITIONAL"}
199 if not self.connection.mysql_is_mariadb and self.connection.mysql_version >= (
200 8,
201 0,
202 16,
203 ):
204 formats.add("TREE")
205 return formats
206
207 @cached_property
208 def supports_transactions(self):
209 """
210 All storage engines except MyISAM support transactions.
211 """
212 return self._mysql_storage_engine != "MyISAM"
213
214 uses_savepoints = property(operator.attrgetter("supports_transactions"))
215 can_release_savepoints = property(operator.attrgetter("supports_transactions"))
216
217 @cached_property
218 def ignores_table_name_case(self):
219 return self.connection.mysql_server_data["lower_case_table_names"]
220
221 @cached_property
222 def supports_default_in_lead_lag(self):
223 # To be added in https://jira.mariadb.org/browse/MDEV-12981.
224 return not self.connection.mysql_is_mariadb
225
226 @cached_property
227 def can_introspect_json_field(self):
228 if self.connection.mysql_is_mariadb:
229 return self.can_introspect_check_constraints
230 return True
231
232 @cached_property
233 def supports_index_column_ordering(self):
234 if self._mysql_storage_engine != "InnoDB":
235 return False
236 if self.connection.mysql_is_mariadb:
237 return self.connection.mysql_version >= (10, 8)
238 return self.connection.mysql_version >= (8, 0, 1)
239
240 @cached_property
241 def supports_expression_indexes(self):
242 return (
243 not self.connection.mysql_is_mariadb
244 and self._mysql_storage_engine != "MyISAM"
245 and self.connection.mysql_version >= (8, 0, 13)
246 )
247
248 @cached_property
249 def supports_select_intersection(self):
250 is_mariadb = self.connection.mysql_is_mariadb
251 return is_mariadb or self.connection.mysql_version >= (8, 0, 31)
252
253 supports_select_difference = property(
254 operator.attrgetter("supports_select_intersection")
255 )
256
257 @cached_property
258 def can_rename_index(self):
259 if self.connection.mysql_is_mariadb:
260 return self.connection.mysql_version >= (10, 5, 2)
261 return True