Plain is headed towards 1.0! Subscribe for development updates →

  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