Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py: 53%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

188 statements  

1# dialects/sqlite/pysqlite.py 

2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors 

3# <see AUTHORS file> 

4# 

5# This module is part of SQLAlchemy and is released under 

6# the MIT License: https://www.opensource.org/licenses/mit-license.php 

7 

8 

9r""" 

10.. dialect:: sqlite+pysqlite 

11 :name: pysqlite 

12 :dbapi: sqlite3 

13 :connectstring: sqlite+pysqlite:///file_path 

14 :url: https://docs.python.org/library/sqlite3.html 

15 

16 Note that ``pysqlite`` is the same driver as the ``sqlite3`` 

17 module included with the Python distribution. 

18 

19Driver 

20------ 

21 

22The ``sqlite3`` Python DBAPI is standard on all modern Python versions; 

23for cPython and Pypy, no additional installation is necessary. 

24 

25 

26Connect Strings 

27--------------- 

28 

29The file specification for the SQLite database is taken as the "database" 

30portion of the URL. Note that the format of a SQLAlchemy url is: 

31 

32.. sourcecode:: text 

33 

34 driver://user:pass@host/database 

35 

36This means that the actual filename to be used starts with the characters to 

37the **right** of the third slash. So connecting to a relative filepath 

38looks like:: 

39 

40 # relative path 

41 e = create_engine("sqlite:///path/to/database.db") 

42 

43An absolute path, which is denoted by starting with a slash, means you 

44need **four** slashes:: 

45 

46 # absolute path 

47 e = create_engine("sqlite:////path/to/database.db") 

48 

49To use a Windows path, regular drive specifications and backslashes can be 

50used. Double backslashes are probably needed:: 

51 

52 # absolute path on Windows 

53 e = create_engine("sqlite:///C:\\path\\to\\database.db") 

54 

55To use sqlite ``:memory:`` database specify it as the filename using 

56``sqlite:///:memory:``. It's also the default if no filepath is 

57present, specifying only ``sqlite://`` and nothing else:: 

58 

59 # in-memory database (note three slashes) 

60 e = create_engine("sqlite:///:memory:") 

61 # also in-memory database 

62 e2 = create_engine("sqlite://") 

63 

64.. _pysqlite_uri_connections: 

65 

66URI Connections 

67^^^^^^^^^^^^^^^ 

68 

69Modern versions of SQLite support an alternative system of connecting using a 

70`driver level URI <https://www.sqlite.org/uri.html>`_, which has the advantage 

71that additional driver-level arguments can be passed including options such as 

72"read only". The Python sqlite3 driver supports this mode under modern Python 

733 versions. The SQLAlchemy pysqlite driver supports this mode of use by 

74specifying "uri=true" in the URL query string. The SQLite-level "URI" is kept 

75as the "database" portion of the SQLAlchemy url (that is, following a slash):: 

76 

77 e = create_engine("sqlite:///file:path/to/database?mode=ro&uri=true") 

78 

79.. note:: The "uri=true" parameter must appear in the **query string** 

80 of the URL. It will not currently work as expected if it is only 

81 present in the :paramref:`_sa.create_engine.connect_args` 

82 parameter dictionary. 

83 

84The logic reconciles the simultaneous presence of SQLAlchemy's query string and 

85SQLite's query string by separating out the parameters that belong to the 

86Python sqlite3 driver vs. those that belong to the SQLite URI. This is 

87achieved through the use of a fixed list of parameters known to be accepted by 

88the Python side of the driver. For example, to include a URL that indicates 

89the Python sqlite3 "timeout" and "check_same_thread" parameters, along with the 

90SQLite "mode" and "nolock" parameters, they can all be passed together on the 

91query string:: 

92 

93 e = create_engine( 

94 "sqlite:///file:path/to/database?" 

95 "check_same_thread=true&timeout=10&mode=ro&nolock=1&uri=true" 

96 ) 

97 

98Above, the pysqlite / sqlite3 DBAPI would be passed arguments as:: 

99 

100 sqlite3.connect( 

101 "file:path/to/database?mode=ro&nolock=1", 

102 check_same_thread=True, 

103 timeout=10, 

104 uri=True, 

105 ) 

106 

107Regarding future parameters added to either the Python or native drivers. new 

108parameter names added to the SQLite URI scheme should be automatically 

109accommodated by this scheme. New parameter names added to the Python driver 

110side can be accommodated by specifying them in the 

111:paramref:`_sa.create_engine.connect_args` dictionary, 

112until dialect support is 

113added by SQLAlchemy. For the less likely case that the native SQLite driver 

114adds a new parameter name that overlaps with one of the existing, known Python 

115driver parameters (such as "timeout" perhaps), SQLAlchemy's dialect would 

116require adjustment for the URL scheme to continue to support this. 

117 

118As is always the case for all SQLAlchemy dialects, the entire "URL" process 

119can be bypassed in :func:`_sa.create_engine` through the use of the 

120:paramref:`_sa.create_engine.creator` 

121parameter which allows for a custom callable 

122that creates a Python sqlite3 driver level connection directly. 

123 

124.. versionadded:: 1.3.9 

125 

126.. seealso:: 

127 

128 `Uniform Resource Identifiers <https://www.sqlite.org/uri.html>`_ - in 

129 the SQLite documentation 

130 

131.. _pysqlite_regexp: 

132 

133Regular Expression Support 

134--------------------------- 

135 

136.. versionadded:: 1.4 

137 

138Support for the :meth:`_sql.ColumnOperators.regexp_match` operator is provided 

139using Python's re.search_ function. SQLite itself does not include a working 

140regular expression operator; instead, it includes a non-implemented placeholder 

141operator ``REGEXP`` that calls a user-defined function that must be provided. 

142 

143SQLAlchemy's implementation makes use of the pysqlite create_function_ hook 

144as follows:: 

145 

146 

147 def regexp(a, b): 

148 return re.search(a, b) is not None 

149 

150 

151 sqlite_connection.create_function( 

152 "regexp", 

153 2, 

154 regexp, 

155 ) 

156 

157There is currently no support for regular expression flags as a separate 

158argument, as these are not supported by SQLite's REGEXP operator, however these 

159may be included inline within the regular expression string. See `Python regular expressions`_ for 

160details. 

161 

162.. seealso:: 

163 

164 `Python regular expressions`_: Documentation for Python's regular expression syntax. 

165 

166.. _create_function: https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_function 

167 

168.. _re.search: https://docs.python.org/3/library/re.html#re.search 

169 

170.. _Python regular expressions: https://docs.python.org/3/library/re.html#re.search 

171 

172 

173 

174Compatibility with sqlite3 "native" date and datetime types 

175----------------------------------------------------------- 

176 

177The pysqlite driver includes the sqlite3.PARSE_DECLTYPES and 

178sqlite3.PARSE_COLNAMES options, which have the effect of any column 

179or expression explicitly cast as "date" or "timestamp" will be converted 

180to a Python date or datetime object. The date and datetime types provided 

181with the pysqlite dialect are not currently compatible with these options, 

182since they render the ISO date/datetime including microseconds, which 

183pysqlite's driver does not. Additionally, SQLAlchemy does not at 

184this time automatically render the "cast" syntax required for the 

185freestanding functions "current_timestamp" and "current_date" to return 

186datetime/date types natively. Unfortunately, pysqlite 

187does not provide the standard DBAPI types in ``cursor.description``, 

188leaving SQLAlchemy with no way to detect these types on the fly 

189without expensive per-row type checks. 

190 

191Keeping in mind that pysqlite's parsing option is not recommended, 

192nor should be necessary, for use with SQLAlchemy, usage of PARSE_DECLTYPES 

193can be forced if one configures "native_datetime=True" on create_engine():: 

194 

195 engine = create_engine( 

196 "sqlite://", 

197 connect_args={ 

198 "detect_types": sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES 

199 }, 

200 native_datetime=True, 

201 ) 

202 

203With this flag enabled, the DATE and TIMESTAMP types (but note - not the 

204DATETIME or TIME types...confused yet ?) will not perform any bind parameter 

205or result processing. Execution of "func.current_date()" will return a string. 

206"func.current_timestamp()" is registered as returning a DATETIME type in 

207SQLAlchemy, so this function still receives SQLAlchemy-level result 

208processing. 

209 

210.. _pysqlite_threading_pooling: 

211 

212Threading/Pooling Behavior 

213--------------------------- 

214 

215The ``sqlite3`` DBAPI by default prohibits the use of a particular connection 

216in a thread which is not the one in which it was created. As SQLite has 

217matured, it's behavior under multiple threads has improved, and even includes 

218options for memory only databases to be used in multiple threads. 

219 

220The thread prohibition is known as "check same thread" and may be controlled 

221using the ``sqlite3`` parameter ``check_same_thread``, which will disable or 

222enable this check. SQLAlchemy's default behavior here is to set 

223``check_same_thread`` to ``False`` automatically whenever a file-based database 

224is in use, to establish compatibility with the default pool class 

225:class:`.QueuePool`. 

226 

227The SQLAlchemy ``pysqlite`` DBAPI establishes the connection pool differently 

228based on the kind of SQLite database that's requested: 

229 

230* When a ``:memory:`` SQLite database is specified, the dialect by default 

231 will use :class:`.SingletonThreadPool`. This pool maintains a single 

232 connection per thread, so that all access to the engine within the current 

233 thread use the same ``:memory:`` database - other threads would access a 

234 different ``:memory:`` database. The ``check_same_thread`` parameter 

235 defaults to ``True``. 

236* When a file-based database is specified, the dialect will use 

237 :class:`.QueuePool` as the source of connections. at the same time, 

238 the ``check_same_thread`` flag is set to False by default unless overridden. 

239 

240 .. versionchanged:: 2.0 

241 

242 SQLite file database engines now use :class:`.QueuePool` by default. 

243 Previously, :class:`.NullPool` were used. The :class:`.NullPool` class 

244 may be used by specifying it via the 

245 :paramref:`_sa.create_engine.poolclass` parameter. 

246 

247Disabling Connection Pooling for File Databases 

248^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

249 

250Pooling may be disabled for a file based database by specifying the 

251:class:`.NullPool` implementation for the :func:`_sa.create_engine.poolclass` 

252parameter:: 

253 

254 from sqlalchemy import NullPool 

255 

256 engine = create_engine("sqlite:///myfile.db", poolclass=NullPool) 

257 

258It's been observed that the :class:`.NullPool` implementation incurs an 

259extremely small performance overhead for repeated checkouts due to the lack of 

260connection re-use implemented by :class:`.QueuePool`. However, it still 

261may be beneficial to use this class if the application is experiencing 

262issues with files being locked. 

263 

264Using a Memory Database in Multiple Threads 

265^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

266 

267To use a ``:memory:`` database in a multithreaded scenario, the same 

268connection object must be shared among threads, since the database exists 

269only within the scope of that connection. The 

270:class:`.StaticPool` implementation will maintain a single connection 

271globally, and the ``check_same_thread`` flag can be passed to Pysqlite 

272as ``False``:: 

273 

274 from sqlalchemy.pool import StaticPool 

275 

276 engine = create_engine( 

277 "sqlite://", 

278 connect_args={"check_same_thread": False}, 

279 poolclass=StaticPool, 

280 ) 

281 

282Note that using a ``:memory:`` database in multiple threads requires a recent 

283version of SQLite. 

284 

285Using Temporary Tables with SQLite 

286^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

287 

288Due to the way SQLite deals with temporary tables, if you wish to use a 

289temporary table in a file-based SQLite database across multiple checkouts 

290from the connection pool, such as when using an ORM :class:`.Session` where 

291the temporary table should continue to remain after :meth:`.Session.commit` or 

292:meth:`.Session.rollback` is called, a pool which maintains a single 

293connection must be used. Use :class:`.SingletonThreadPool` if the scope is 

294only needed within the current thread, or :class:`.StaticPool` is scope is 

295needed within multiple threads for this case:: 

296 

297 # maintain the same connection per thread 

298 from sqlalchemy.pool import SingletonThreadPool 

299 

300 engine = create_engine("sqlite:///mydb.db", poolclass=SingletonThreadPool) 

301 

302 

303 # maintain the same connection across all threads 

304 from sqlalchemy.pool import StaticPool 

305 

306 engine = create_engine("sqlite:///mydb.db", poolclass=StaticPool) 

307 

308Note that :class:`.SingletonThreadPool` should be configured for the number 

309of threads that are to be used; beyond that number, connections will be 

310closed out in a non deterministic way. 

311 

312 

313Dealing with Mixed String / Binary Columns 

314------------------------------------------------------ 

315 

316The SQLite database is weakly typed, and as such it is possible when using 

317binary values, which in Python are represented as ``b'some string'``, that a 

318particular SQLite database can have data values within different rows where 

319some of them will be returned as a ``b''`` value by the Pysqlite driver, and 

320others will be returned as Python strings, e.g. ``''`` values. This situation 

321is not known to occur if the SQLAlchemy :class:`.LargeBinary` datatype is used 

322consistently, however if a particular SQLite database has data that was 

323inserted using the Pysqlite driver directly, or when using the SQLAlchemy 

324:class:`.String` type which was later changed to :class:`.LargeBinary`, the 

325table will not be consistently readable because SQLAlchemy's 

326:class:`.LargeBinary` datatype does not handle strings so it has no way of 

327"encoding" a value that is in string format. 

328 

329To deal with a SQLite table that has mixed string / binary data in the 

330same column, use a custom type that will check each row individually:: 

331 

332 from sqlalchemy import String 

333 from sqlalchemy import TypeDecorator 

334 

335 

336 class MixedBinary(TypeDecorator): 

337 impl = String 

338 cache_ok = True 

339 

340 def process_result_value(self, value, dialect): 

341 if isinstance(value, str): 

342 value = bytes(value, "utf-8") 

343 elif value is not None: 

344 value = bytes(value) 

345 

346 return value 

347 

348Then use the above ``MixedBinary`` datatype in the place where 

349:class:`.LargeBinary` would normally be used. 

350 

351.. _pysqlite_serializable: 

352 

353Serializable isolation / Savepoints / Transactional DDL 

354------------------------------------------------------- 

355 

356A newly revised version of this important section is now available 

357at the top level of the SQLAlchemy SQLite documentation, in the section 

358:ref:`sqlite_transactions`. 

359 

360 

361.. _pysqlite_udfs: 

362 

363User-Defined Functions 

364---------------------- 

365 

366pysqlite supports a `create_function() <https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_function>`_ 

367method that allows us to create our own user-defined functions (UDFs) in Python and use them directly in SQLite queries. 

368These functions are registered with a specific DBAPI Connection. 

369 

370SQLAlchemy uses connection pooling with file-based SQLite databases, so we need to ensure that the UDF is attached to the 

371connection when it is created. That is accomplished with an event listener:: 

372 

373 from sqlalchemy import create_engine 

374 from sqlalchemy import event 

375 from sqlalchemy import text 

376 

377 

378 def udf(): 

379 return "udf-ok" 

380 

381 

382 engine = create_engine("sqlite:///./db_file") 

383 

384 

385 @event.listens_for(engine, "connect") 

386 def connect(conn, rec): 

387 conn.create_function("udf", 0, udf) 

388 

389 

390 for i in range(5): 

391 with engine.connect() as conn: 

392 print(conn.scalar(text("SELECT UDF()"))) 

393 

394""" # noqa 

