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