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

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-2026 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.. seealso:: 

125 

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

127 the SQLite documentation 

128 

129.. _pysqlite_regexp: 

130 

131Regular Expression Support 

132--------------------------- 

133 

134.. versionadded:: 1.4 

135 

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

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

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

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

140 

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

142as follows:: 

143 

144 

145 def regexp(a, b): 

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

147 

148 

149 sqlite_connection.create_function( 

150 "regexp", 

151 2, 

152 regexp, 

153 ) 

154 

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

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

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

158details. 

159 

160.. seealso:: 

161 

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

163 

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

165 

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

167 

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

169 

170 

171 

172Compatibility with sqlite3 "native" date and datetime types 

173----------------------------------------------------------- 

174 

175The pysqlite driver includes the sqlite3.PARSE_DECLTYPES and 

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

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

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

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

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

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

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

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

184datetime/date types natively. Unfortunately, pysqlite 

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

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

187without expensive per-row type checks. 

188 

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

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

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

192 

193 engine = create_engine( 

194 "sqlite://", 

195 connect_args={ 

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

197 }, 

198 native_datetime=True, 

199 ) 

200 

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

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

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

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

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

206processing. 

207 

208.. _pysqlite_threading_pooling: 

209 

210Threading/Pooling Behavior 

211--------------------------- 

212 

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

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

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

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

217 

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

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

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

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

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

223:class:`.QueuePool`. 

224 

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

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

227 

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

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

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

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

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

233 defaults to ``True``. 

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

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

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

237 

238 .. versionchanged:: 2.0 

239 

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

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

242 may be used by specifying it via the 

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

244 

245Disabling Connection Pooling for File Databases 

246^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

247 

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

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

250parameter:: 

251 

252 from sqlalchemy import NullPool 

253 

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

255 

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

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

258connection reuse implemented by :class:`.QueuePool`. However, it still 

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

260issues with files being locked. 

261 

262Using a Memory Database in Multiple Threads 

263^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

264 

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

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

267only within the scope of that connection. The 

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

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

270as ``False``:: 

271 

272 from sqlalchemy.pool import StaticPool 

273 

274 engine = create_engine( 

275 "sqlite://", 

276 connect_args={"check_same_thread": False}, 

277 poolclass=StaticPool, 

278 ) 

279 

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

281version of SQLite. 

282 

283Using Temporary Tables with SQLite 

284^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

285 

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

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

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

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

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

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

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

293needed within multiple threads for this case:: 

294 

295 # maintain the same connection per thread 

296 from sqlalchemy.pool import SingletonThreadPool 

297 

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

299 

300 

301 # maintain the same connection across all threads 

302 from sqlalchemy.pool import StaticPool 

303 

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

305 

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

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

308closed out in a non deterministic way. 

309 

310 

311Dealing with Mixed String / Binary Columns 

312------------------------------------------------------ 

313 

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

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

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

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

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

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

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

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

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

323table will not be consistently readable because SQLAlchemy's 

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

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

326 

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

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

329 

330 from sqlalchemy import String 

331 from sqlalchemy import TypeDecorator 

332 

333 

334 class MixedBinary(TypeDecorator): 

335 impl = String 

336 cache_ok = True 

337 

338 def process_result_value(self, value, dialect): 

339 if isinstance(value, str): 

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

341 elif value is not None: 

342 value = bytes(value) 

343 

344 return value 

345 

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

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

348 

349.. _pysqlite_serializable: 

350 

351Serializable isolation / Savepoints / Transactional DDL 

352------------------------------------------------------- 

353 

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

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

356:ref:`sqlite_transactions`. 

357 

358 

359.. _pysqlite_udfs: 

360 

361User-Defined Functions 

362---------------------- 

363 

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

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

366These functions are registered with a specific DBAPI Connection. 

367 

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

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

370 

371 from sqlalchemy import create_engine 

372 from sqlalchemy import event 

373 from sqlalchemy import text 

374 

375 

376 def udf(): 

377 return "udf-ok" 

378 

379 

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

381 

382 

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

384 def connect(conn, rec): 

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

386 

387 

388 for i in range(5): 

389 with engine.connect() as conn: 

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

391 

