Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-models/plain/models/backends/base/operations.py: 46%

1 import datetime

2 import decimal

3 import json

4 from importlib import import_module

5

6 import sqlparse

7

8 from plain.models.backends import utils

9 from plain.models.db import NotSupportedError

10 from plain.utils import timezone

11 from plain.utils.encoding import force_str

12

13

14 class 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 """

19

20 compiler_module = "plain.models.sql.compiler"

21

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

46

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"

53

54 # Prefix for EXPLAIN queries, or None EXPLAIN isn't supported.

55 explain_prefix = None

56

57 def __init__(self, connection):

58 self.connection = connection

59 self._cache = None

60

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.

65

66 This SQL is executed when a table is created.

67 """

68 return None

69

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)

77

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 )

83

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.

88

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"

94

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"

102

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 )

112

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.

118

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 )

126

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 )

135

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 )

144

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 )

155

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 )

166

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.

172

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 )

179

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)

186

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 ""

193

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"], []

206

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()

213

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"

222

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 []

229

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 {}".format(", ".join(of)) if of else "",

237 " NOWAIT" if nowait else "",

238 " SKIP LOCKED" if skip_locked else "",

239 )

240

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

248

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 )

260

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.

265

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 """

271

272 # Convert params to contain string values.

273 def to_string(s):

274 return force_str(s, strings_only=True, errors="replace")

275

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()}

282

283 return f"QUERY = {sql! r} - PARAMS = {u_params! r}"

284

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.

289

290 `pk_name` is the name of the primary-key column.

291 """

292 return cursor.lastrowid

293

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"

301

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

308

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

315

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 )

324

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"

331

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.

336

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 ]

346

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

353

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

361

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)

371

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 )

380

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.

386

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 )

393

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 f"SAVEPOINT {self.quote_name(sid)}"

401

402 def savepoint_commit_sql(self, sid):

403 """

404 Return the SQL for committing the given savepoint.

405 """

406 return f"RELEASE SAVEPOINT {self.quote_name(sid)}"

407

408 def savepoint_rollback_sql(self, sid):

409 """

410 Return the SQL for rolling back the given savepoint.

411 """

412 return f"ROLLBACK TO SAVEPOINT {self.quote_name(sid)}"

413

414 def set_time_zone_sql(self):

415 """

416 Return the SQL that will set the connection's time zone.

417

418 Return '' if the backend doesn't support time zones.

419 """

420 return ""

421

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`.

426

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 []

431

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.

436

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.

441

442 def start_transaction_sql(self):

443 """Return the SQL statement required to start a transaction."""

444 return "BEGIN;"

445

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;"

451

452 def tablespace_sql(self, tablespace, inline=False):

453 """

454 Return the SQL that will be used in a query to define the tablespace.

455

456 Return '' if the backend doesn't support tablespaces.

457

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 ""

462

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"\_")

466

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

470

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

478

479 def adapt_unknown_value(self, value):

480 """

481 Transform a value to something compatible with the backend driver.

482

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

497

498 def adapt_integerfield_value(self, value, internal_type):

499 return value

500

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)

509

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

520

521 return str(value)

522

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

533

534 if timezone.is_aware(value):

535 raise ValueError("Plain does not support timezone-aware times.")

536 return str(value)

537

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)

544

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

551

552 def adapt_json_value(self, value, encoder):

553 return json.dumps(value, cls=encoder)

554

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.

560

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]

575

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.

581

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)

593

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)

598

599 first = self.adapt_datetimefield_value(first)

600 second = self.adapt_datetimefield_value(second)

601 return [first, second]

602

603 def get_db_converters(self, expression):

604 """

605 Return a list of functions needed to convert field data.

606

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 []

611

612 def convert_durationfield_value(self, value, expression, connection):

613 if value is not None:

614 return datetime.timedelta(0, 0, value)

615

616 def check_expression_support(self, expression):

617 """

618 Check that the backend supports the provided expression.

619

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

626

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

633

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 = f" {connector} "

642 return conn.join(sub_expressions)

643

644 def combine_duration_expression(self, connector, sub_expressions):

645 return self.combine_expression(connector, sub_expressions)

646

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"

653

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

660

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]

668

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 f"This backend does not support {internal_type} subtraction."

676 )

677

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 f"start argument must be a negative integer, zero, or None, but got '{start}'."

688 )

689

690 def window_frame_end(self, end):

691 if isinstance(end, int):

692 if end == 0:

693 return self.CURRENT_ROW

694 elif end > 0:

695 return "%d %s" % (end, self.FOLLOWING)

696 elif end is None:

697 return self.UNBOUNDED_FOLLOWING

698 raise ValueError(

699 f"end argument must be a positive integer, zero, or None, but got '{end}'."

700 )

701

702 def window_frame_rows_start_end(self, start=None, end=None):

703 """

704 Return SQL for start and end points in an OVER clause window frame.

705 """

706 if not self.connection.features.supports_over_clause:

707 raise NotSupportedError("This backend does not support window expressions.")

708 return self.window_frame_start(start), self.window_frame_end(end)

709

710 def window_frame_range_start_end(self, start=None, end=None):

711 start_, end_ = self.window_frame_rows_start_end(start, end)

712 features = self.connection.features

713 if features.only_supports_unbounded_with_preceding_and_following and (

714 (start and start < 0) or (end and end > 0)

715 ):

716 raise NotSupportedError(

717 f"{self.connection.display_name} only supports UNBOUNDED together with PRECEDING and "

718 "FOLLOWING."

719 )

720 return start_, end_

721

722 def explain_query_prefix(self, format=None, **options):

723 if not self.connection.features.supports_explaining_query_execution:

724 raise NotSupportedError(

725 "This backend does not support explaining query execution."

726 )

727 if format:

728 supported_formats = self.connection.features.supported_explain_formats

729 normalized_format = format.upper()

730 if normalized_format not in supported_formats:

731 msg = f"{normalized_format} is not a recognized format."

732 if supported_formats:

733 msg += " Allowed formats: {}".format(

734 ", ".join(sorted(supported_formats))

735 )

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(

743 "Unknown options: {}".format(", ".join(sorted(options.keys())))

744 )

745 return self.explain_prefix

746

747 def insert_statement(self, on_conflict=None):

748 return "INSERT INTO"

749

750 def on_conflict_suffix_sql(self, fields, on_conflict, update_fields, unique_fields):

751 return ""