1from __future__ import annotations
  2
  3import datetime
  4import decimal
  5import json
  6from importlib import import_module
  7from typing import TYPE_CHECKING, Any
  8
  9import sqlparse
 10
 11from plain.models.backends import utils
 12from plain.models.db import NotSupportedError
 13from plain.utils import timezone
 14from plain.utils.encoding import force_str
 15
 16if TYPE_CHECKING:
 17    from types import ModuleType
 18
 19    from plain.models.backends.base.base import BaseDatabaseWrapper
 20    from plain.models.fields import Field
 21
 22
 23class BaseDatabaseOperations:
 24    """
 25    Encapsulate backend-specific differences, such as the way a backend
 26    performs ordering or calculates the ID of a recently-inserted row.
 27    """
 28
 29    compiler_module: str = "plain.models.sql.compiler"
 30
 31    # Integer field safe ranges by `internal_type` as documented
 32    # in docs/ref/models/fields.txt.
 33    integer_field_ranges: dict[str, tuple[int, int]] = {
 34        "SmallIntegerField": (-32768, 32767),
 35        "IntegerField": (-2147483648, 2147483647),
 36        "BigIntegerField": (-9223372036854775808, 9223372036854775807),
 37        "PositiveBigIntegerField": (0, 9223372036854775807),
 38        "PositiveSmallIntegerField": (0, 32767),
 39        "PositiveIntegerField": (0, 2147483647),
 40        "PrimaryKeyField": (-9223372036854775808, 9223372036854775807),
 41    }
 42    set_operators: dict[str, str] = {
 43        "union": "UNION",
 44        "intersection": "INTERSECT",
 45        "difference": "EXCEPT",
 46    }
 47    # Mapping of Field.get_internal_type() (typically the model field's class
 48    # name) to the data type to use for the Cast() function, if different from
 49    # DatabaseWrapper.data_types.
 50    cast_data_types: dict[str, str] = {}
 51    # CharField data type if the max_length argument isn't provided.
 52    cast_char_field_without_max_length: str | None = None
 53
 54    # Start and end points for window expressions.
 55    PRECEDING: str = "PRECEDING"
 56    FOLLOWING: str = "FOLLOWING"
 57    UNBOUNDED_PRECEDING: str = "UNBOUNDED " + PRECEDING
 58    UNBOUNDED_FOLLOWING: str = "UNBOUNDED " + FOLLOWING
 59    CURRENT_ROW: str = "CURRENT ROW"
 60
 61    # Prefix for EXPLAIN queries, or None EXPLAIN isn't supported.
 62    explain_prefix: str | None = None
 63
 64    def __init__(self, connection: BaseDatabaseWrapper):
 65        self.connection = connection
 66        self._cache: ModuleType | None = None
 67
 68    def autoinc_sql(self, table: str, column: str) -> str | None:
 69        """
 70        Return any SQL needed to support auto-incrementing primary keys, or
 71        None if no SQL is necessary.
 72
 73        This SQL is executed when a table is created.
 74        """
 75        return None
 76
 77    def bulk_batch_size(self, fields: list[Field], objs: list[Any]) -> int:
 78        """
 79        Return the maximum allowed batch size for the backend. The fields
 80        are the fields going to be inserted in the batch, the objs contains
 81        all the objects to be inserted.
 82        """
 83        return len(objs)
 84
 85    def format_for_duration_arithmetic(self, sql: str) -> str:
 86        raise NotImplementedError(
 87            "subclasses of BaseDatabaseOperations may require a "
 88            "format_for_duration_arithmetic() method."
 89        )
 90
 91    def unification_cast_sql(self, output_field: Field) -> str:
 92        """
 93        Given a field instance, return the SQL that casts the result of a union
 94        to that type. The resulting string should contain a '%s' placeholder
 95        for the expression being cast.
 96        """
 97        return "%s"
 98
 99    def date_extract_sql(
100        self, lookup_type: str, sql: str, params: list[Any] | tuple[Any, ...]
101    ) -> tuple[str, list[Any] | tuple[Any, ...]]:
102        """
103        Given a lookup_type of 'year', 'month', or 'day', return the SQL that
104        extracts a value from the given date field field_name.
105        """
106        raise NotImplementedError(
107            "subclasses of BaseDatabaseOperations may require a date_extract_sql() "
108            "method"
109        )
110
111    def date_trunc_sql(
112        self,
113        lookup_type: str,
114        sql: str,
115        params: list[Any] | tuple[Any, ...],
116        tzname: str | None = None,
117    ) -> tuple[str, list[Any] | tuple[Any, ...]]:
118        """
119        Given a lookup_type of 'year', 'month', or 'day', return the SQL that
120        truncates the given date or datetime field field_name to a date object
121        with only the given specificity.
122
123        If `tzname` is provided, the given value is truncated in a specific
124        timezone.
125        """
126        raise NotImplementedError(
127            "subclasses of BaseDatabaseOperations may require a date_trunc_sql() "
128            "method."
129        )
130
131    def datetime_cast_date_sql(
132        self, sql: str, params: list[Any] | tuple[Any, ...], tzname: str | None
133    ) -> tuple[str, list[Any] | tuple[Any, ...]]:
134        """
135        Return the SQL to cast a datetime value to date value.
136        """
137        raise NotImplementedError(
138            "subclasses of BaseDatabaseOperations may require a "
139            "datetime_cast_date_sql() method."
140        )
141
142    def datetime_cast_time_sql(
143        self, sql: str, params: list[Any] | tuple[Any, ...], tzname: str | None
144    ) -> tuple[str, list[Any] | tuple[Any, ...]]:
145        """
146        Return the SQL to cast a datetime value to time value.
147        """
148        raise NotImplementedError(
149            "subclasses of BaseDatabaseOperations may require a "
150            "datetime_cast_time_sql() method"
151        )
152
153    def datetime_extract_sql(
154        self,
155        lookup_type: str,
156        sql: str,
157        params: list[Any] | tuple[Any, ...],
158        tzname: str | None,
159    ) -> tuple[str, list[Any] | tuple[Any, ...]]:
160        """
161        Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute', or
162        'second', return the SQL that extracts a value from the given
163        datetime field field_name.
164        """
165        raise NotImplementedError(
166            "subclasses of BaseDatabaseOperations may require a datetime_extract_sql() "
167            "method"
168        )
169
170    def datetime_trunc_sql(
171        self,
172        lookup_type: str,
173        sql: str,
174        params: list[Any] | tuple[Any, ...],
175        tzname: str | None,
176    ) -> tuple[str, list[Any] | tuple[Any, ...]]:
177        """
178        Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute', or
179        'second', return the SQL that truncates the given datetime field
180        field_name to a datetime object with only the given specificity.
181        """
182        raise NotImplementedError(
183            "subclasses of BaseDatabaseOperations may require a datetime_trunc_sql() "
184            "method"
185        )
186
187    def time_trunc_sql(
188        self,
189        lookup_type: str,
190        sql: str,
191        params: list[Any] | tuple[Any, ...],
192        tzname: str | None = None,
193    ) -> tuple[str, list[Any] | tuple[Any, ...]]:
194        """
195        Given a lookup_type of 'hour', 'minute' or 'second', return the SQL
196        that truncates the given time or datetime field field_name to a time
197        object with only the given specificity.
198
199        If `tzname` is provided, the given value is truncated in a specific
200        timezone.
201        """
202        raise NotImplementedError(
203            "subclasses of BaseDatabaseOperations may require a time_trunc_sql() method"
204        )
205
206    def time_extract_sql(
207        self, lookup_type: str, sql: str, params: list[Any] | tuple[Any, ...]
208    ) -> tuple[str, list[Any] | tuple[Any, ...]]:
209        """
210        Given a lookup_type of 'hour', 'minute', or 'second', return the SQL
211        that extracts a value from the given time field field_name.
212        """
213        return self.date_extract_sql(lookup_type, sql, params)
214
215    def deferrable_sql(self) -> str:
216        """
217        Return the SQL to make a constraint "initially deferred" during a
218        CREATE TABLE statement.
219        """
220        return ""
221
222    def distinct_sql(
223        self, fields: list[str], params: list[Any] | tuple[Any, ...]
224    ) -> tuple[list[str], list[Any]]:
225        """
226        Return an SQL DISTINCT clause which removes duplicate rows from the
227        result set. If any fields are given, only check the given fields for
228        duplicates.
229        """
230        if fields:
231            raise NotSupportedError(
232                "DISTINCT ON fields is not supported by this database backend"
233            )
234        else:
235            return ["DISTINCT"], []
236
237    def fetch_returned_insert_columns(self, cursor: Any, returning_params: Any) -> Any:
238        """
239        Given a cursor object that has just performed an INSERT...RETURNING
240        statement into a table, return the newly created data.
241        """
242        return cursor.fetchone()
243
244    def field_cast_sql(self, db_type: str | None, internal_type: str) -> str:
245        """
246        Given a column type (e.g. 'BLOB', 'VARCHAR') and an internal type
247        (e.g. 'GenericIPAddressField'), return the SQL to cast it before using
248        it in a WHERE statement. The resulting string should contain a '%s'
249        placeholder for the column being searched against.
250        """
251        return "%s"
252
253    def force_no_ordering(self) -> list[str]:
254        """
255        Return a list used in the "ORDER BY" clause to force no ordering at
256        all. Return an empty list to include nothing in the ordering.
257        """
258        return []
259
260    def for_update_sql(
261        self,
262        nowait: bool = False,
263        skip_locked: bool = False,
264        of: tuple[str, ...] = (),
265        no_key: bool = False,
266    ) -> str:
267        """
268        Return the FOR UPDATE SQL clause to lock rows for an update operation.
269        """
270        return "FOR{} UPDATE{}{}{}".format(
271            " NO KEY" if no_key else "",
272            " OF {}".format(", ".join(of)) if of else "",
273            " NOWAIT" if nowait else "",
274            " SKIP LOCKED" if skip_locked else "",
275        )
276
277    def _get_limit_offset_params(
278        self, low_mark: int | None, high_mark: int | None
279    ) -> tuple[int | None, int]:
280        offset = low_mark or 0
281        if high_mark is not None:
282            return (high_mark - offset), offset
283        elif offset:
284            return self.connection.ops.no_limit_value(), offset
285        return None, offset
286
287    def limit_offset_sql(self, low_mark: int | None, high_mark: int | None) -> str:
288        """Return LIMIT/OFFSET SQL clause."""
289        limit, offset = self._get_limit_offset_params(low_mark, high_mark)
290        return " ".join(
291            sql
292            for sql in (
293                ("LIMIT %d" % limit) if limit else None,  # noqa: UP031
294                ("OFFSET %d" % offset) if offset else None,  # noqa: UP031
295            )
296            if sql
297        )
298
299    def last_executed_query(
300        self,
301        cursor: Any,
302        sql: str,
303        params: list[Any] | tuple[Any, ...] | dict[str, Any] | None,
304    ) -> str:
305        """
306        Return a string of the query last executed by the given cursor, with
307        placeholders replaced with actual values.
308
309        `sql` is the raw query containing placeholders and `params` is the
310        sequence of parameters. These are used by default, but this method
311        exists for database backends to provide a better implementation
312        according to their own quoting schemes.
313        """
314
315        # Convert params to contain string values.
316        def to_string(s: Any) -> str:
317            return force_str(s, strings_only=True, errors="replace")
318
319        u_params: tuple[str, ...] | dict[str, str]
320        if isinstance(params, (list, tuple)):  # noqa: UP038
321            u_params = tuple(to_string(val) for val in params)
322        elif params is None:
323            u_params = ()
324        else:
325            u_params = {to_string(k): to_string(v) for k, v in params.items()}  # type: ignore[union-attr]
326
327        return f"QUERY = {sql!r} - PARAMS = {u_params!r}"
328
329    def last_insert_id(self, cursor: Any, table_name: str, pk_name: str) -> int:
330        """
331        Given a cursor object that has just performed an INSERT statement into
332        a table that has an auto-incrementing ID, return the newly created ID.
333
334        `pk_name` is the name of the primary-key column.
335        """
336        return cursor.lastrowid
337
338    def lookup_cast(self, lookup_type: str, internal_type: str | None = None) -> str:
339        """
340        Return the string to use in a query when performing lookups
341        ("contains", "like", etc.). It should contain a '%s' placeholder for
342        the column being searched against.
343        """
344        return "%s"
345
346    def max_in_list_size(self) -> int | None:
347        """
348        Return the maximum number of items that can be passed in a single 'IN'
349        list condition, or None if the backend does not impose a limit.
350        """
351        return None
352
353    def max_name_length(self) -> int | None:
354        """
355        Return the maximum length of table and column names, or None if there
356        is no limit.
357        """
358        return None
359
360    def no_limit_value(self) -> int | None:
361        """
362        Return the value to use for the LIMIT when we are wanting "LIMIT
363        infinity". Return None if the limit clause can be omitted in this case.
364        """
365        raise NotImplementedError(
366            "subclasses of BaseDatabaseOperations may require a no_limit_value() method"
367        )
368
369    def pk_default_value(self) -> str:
370        """
371        Return the value to use during an INSERT statement to specify that
372        the field should use its default value.
373        """
374        return "DEFAULT"
375
376    def prepare_sql_script(self, sql: str) -> list[str]:
377        """
378        Take an SQL script that may contain multiple lines and return a list
379        of statements to feed to successive cursor.execute() calls.
380
381        Since few databases are able to process raw SQL scripts in a single
382        cursor.execute() call and PEP 249 doesn't talk about this use case,
383        the default implementation is conservative.
384        """
385        return [
386            sqlparse.format(statement, strip_comments=True)
387            for statement in sqlparse.split(sql)
388            if statement
389        ]
390
391    def return_insert_columns(
392        self, fields: list[Field]
393    ) -> tuple[str, list[Any]] | None:
394        """
395        For backends that support returning columns as part of an insert query,
396        return the SQL and params to append to the INSERT query. The returned
397        fragment should contain a format string to hold the appropriate column.
398        """
399        return None
400
401    def compiler(self, compiler_name: str) -> type[Any]:
402        """
403        Return the SQLCompiler class corresponding to the given name,
404        in the namespace corresponding to the `compiler_module` attribute
405        on this backend.
406        """
407        if self._cache is None:
408            self._cache = import_module(self.compiler_module)
409        return getattr(self._cache, compiler_name)
410
411    def quote_name(self, name: str) -> str:
412        """
413        Return a quoted version of the given table, index, or column name. Do
414        not quote the given name if it's already been quoted.
415        """
416        raise NotImplementedError(
417            "subclasses of BaseDatabaseOperations may require a quote_name() method"
418        )
419
420    def regex_lookup(self, lookup_type: str) -> str:
421        """
422        Return the string to use in a query when performing regular expression
423        lookups (using "regex" or "iregex"). It should contain a '%s'
424        placeholder for the column being searched against.
425
426        If the feature is not supported (or part of it is not supported), raise
427        NotImplementedError.
428        """
429        raise NotImplementedError(
430            "subclasses of BaseDatabaseOperations may require a regex_lookup() method"
431        )
432
433    def savepoint_create_sql(self, sid: str) -> str:
434        """
435        Return the SQL for starting a new savepoint. Only required if the
436        "uses_savepoints" feature is True. The "sid" parameter is a string
437        for the savepoint id.
438        """
439        return f"SAVEPOINT {self.quote_name(sid)}"
440
441    def savepoint_commit_sql(self, sid: str) -> str:
442        """
443        Return the SQL for committing the given savepoint.
444        """
445        return f"RELEASE SAVEPOINT {self.quote_name(sid)}"
446
447    def savepoint_rollback_sql(self, sid: str) -> str:
448        """
449        Return the SQL for rolling back the given savepoint.
450        """
451        return f"ROLLBACK TO SAVEPOINT {self.quote_name(sid)}"
452
453    def set_time_zone_sql(self) -> str:
454        """
455        Return the SQL that will set the connection's time zone.
456
457        Return '' if the backend doesn't support time zones.
458        """
459        return ""
460
461    def prep_for_like_query(self, x: str) -> str:
462        """Prepare a value for use in a LIKE query."""
463        return str(x).replace("\\", "\\\\").replace("%", r"\%").replace("_", r"\_")
464
465    # Same as prep_for_like_query(), but called for "iexact" matches, which
466    # need not necessarily be implemented using "LIKE" in the backend.
467    prep_for_iexact_query = prep_for_like_query
468
469    def validate_autopk_value(self, value: int) -> int:
470        """
471        Certain backends do not accept some values for "serial" fields
472        (for example zero in MySQL). Raise a ValueError if the value is
473        invalid, otherwise return the validated value.
474        """
475        return value
476
477    def adapt_unknown_value(self, value: Any) -> Any:
478        """
479        Transform a value to something compatible with the backend driver.
480
481        This method only depends on the type of the value. It's designed for
482        cases where the target type isn't known, such as .raw() SQL queries.
483        As a consequence it may not work perfectly in all circumstances.
484        """
485        if isinstance(value, datetime.datetime):  # must be before date
486            return self.adapt_datetimefield_value(value)
487        elif isinstance(value, datetime.date):
488            return self.adapt_datefield_value(value)
489        elif isinstance(value, datetime.time):
490            return self.adapt_timefield_value(value)
491        elif isinstance(value, decimal.Decimal):
492            return self.adapt_decimalfield_value(value)
493        else:
494            return value
495
496    def adapt_integerfield_value(
497        self, value: int | None, internal_type: str
498    ) -> int | None:
499        return value
500
501    def adapt_datefield_value(self, value: datetime.date | None) -> str | None:
502        """
503        Transform a date value to an object compatible with what is expected
504        by the backend driver for date columns.
505        """
506        if value is None:
507            return None
508        return str(value)
509
510    def adapt_datetimefield_value(
511        self, value: datetime.datetime | Any | None
512    ) -> str | Any | None:
513        """
514        Transform a datetime value to an object compatible with what is expected
515        by the backend driver for datetime columns.
516        """
517        if value is None:
518            return None
519        # Expression values are adapted by the database.
520        if hasattr(value, "resolve_expression"):
521            return value
522
523        return str(value)
524
525    def adapt_timefield_value(
526        self, value: datetime.time | Any | None
527    ) -> str | Any | None:
528        """
529        Transform a time value to an object compatible with what is expected
530        by the backend driver for time columns.
531        """
532        if value is None:
533            return None
534        # Expression values are adapted by the database.
535        if hasattr(value, "resolve_expression"):
536            return value
537
538        if timezone.is_aware(value):  # type: ignore[arg-type]
539            raise ValueError("Plain does not support timezone-aware times.")
540        return str(value)
541
542    def adapt_decimalfield_value(
543        self,
544        value: decimal.Decimal | None,
545        max_digits: int | None = None,
546        decimal_places: int | None = None,
547    ) -> str | None:
548        """
549        Transform a decimal.Decimal value to an object compatible with what is
550        expected by the backend driver for decimal (numeric) columns.
551        """
552        return utils.format_number(value, max_digits, decimal_places)
553
554    def adapt_ipaddressfield_value(self, value: str | None) -> str | None:
555        """
556        Transform a string representation of an IP address into the expected
557        type for the backend driver.
558        """
559        return value or None
560
561    def adapt_json_value(self, value: Any, encoder: type[json.JSONEncoder]) -> str:
562        return json.dumps(value, cls=encoder)
563
564    def year_lookup_bounds_for_date_field(
565        self, value: int, iso_year: bool = False
566    ) -> list[str | None]:
567        """
568        Return a two-elements list with the lower and upper bound to be used
569        with a BETWEEN operator to query a DateField value using a year
570        lookup.
571
572        `value` is an int, containing the looked-up year.
573        If `iso_year` is True, return bounds for ISO-8601 week-numbering years.
574        """
575        if iso_year:
576            first = datetime.date.fromisocalendar(value, 1, 1)
577            second = datetime.date.fromisocalendar(
578                value + 1, 1, 1
579            ) - datetime.timedelta(days=1)
580        else:
581            first = datetime.date(value, 1, 1)
582            second = datetime.date(value, 12, 31)
583        first_adapted = self.adapt_datefield_value(first)
584        second_adapted = self.adapt_datefield_value(second)
585        return [first_adapted, second_adapted]
586
587    def year_lookup_bounds_for_datetime_field(
588        self, value: int, iso_year: bool = False
589    ) -> list[str | Any | None]:
590        """
591        Return a two-elements list with the lower and upper bound to be used
592        with a BETWEEN operator to query a DateTimeField value using a year
593        lookup.
594
595        `value` is an int, containing the looked-up year.
596        If `iso_year` is True, return bounds for ISO-8601 week-numbering years.
597        """
598        if iso_year:
599            first = datetime.datetime.fromisocalendar(value, 1, 1)
600            second = datetime.datetime.fromisocalendar(
601                value + 1, 1, 1
602            ) - datetime.timedelta(microseconds=1)
603        else:
604            first = datetime.datetime(value, 1, 1)
605            second = datetime.datetime(value, 12, 31, 23, 59, 59, 999999)
606
607        # Make sure that datetimes are aware in the current timezone
608        tz = timezone.get_current_timezone()
609        first = timezone.make_aware(first, tz)
610        second = timezone.make_aware(second, tz)
611
612        first_adapted = self.adapt_datetimefield_value(first)
613        second_adapted = self.adapt_datetimefield_value(second)
614        return [first_adapted, second_adapted]
615
616    def get_db_converters(self, expression: Any) -> list[Any]:
617        """
618        Return a list of functions needed to convert field data.
619
620        Some field types on some backends do not provide data in the correct
621        format, this is the hook for converter functions.
622        """
623        return []
624
625    def convert_durationfield_value(
626        self, value: int | None, expression: Any, connection: BaseDatabaseWrapper
627    ) -> datetime.timedelta | None:
628        if value is not None:
629            return datetime.timedelta(0, 0, value)
630        return None
631
632    def check_expression_support(self, expression: Any) -> None:
633        """
634        Check that the backend supports the provided expression.
635
636        This is used on specific backends to rule out known expressions
637        that have problematic or nonexistent implementations. If the
638        expression has a known problem, the backend should raise
639        NotSupportedError.
640        """
641        return None
642
643    def conditional_expression_supported_in_where_clause(self, expression: Any) -> bool:
644        """
645        Return True, if the conditional expression is supported in the WHERE
646        clause.
647        """
648        return True
649
650    def combine_expression(self, connector: str, sub_expressions: list[str]) -> str:
651        """
652        Combine a list of subexpressions into a single expression, using
653        the provided connecting operator. This is required because operators
654        can vary between backends (e.g., Oracle with %% and &) and between
655        subexpression types (e.g., date expressions).
656        """
657        conn = f" {connector} "
658        return conn.join(sub_expressions)
659
660    def combine_duration_expression(
661        self, connector: str, sub_expressions: list[str]
662    ) -> str:
663        return self.combine_expression(connector, sub_expressions)
664
665    def binary_placeholder_sql(self, value: Any) -> str:
666        """
667        Some backends require special syntax to insert binary content (MySQL
668        for example uses '_binary %s').
669        """
670        return "%s"
671
672    def modify_insert_params(
673        self, placeholder: str, params: list[Any] | tuple[Any, ...]
674    ) -> list[Any] | tuple[Any, ...]:
675        """
676        Allow modification of insert parameters. Needed for Oracle Spatial
677        backend due to #10888.
678        """
679        return params
680
681    def integer_field_range(self, internal_type: str) -> tuple[int, int]:
682        """
683        Given an integer field internal type (e.g. 'PositiveIntegerField'),
684        return a tuple of the (min_value, max_value) form representing the
685        range of the column type bound to the field.
686        """
687        return self.integer_field_ranges[internal_type]
688
689    def subtract_temporals(
690        self,
691        internal_type: str,
692        lhs: tuple[str, list[Any] | tuple[Any, ...]],
693        rhs: tuple[str, list[Any] | tuple[Any, ...]],
694    ) -> tuple[str, tuple[Any, ...]]:
695        if self.connection.features.supports_temporal_subtraction:
696            lhs_sql, lhs_params = lhs
697            rhs_sql, rhs_params = rhs
698            return f"({lhs_sql} - {rhs_sql})", (*lhs_params, *rhs_params)
699        raise NotSupportedError(
700            f"This backend does not support {internal_type} subtraction."
701        )
702
703    def window_frame_start(self, start: int | None) -> str:
704        if isinstance(start, int):
705            if start < 0:
706                return "%d %s" % (abs(start), self.PRECEDING)  # noqa: UP031
707            elif start == 0:
708                return self.CURRENT_ROW
709        elif start is None:
710            return self.UNBOUNDED_PRECEDING
711        raise ValueError(
712            f"start argument must be a negative integer, zero, or None, but got '{start}'."
713        )
714
715    def window_frame_end(self, end: int | None) -> str:
716        if isinstance(end, int):
717            if end == 0:
718                return self.CURRENT_ROW
719            elif end > 0:
720                return "%d %s" % (end, self.FOLLOWING)  # noqa: UP031
721        elif end is None:
722            return self.UNBOUNDED_FOLLOWING
723        raise ValueError(
724            f"end argument must be a positive integer, zero, or None, but got '{end}'."
725        )
726
727    def window_frame_rows_start_end(
728        self, start: int | None = None, end: int | None = None
729    ) -> tuple[str, str]:
730        """
731        Return SQL for start and end points in an OVER clause window frame.
732        """
733        if not self.connection.features.supports_over_clause:
734            raise NotSupportedError("This backend does not support window expressions.")
735        return self.window_frame_start(start), self.window_frame_end(end)
736
737    def window_frame_range_start_end(
738        self, start: int | None = None, end: int | None = None
739    ) -> tuple[str, str]:
740        start_, end_ = self.window_frame_rows_start_end(start, end)
741        features = self.connection.features
742        if features.only_supports_unbounded_with_preceding_and_following and (
743            (start and start < 0) or (end and end > 0)
744        ):
745            raise NotSupportedError(
746                f"{self.connection.display_name} only supports UNBOUNDED together with PRECEDING and "
747                "FOLLOWING."
748            )
749        return start_, end_
750
751    def explain_query_prefix(self, format: str | None = None, **options: Any) -> str:
752        if not self.connection.features.supports_explaining_query_execution:
753            raise NotSupportedError(
754                "This backend does not support explaining query execution."
755            )
756        if format:
757            supported_formats = self.connection.features.supported_explain_formats
758            normalized_format = format.upper()
759            if normalized_format not in supported_formats:
760                msg = f"{normalized_format} is not a recognized format."
761                if supported_formats:
762                    msg += " Allowed formats: {}".format(
763                        ", ".join(sorted(supported_formats))
764                    )
765                else:
766                    msg += (
767                        f" {self.connection.display_name} does not support any formats."
768                    )
769                raise ValueError(msg)
770        if options:
771            raise ValueError(
772                "Unknown options: {}".format(", ".join(sorted(options.keys())))
773            )
774        return self.explain_prefix  # type: ignore[return-value]
775
776    def insert_statement(self, on_conflict: Any = None) -> str:
777        return "INSERT INTO"
778
779    def on_conflict_suffix_sql(
780        self,
781        fields: list[Field],
782        on_conflict: Any,
783        update_fields: list[Field],
784        unique_fields: list[Field],
785    ) -> str:
786        return ""