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

265 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

1import datetime 

2import decimal 

3import json 

4from importlib import import_module 

5 

6import sqlparse 

7 

8from plain.models.backends import utils 

9from plain.models.db import NotSupportedError 

10from plain.utils import timezone 

11from plain.utils.encoding import force_str 

12 

13 

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

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