395from __future__ import annotations 

396 

397import math 

398import os 

399import re 

400from typing import Any 

401from typing import Callable 

402from typing import cast 

403from typing import Optional 

404from typing import Pattern 

405from typing import TYPE_CHECKING 

406from typing import TypeVar 

407from typing import Union 

408 

409from .base import DATE 

410from .base import DATETIME 

411from .base import SQLiteDialect 

412from ... import exc 

413from ... import pool 

414from ... import types as sqltypes 

415from ... import util 

416from ...util.typing import Self 

417 

418if TYPE_CHECKING: 

419 from ...engine.interfaces import ConnectArgsType 

420 from ...engine.interfaces import DBAPIConnection 

421 from ...engine.interfaces import DBAPICursor 

422 from ...engine.interfaces import DBAPIModule 

423 from ...engine.interfaces import IsolationLevel 

424 from ...engine.interfaces import VersionInfoType 

425 from ...engine.url import URL 

426 from ...pool.base import PoolProxiedConnection 

427 from ...sql.type_api import _BindProcessorType 

428 from ...sql.type_api import _ResultProcessorType 

429 

430 

431class _SQLite_pysqliteTimeStamp(DATETIME): 

432 def bind_processor( # type: ignore[override] 

433 self, dialect: SQLiteDialect 

434 ) -> Optional[_BindProcessorType[Any]]: 