392""" # noqa 

393 

394from __future__ import annotations 

395 

396import math 

397import os 

398import re 

399from typing import Any 

400from typing import Callable 

401from typing import cast 

402from typing import Optional 

403from typing import Pattern 

404from typing import TYPE_CHECKING 

405from typing import TypeVar 

406from typing import Union 

407 

408from .base import DATE 

409from .base import DATETIME 

410from .base import SQLiteDialect 

411from ... import exc 

412from ... import pool 

413from ... import types as sqltypes 

414from ... import util 

415from ...util.typing import Self 

416 

417if TYPE_CHECKING: 

418 from ...engine.interfaces import ConnectArgsType 

419 from ...engine.interfaces import DBAPIConnection 

420 from ...engine.interfaces import DBAPICursor 

421 from ...engine.interfaces import DBAPIModule 

422 from ...engine.interfaces import IsolationLevel 

423 from ...engine.interfaces import VersionInfoType 

424 from ...engine.url import URL 

425 from ...pool.base import PoolProxiedConnection 

426 from ...sql.type_api import _BindProcessorType 

427 from ...sql.type_api import _ResultProcessorType 

428 

429 

430class _SQLite_pysqliteTimeStamp(DATETIME): 

431 def bind_processor( # type: ignore[override] 

432 self, dialect: SQLiteDialect 

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

434 if dialect.native_datetime: 

435 return None 

436 else: 

437 return DATETIME.bind_processor(self, dialect) 

438 

439 def result_processor( # type: ignore[override] 

440 self, dialect: SQLiteDialect, coltype: object 

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

442 if dialect.native_datetime: 

443 return None 

444 else: 

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

446 

447 

448class _SQLite_pysqliteDate(DATE): 

449 def bind_processor( # type: ignore[override] 

450 self, dialect: SQLiteDialect 

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

452 if dialect.native_datetime: 

453 return None 

454 else: 

455 return DATE.bind_processor(self, dialect) 

456 

457 def result_processor( # type: ignore[override] 

458 self, dialect: SQLiteDialect, coltype: object 

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

460 if dialect.native_datetime: 

461 return None 

462 else: 

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

464 

465 

466class SQLiteDialect_pysqlite(SQLiteDialect): 

467 default_paramstyle = "qmark" 

468 supports_statement_cache = True 

469 returns_native_bytes = True 

470 

471 colspecs = util.update_copy( 

472 SQLiteDialect.colspecs, 

473 { 

474 sqltypes.Date: _SQLite_pysqliteDate, 

475 sqltypes.TIMESTAMP: _SQLite_pysqliteTimeStamp, 

476 }, 

477 ) 

478 

479 description_encoding = None 

480 

481 driver = "pysqlite" 

482 

483 @classmethod 

484 def import_dbapi(cls) -> DBAPIModule: 

485 from sqlite3 import dbapi2 as sqlite 

486 

487 return cast("DBAPIModule", sqlite) 

488 

489 @classmethod 

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

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

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

493 ): 

494 return True 

495 else: 

496 return False 

497 

498 @classmethod 

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

500 if cls._is_url_file_db(url): 

501 return pool.QueuePool 

502 else: 

503 return pool.SingletonThreadPool 

504 

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

506 return self.dbapi.sqlite_version_info # type: ignore 

507 

508 _isolation_lookup = SQLiteDialect._isolation_lookup.union( 

509 { 

510 "AUTOCOMMIT": None, 

511 } 

512 ) 

513 

514 def set_isolation_level( 

515 self, dbapi_connection: DBAPIConnection, level: IsolationLevel 

516 ) -> None: 

517 if level == "AUTOCOMMIT": 

518 dbapi_connection.isolation_level = None 

519 else: 

520 dbapi_connection.isolation_level = "" 

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

522 

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

524 return dbapi_conn.isolation_level is None 

525 

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

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

528 if b is None: 

529 return None 

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

531 

532 if self._get_server_version_info(None) >= (3, 9): 

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

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

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

536 # with following 3.8 sqlite versions 

537 create_func_kw = {"deterministic": True} 

538 else: 

539 create_func_kw = {} 

540 

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

542 dbapi_connection.create_function( 

543 "regexp", 2, regexp, **create_func_kw 

544 ) 

545 

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

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

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

549 # for now. 

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

551 dbapi_connection.create_function( 

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

553 ) 

554 

555 fns = [set_regexp, floor_func] 

556 

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

558 for fn in fns: 

559 fn(conn) 

560 

561 return connect 

562 

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

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

565 raise exc.ArgumentError( 

566 "Invalid SQLite URL: %s\n" 

567 "Valid SQLite URL forms are:\n" 

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

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

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

571 ) 

572 

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

574 # parameter names accepted by sqlite3/pysqlite, using 

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

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

577 # parameters passed to connect_args will always go to the 

578 # sqlite3/pysqlite driver. 

579 pysqlite_args = [ 

580 ("uri", bool), 

581 ("timeout", float), 

582 ("isolation_level", str), 

583 ("detect_types", int), 

584 ("check_same_thread", bool), 

585 ("cached_statements", int), 

586 ] 

587 opts = url.query 

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

589 for key, type_ in pysqlite_args: 

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

591 

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

593 uri_opts = dict(opts) 

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

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

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

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

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

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

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

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

602 # to adjust for that here. 

603 for key, type_ in pysqlite_args: 

604 uri_opts.pop(key, None) 

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

606 if uri_opts: 

607 # sorting of keys is for unit test support 

608 filename += "?" + ( 

609 "&".join( 

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

611 for key in sorted(uri_opts) 

612 ) 

613 ) 

614 else: 

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

616 if filename != ":memory:": 

617 filename = os.path.abspath(filename) 

618 

619 pysqlite_opts.setdefault( 

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

621 ) 

622 

623 return ([filename], pysqlite_opts) 

624 

625 def is_disconnect( 

626 self, 

627 e: DBAPIModule.Error, 

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

629 cursor: Optional[DBAPICursor], 

630 ) -> bool: 

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

632 return isinstance( 

633 e, self.dbapi.ProgrammingError 

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

635 

636 

637dialect = SQLiteDialect_pysqlite 

638 

639 

640class _SQLiteDialect_pysqlite_numeric(SQLiteDialect_pysqlite): 

641 """numeric dialect for testing only 