435 if dialect.native_datetime: 

436 return None 

437 else: 

438 return DATETIME.bind_processor(self, dialect) 

439 

440 def result_processor( # type: ignore[override] 

441 self, dialect: SQLiteDialect, coltype: object 

442 ) -> Optional[_ResultProcessorType[Any]]: 

443 if dialect.native_datetime: 

444 return None 

445 else: 

446 return DATETIME.result_processor(self, dialect, coltype) 

447 

448 

449class _SQLite_pysqliteDate(DATE): 

450 def bind_processor( # type: ignore[override] 

451 self, dialect: SQLiteDialect 

452 ) -> Optional[_BindProcessorType[Any]]: 

453 if dialect.native_datetime: 

454 return None 

455 else: 

456 return DATE.bind_processor(self, dialect) 

457 

458 def result_processor( # type: ignore[override] 

459 self, dialect: SQLiteDialect, coltype: object 

460 ) -> Optional[_ResultProcessorType[Any]]: 

461 if dialect.native_datetime: 

462 return None 

463 else: 

464 return DATE.result_processor(self, dialect, coltype) 

465 

466 

467class SQLiteDialect_pysqlite(SQLiteDialect): 

468 default_paramstyle = "qmark" 

469 supports_statement_cache = True 

470 returns_native_bytes = True 

471 

472 colspecs = util.update_copy( 

473 SQLiteDialect.colspecs, 

474 { 

475 sqltypes.Date: _SQLite_pysqliteDate, 

476 sqltypes.TIMESTAMP: _SQLite_pysqliteTimeStamp, 

477 }, 

478 ) 

479 

480 description_encoding = None 

481 

482 driver = "pysqlite" 

483 

484 @classmethod 

485 def import_dbapi(cls) -> DBAPIModule: 

486 from sqlite3 import dbapi2 as sqlite 

487 

488 return cast("DBAPIModule", sqlite) 

489 

490 @classmethod 

491 def _is_url_file_db(cls, url: URL) -> bool: 

492 if (url.database and url.database != ":memory:") and ( 

493 url.query.get("mode", None) != "memory" 

494 ): 

495 return True 

496 else: 

497 return False 

498 

499 @classmethod 

500 def get_pool_class(cls, url: URL) -> type[pool.Pool]: 

501 if cls._is_url_file_db(url): 

502 return pool.QueuePool 

503 else: 

504 return pool.SingletonThreadPool 

505 

506 def _get_server_version_info(self, connection: Any) -> VersionInfoType: 

507 return self.dbapi.sqlite_version_info # type: ignore 

508 

509 _isolation_lookup = SQLiteDialect._isolation_lookup.union( 

510 { 

511 "AUTOCOMMIT": None, # type: ignore[dict-item] 

512 } 

513 ) 