642 

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

644 and may change at any time. 

645 

646 """ 

647 

648 supports_statement_cache = True 

649 default_paramstyle = "numeric" 

650 driver = "pysqlite_numeric" 

651 

652 _first_bind = ":1" 

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

654 

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

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

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

658 

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

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

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

662 return arg, opts 

663 

664 def _fix_sqlite_issue_99953(self) -> Any: 

665 import sqlite3 

666 

667 first_bind = self._first_bind 

668 if self._not_in_statement_regexp: 

669 nis = self._not_in_statement_regexp 

670 

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

672 m = nis.search(sql) 

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

674 

675 else: 

676 

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

678 pass 

679 

680 def _numeric_param_as_dict( 

681 parameters: Any, 

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

683 if parameters: 

684 assert isinstance(parameters, tuple) 

685 return { 

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

687 } 

688 else: 

689 return () 

690 

691 class SQLiteFix99953Cursor(sqlite3.Cursor): 

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

693 _test_sql(sql) 

694 if first_bind in sql: 

695 parameters = _numeric_param_as_dict(parameters) 

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

697 

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

699 _test_sql(sql) 

700 if first_bind in sql: 

701 parameters = [ 

702 _numeric_param_as_dict(p) for p in parameters 

703 ] 

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

705 

706 class SQLiteFix99953Connection(sqlite3.Connection): 

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

708 

709 def cursor( 

710 self, 

711 factory: Optional[ 

712 Callable[[sqlite3.Connection], _CursorT] 

713 ] = None, 

714 ) -> _CursorT: 

715 if factory is None: 

716 factory = SQLiteFix99953Cursor # type: ignore[assignment] 

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

718 

719 def execute( 

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

721 ) -> sqlite3.Cursor: 

722 _test_sql(sql) 

723 if first_bind in sql: 

724 parameters = _numeric_param_as_dict(parameters) 

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

726 

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

728 _test_sql(sql) 

729 if first_bind in sql: 

730 parameters = [ 

731 _numeric_param_as_dict(p) for p in parameters 

732 ] 

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

734 

735 return SQLiteFix99953Connection 

736 

737 

738class _SQLiteDialect_pysqlite_dollar(_SQLiteDialect_pysqlite_numeric): 

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

740 

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

742 and may change at any time. 

743 

744 """ 

745 

746 supports_statement_cache = True 

747 default_paramstyle = "numeric_dollar" 

748 driver = "pysqlite_dollar" 

749 

750 _first_bind = "$1" 

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

752 

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

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

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