514 

515 def set_isolation_level( 

516 self, dbapi_connection: DBAPIConnection, level: IsolationLevel 

517 ) -> None: 

518 if level == "AUTOCOMMIT": 

519 dbapi_connection.isolation_level = None 

520 else: 

521 dbapi_connection.isolation_level = "" 

522 return super().set_isolation_level(dbapi_connection, level) 

523 

524 def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool: 

525 return dbapi_conn.isolation_level is None 

526 

527 def on_connect(self) -> Callable[[DBAPIConnection], None]: 

528 def regexp(a: str, b: Optional[str]) -> Optional[bool]: 

529 if b is None: 

530 return None 

531 return re.search(a, b) is not None 

532 

533 if util.py38 and self._get_server_version_info(None) >= (3, 9): 

534 # sqlite must be greater than 3.8.3 for deterministic=True 

535 # https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_function 

536 # the check is more conservative since there were still issues 

537 # with following 3.8 sqlite versions 

538 create_func_kw = {"deterministic": True} 

539 else: 

540 create_func_kw = {} 

541 

542 def set_regexp(dbapi_connection: DBAPIConnection) -> None: 

543 dbapi_connection.create_function( 

544 "regexp", 2, regexp, **create_func_kw 

545 ) 

546 

547 def floor_func(dbapi_connection: DBAPIConnection) -> None: 

548 # NOTE: floor is optionally present in sqlite 3.35+ , however 

549 # as it is normally non-present we deliver floor() unconditionally 

550 # for now. 

551 # https://www.sqlite.org/lang_mathfunc.html 

552 dbapi_connection.create_function( 

553 "floor", 1, math.floor, **create_func_kw 

554 ) 

555 

556 fns = [set_regexp, floor_func] 

557 

558 def connect(conn: DBAPIConnection) -> None: 

559 for fn in fns: 

560 fn(conn) 

561 

562 return connect 

563 

564 def create_connect_args(self, url: URL) -> ConnectArgsType: 

565 if url.username or url.password or url.host or url.port: 

566 raise exc.ArgumentError( 

567 "Invalid SQLite URL: %s\n" 

568 "Valid SQLite URL forms are:\n" 

569 " sqlite:///:memory: (or, sqlite://)\n" 

570 " sqlite:///relative/path/to/file.db\n" 

571 " sqlite:////absolute/path/to/file.db" % (url,) 

572 ) 

573 

574 # theoretically, this list can be augmented, at least as far as 

575 # parameter names accepted by sqlite3/pysqlite, using 

576 # inspect.getfullargspec(). for the moment this seems like overkill 

577 # as these parameters don't change very often, and as always, 

578 # parameters passed to connect_args will always go to the 

579 # sqlite3/pysqlite driver. 

580 pysqlite_args = [ 

581 ("uri", bool), 

582 ("timeout", float), 

583 ("isolation_level", str), 

584 ("detect_types", int), 

585 ("check_same_thread", bool), 

586 ("cached_statements", int), 

587 ] 

588 opts = url.query 

589 pysqlite_opts: dict[str, Any] = {} 

590 for key, type_ in pysqlite_args: 

591 util.coerce_kw_type(opts, key, type_, dest=pysqlite_opts) 

592 

593 if pysqlite_opts.get("uri", False): 

594 uri_opts = dict(opts) 

595 # here, we are actually separating the parameters that go to 

596 # sqlite3/pysqlite vs. those that go the SQLite URI. What if 

597 # two names conflict? again, this seems to be not the case right 

598 # now, and in the case that new names are added to 

599 # either side which overlap, again the sqlite3/pysqlite parameters 

600 # can be passed through connect_args instead of in the URL. 

601 # If SQLite native URIs add a parameter like "timeout" that 

602 # we already have listed here for the python driver, then we need 

603 # to adjust for that here. 

604 for key, type_ in pysqlite_args: 

605 uri_opts.pop(key, None) 

606 filename: str = url.database # type: ignore[assignment] 

607 if uri_opts: 

608 # sorting of keys is for unit test support 

609 filename += "?" + ( 

610 "&".join( 

611 "%s=%s" % (key, uri_opts[key]) 

612 for key in sorted(uri_opts) 

613 ) 

614 ) 

615 else: 

616 filename = url.database or ":memory:" 

617 if filename != ":memory:": 

618 filename = os.path.abspath(filename) 

619 

620 pysqlite_opts.setdefault( 

621 "check_same_thread", not self._is_url_file_db(url) 

622 ) 

623 

624 return ([filename], pysqlite_opts) 

625 

626 def is_disconnect( 

627 self, 

628 e: DBAPIModule.Error, 

629 connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]], 

630 cursor: Optional[DBAPICursor], 

631 ) -> bool: 

632 self.dbapi = cast("DBAPIModule", self.dbapi) 

633 return isinstance( 

634 e, self.dbapi.ProgrammingError 

635 ) and "Cannot operate on a closed database." in str(e) 

636 

637 

638dialect = SQLiteDialect_pysqlite 

639 

640 

641class _SQLiteDialect_pysqlite_numeric(SQLiteDialect_pysqlite): 

642 """numeric dialect for testing only 

643 

644 internal use only. This dialect is **NOT** supported by SQLAlchemy 

645 and may change at any time. 

646 

647 """ 

648 

649 supports_statement_cache = True 

650 default_paramstyle = "numeric" 

651 driver = "pysqlite_numeric" 

652 

653 _first_bind = ":1" 

654 _not_in_statement_regexp: Optional[Pattern[str]] = None 

655 

656 def __init__(self, *arg: Any, **kw: Any) -> None: 

657 kw.setdefault("paramstyle", "numeric") 

658 super().__init__(*arg, **kw) 

659 

660 def create_connect_args(self, url: URL) -> ConnectArgsType: 

661 arg, opts = super().create_connect_args(url) 

662 opts["factory"] = self._fix_sqlite_issue_99953() 

663 return arg, opts 

664 

665 def _fix_sqlite_issue_99953(self) -> Any: 

666 import sqlite3 

667 

668 first_bind = self._first_bind 

669 if self._not_in_statement_regexp: 

670 nis = self._not_in_statement_regexp 

671 

672 def _test_sql(sql: str) -> None: 

673 m = nis.search(sql) 

674 assert not m, f"Found {nis.pattern!r} in {sql!r}" 

675 

676 else: 

677 

678 def _test_sql(sql: str) -> None: 

679 pass 

680 

681 def _numeric_param_as_dict( 

682 parameters: Any, 

683 ) -> Union[dict[str, Any], tuple[Any, ...]]: 

684 if parameters: 

685 assert isinstance(parameters, tuple) 

686 return { 

687 str(idx): value for idx, value in enumerate(parameters, 1) 

688 } 

689 else: 

690 return () 

691 

692 class SQLiteFix99953Cursor(sqlite3.Cursor): 

693 def execute(self, sql: str, parameters: Any = ()) -> Self: 

694 _test_sql(sql) 

695 if first_bind in sql: 

696 parameters = _numeric_param_as_dict(parameters) 

697 return super().execute(sql, parameters) 

698 

699 def executemany(self, sql: str, parameters: Any) -> Self: 

700 _test_sql(sql) 

701 if first_bind in sql: 

702 parameters = [ 

703 _numeric_param_as_dict(p) for p in parameters 

704 ] 

705 return super().executemany(sql, parameters) 

706 

707 class SQLiteFix99953Connection(sqlite3.Connection): 

708 _CursorT = TypeVar("_CursorT", bound=sqlite3.Cursor) 

709 

710 def cursor( 

711 self, 

712 factory: Optional[ 

713 Callable[[sqlite3.Connection], _CursorT] 

714 ] = None, 

715 ) -> _CursorT: 

716 if factory is None: 

717 factory = SQLiteFix99953Cursor # type: ignore[assignment] 

718 return super().cursor(factory=factory) # type: ignore[return-value] # noqa[E501] 

719 

720 def execute( 

721 self, sql: str, parameters: Any = () 

722 ) -> sqlite3.Cursor: 

723 _test_sql(sql) 

724 if first_bind in sql: 

725 parameters = _numeric_param_as_dict(parameters) 

726 return super().execute(sql, parameters) 

727 

728 def executemany(self, sql: str, parameters: Any) -> sqlite3.Cursor: 

729 _test_sql(sql) 

730 if first_bind in sql: 

731 parameters = [ 

732 _numeric_param_as_dict(p) for p in parameters 

733 ] 

734 return super().executemany(sql, parameters) 

735 

736 return SQLiteFix99953Connection 

737 

738 

739class _SQLiteDialect_pysqlite_dollar(_SQLiteDialect_pysqlite_numeric): 

740 """numeric dialect that uses $ for testing only 

741 

742 internal use only. This dialect is **NOT** supported by SQLAlchemy 

743 and may change at any time. 

744 

745 """ 

746 

747 supports_statement_cache = True 

748 default_paramstyle = "numeric_dollar" 

749 driver = "pysqlite_dollar" 

750 

751 _first_bind = "$1" 

752 _not_in_statement_regexp = re.compile(r"[^\d]:\d+") 

753 

754 def __init__(self, *arg: Any, **kw: Any) -> None: 

755 kw.setdefault("paramstyle", "numeric_dollar") 

756 super().__init__(*arg, **kw)