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
210Concurrency/Threading/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 or Coroutines
263^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
264
265A ``:memory:`` SQLite database exists only within the scope of a single
266DBAPI connection. It is not possible for two separate ``sqlite3``
267connection objects to access the same ``:memory:`` database unless SQLite's
268shared-cache feature is enabled. Without shared cache, each connection
269creates its own independent in-memory database.
270
271This means a ``:memory:`` database is **not suitable** for use with
272multiple concurrent threads or coroutines unless either:
273
274* All workers are fully serialized (mutexed) against each other
275 such that only one is using the database at a time, or
276* SQLite's shared-cache URI feature is used to allow multiple
277 independent connections to access the same in-memory database.
278
279The same considerations apply when using the :ref:`aiosqlite <aiosqlite>`
280dialect, which wraps ``pysqlite`` connections in an async interface —
281without shared cache, each connection still creates its own independent
282in-memory database.
283
284The recommended approach for multithreaded or async in-memory
285use is the shared-cache URI feature, described below.
286
287.. _pysqlite_uri_shared_cache:
288
289Using a Shared-Cache Memory Database
290~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
291
292SQLite's
293`URI shared-cache <https://www.sqlite.org/sharedcache.html>`_
294mode allows multiple independent DBAPI connections, each with
295their own transaction state, to access the same in-memory
296database. This is enabled by using a ``file:`` URI with
297``cache=shared`` and passing ``uri=true`` in the query
298string::
299
300 engine = create_engine("sqlite:///file::memory:?cache=shared&uri=true")
301
302For async use with aiosqlite, use
303:func:`_asyncio.create_async_engine`::
304
305 engine = create_async_engine(
306 "sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
307 )
308
309Because this URL form is treated as a file-based database by the
310dialect, :class:`.QueuePool` is used automatically and
311``check_same_thread`` defaults to ``False``, so no additional pool
312or connect_args configuration is needed. Each checkout from the
313pool is a distinct DBAPI connection with its own transaction state,
314and the in-memory database persists as long as at least one
315connection remains open.
316
317The shared-cache database is scoped by the filename component of
318the URI. ``file::memory:`` (empty name) is process-global — all
319engines in the process that use this URI share the same database.
320To maintain multiple independent in-memory databases within the
321same process, supply a distinct name for each::
322
323 engine_a = create_engine(
324 "sqlite:///file:db_a?mode=memory&cache=shared&uri=true"
325 )
326 engine_b = create_engine(
327 "sqlite:///file:db_b?mode=memory&cache=shared&uri=true"
328 )
329
330Using StaticPool for Single-Connection Memory Databases
331~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
332
333An older approach for sharing a ``:memory:`` database among threads
334is to force all checkouts to return the same DBAPI connection using
335:class:`.StaticPool`. **This approach does not support any form of
336concurrency** and is only useful when all access to the engine is
337fully serialized, such as in single-threaded test suites::
338
339 from sqlalchemy.pool import StaticPool
340
341 engine = create_engine(
342 "sqlite://",
343 connect_args={"check_same_thread": False},
344 poolclass=StaticPool,
345 )
346
347.. warning::
348
349 Because :class:`.StaticPool` maintains a single DBAPI connection,
350 all :class:`.Session` or :class:`.Connection` objects that use
351 this engine share that one underlying connection and its single
352 SQLite transaction state. A ``ROLLBACK`` issued by one session
353 (e.g. during error handling) will also roll back uncommitted work
354 from any other session, and concurrent ``COMMIT`` / ``ROLLBACK``
355 calls can interfere with each other unpredictably. This approach
356 is only appropriate when access to the engine is fully serialized,
357 such as in single-threaded test suites. For concurrent workloads,
358 use the shared-cache URI approach described above.
359
360Using Temporary Tables with SQLite
361^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
362
363Due to the way SQLite deals with temporary tables, if you wish to use a
364temporary table in a file-based SQLite database across multiple checkouts
365from the connection pool, such as when using an ORM :class:`.Session` where
366the temporary table should continue to remain after :meth:`.Session.commit` or
367:meth:`.Session.rollback` is called, a pool which maintains a single
368connection must be used. Use :class:`.SingletonThreadPool` if the scope is
369only needed within the current thread, or :class:`.StaticPool` is scope is
370needed within multiple threads for this case::
371
372 # maintain the same connection per thread
373 from sqlalchemy.pool import SingletonThreadPool
374
375 engine = create_engine("sqlite:///mydb.db", poolclass=SingletonThreadPool)
376
377
378 # maintain the same connection across all threads
379 from sqlalchemy.pool import StaticPool
380
381 engine = create_engine("sqlite:///mydb.db", poolclass=StaticPool)
382
383Note that :class:`.SingletonThreadPool` should be configured for the number
384of threads that are to be used; beyond that number, connections will be
385closed out in a non deterministic way.
386
387
388Dealing with Mixed String / Binary Columns
389------------------------------------------------------
390
391The SQLite database is weakly typed, and as such it is possible when using
392binary values, which in Python are represented as ``b'some string'``, that a
393particular SQLite database can have data values within different rows where
394some of them will be returned as a ``b''`` value by the Pysqlite driver, and
395others will be returned as Python strings, e.g. ``''`` values. This situation
396is not known to occur if the SQLAlchemy :class:`.LargeBinary` datatype is used
397consistently, however if a particular SQLite database has data that was
398inserted using the Pysqlite driver directly, or when using the SQLAlchemy
399:class:`.String` type which was later changed to :class:`.LargeBinary`, the
400table will not be consistently readable because SQLAlchemy's
401:class:`.LargeBinary` datatype does not handle strings so it has no way of
402"encoding" a value that is in string format.
403
404To deal with a SQLite table that has mixed string / binary data in the
405same column, use a custom type that will check each row individually::
406
407 from sqlalchemy import String
408 from sqlalchemy import TypeDecorator
409
410
411 class MixedBinary(TypeDecorator):
412 impl = String
413 cache_ok = True
414
415 def process_result_value(self, value, dialect):
416 if isinstance(value, str):
417 value = bytes(value, "utf-8")
418 elif value is not None:
419 value = bytes(value)
420
421 return value
422
423Then use the above ``MixedBinary`` datatype in the place where
424:class:`.LargeBinary` would normally be used.
425
426.. _pysqlite_serializable:
427
428Serializable isolation / Savepoints / Transactional DDL
429-------------------------------------------------------
430
431A newly revised version of this important section is now available
432at the top level of the SQLAlchemy SQLite documentation, in the section
433:ref:`sqlite_transactions`.
434
435
436.. _pysqlite_udfs:
437
438User-Defined Functions
439----------------------
440
441pysqlite supports a `create_function() <https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_function>`_
442method that allows us to create our own user-defined functions (UDFs) in Python and use them directly in SQLite queries.
443These functions are registered with a specific DBAPI Connection.
444
445SQLAlchemy uses connection pooling with file-based SQLite databases, so we need to ensure that the UDF is attached to the
446connection when it is created. That is accomplished with an event listener::
447
448 from sqlalchemy import create_engine
449 from sqlalchemy import event
450 from sqlalchemy import text
451
452
453 def udf():
454 return "udf-ok"
455
456
457 engine = create_engine("sqlite:///./db_file")
458
459
460 @event.listens_for(engine, "connect")
461 def connect(conn, rec):
462 conn.create_function("udf", 0, udf)
463
464
465 for i in range(5):
466 with engine.connect() as conn:
467 print(conn.scalar(text("SELECT UDF()")))
468
469""" # noqa
470
471from __future__ import annotations
472
473import math
474import os
475import re
476import sys
477from typing import Any
478from typing import Callable
479from typing import cast
480from typing import Optional
481from typing import Pattern
482from typing import TYPE_CHECKING
483from typing import TypeVar
484from typing import Union
485
486from .base import DATE
487from .base import DATETIME
488from .base import SQLiteDialect
489from ... import exc
490from ... import pool
491from ... import types as sqltypes
492from ... import util
493from ...util.typing import Self
494
495if TYPE_CHECKING:
496 from ...engine.interfaces import ConnectArgsType
497 from ...engine.interfaces import DBAPIConnection
498 from ...engine.interfaces import DBAPICursor
499 from ...engine.interfaces import DBAPIModule
500 from ...engine.interfaces import IsolationLevel
501 from ...engine.interfaces import ServerVersionInfoType
502 from ...engine.url import URL
503 from ...pool.base import PoolProxiedConnection
504 from ...sql.type_api import _BindProcessorType
505 from ...sql.type_api import _ResultProcessorType
506
507
508class _SQLite_pysqliteTimeStamp(DATETIME):
509 def bind_processor( # type: ignore[override]
510 self, dialect: SQLiteDialect
511 ) -> Optional[_BindProcessorType[Any]]:
512 if dialect.native_datetime:
513 return None
514 else:
515 return DATETIME.bind_processor(self, dialect)
516
517 def result_processor( # type: ignore[override]
518 self, dialect: SQLiteDialect, coltype: object
519 ) -> Optional[_ResultProcessorType[Any]]:
520 if dialect.native_datetime:
521 return None
522 else:
523 return DATETIME.result_processor(self, dialect, coltype)
524
525
526class _SQLite_pysqliteDate(DATE):
527 def bind_processor( # type: ignore[override]
528 self, dialect: SQLiteDialect
529 ) -> Optional[_BindProcessorType[Any]]:
530 if dialect.native_datetime:
531 return None
532 else:
533 return DATE.bind_processor(self, dialect)
534
535 def result_processor( # type: ignore[override]
536 self, dialect: SQLiteDialect, coltype: object
537 ) -> Optional[_ResultProcessorType[Any]]:
538 if dialect.native_datetime:
539 return None
540 else:
541 return DATE.result_processor(self, dialect, coltype)
542
543
544class SQLiteDialect_pysqlite(SQLiteDialect):
545 default_paramstyle = "qmark"
546 supports_statement_cache = True
547 returns_native_bytes = True
548
549 colspecs = util.update_copy(
550 SQLiteDialect.colspecs,
551 {
552 sqltypes.Date: _SQLite_pysqliteDate,
553 sqltypes.TIMESTAMP: _SQLite_pysqliteTimeStamp,
554 },
555 )
556
557 description_encoding = None
558
559 driver = "pysqlite"
560
561 @classmethod
562 def import_dbapi(cls) -> DBAPIModule:
563 from sqlite3 import dbapi2 as sqlite
564
565 return cast("DBAPIModule", sqlite)
566
567 @classmethod
568 def _is_url_file_db(cls, url: URL) -> bool:
569 if (url.database and url.database != ":memory:") and (
570 url.query.get("mode", None) != "memory"
571 ):
572 return True
573 else:
574 return False
575
576 @classmethod
577 def get_pool_class(cls, url: URL) -> type[pool.Pool]:
578 if cls._is_url_file_db(url):
579 return pool.QueuePool
580 else:
581 return pool.SingletonThreadPool
582
583 def _get_server_version_info(
584 self, connection: Any
585 ) -> ServerVersionInfoType:
586 return self.dbapi.sqlite_version_info # type: ignore[no-any-return, union-attr] # noqa: E501
587
588 def retrieve_dbapi_version(self, dbapi: DBAPIModule) -> util.VersionInfo:
589 # the ``sqlite3`` module ships with CPython and has no version of
590 # its own (the legacy ``sqlite3.version`` attribute was frozen at
591 # 2.6.0 and removed in Python 3.14), so the Python version is used
592 # here, which is what its feature set actually tracks. The version
593 # of the SQLite library itself is available as
594 # :attr:`.Dialect.server_version_info`.
595 return util.VersionInfo(sys.version_info[:3])
596
597 _isolation_lookup = SQLiteDialect._isolation_lookup.union(
598 {
599 "AUTOCOMMIT": None,
600 }
601 )
602
603 def set_isolation_level(
604 self, dbapi_connection: DBAPIConnection, level: IsolationLevel
605 ) -> None:
606 if level == "AUTOCOMMIT":
607 dbapi_connection.isolation_level = None
608 else:
609 dbapi_connection.isolation_level = ""
610 return super().set_isolation_level(dbapi_connection, level)
611
612 def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
613 return dbapi_conn.isolation_level is None
614
615 def on_connect(self) -> Callable[[DBAPIConnection], None]:
616 def regexp(a: str, b: Optional[str]) -> Optional[bool]:
617 if b is None:
618 return None
619 return re.search(a, b) is not None
620
621 if self._get_server_version_info(None) >= (3, 9):
622 # sqlite must be greater than 3.8.3 for deterministic=True
623 # https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_function
624 # the check is more conservative since there were still issues
625 # with following 3.8 sqlite versions
626 create_func_kw = {"deterministic": True}
627 else:
628 create_func_kw = {}
629
630 def set_regexp(dbapi_connection: DBAPIConnection) -> None:
631 dbapi_connection.create_function(
632 "regexp", 2, regexp, **create_func_kw
633 )
634
635 def floor_func(dbapi_connection: DBAPIConnection) -> None:
636 # NOTE: floor is optionally present in sqlite 3.35+ , however
637 # as it is normally non-present we deliver floor() unconditionally
638 # for now.
639 # https://www.sqlite.org/lang_mathfunc.html
640 dbapi_connection.create_function(
641 "floor", 1, math.floor, **create_func_kw
642 )
643
644 fns = [set_regexp, floor_func]
645
646 def connect(conn: DBAPIConnection) -> None:
647 for fn in fns:
648 fn(conn)
649
650 return connect
651
652 def create_connect_args(self, url: URL) -> ConnectArgsType:
653 if url.username or url.password or url.host or url.port:
654 raise exc.ArgumentError(
655 "Invalid SQLite URL: %s\n"
656 "Valid SQLite URL forms are:\n"
657 " sqlite:///:memory: (or, sqlite://)\n"
658 " sqlite:///relative/path/to/file.db\n"
659 " sqlite:////absolute/path/to/file.db" % (url,)
660 )
661
662 # theoretically, this list can be augmented, at least as far as
663 # parameter names accepted by sqlite3/pysqlite, using
664 # inspect.getfullargspec(). for the moment this seems like overkill
665 # as these parameters don't change very often, and as always,
666 # parameters passed to connect_args will always go to the
667 # sqlite3/pysqlite driver.
668 pysqlite_args = [
669 ("uri", bool),
670 ("timeout", float),
671 ("isolation_level", str),
672 ("detect_types", int),
673 ("check_same_thread", bool),
674 ("cached_statements", int),
675 ]
676 opts = url.query
677 pysqlite_opts: dict[str, Any] = {}
678 for key, type_ in pysqlite_args:
679 util.coerce_kw_type(opts, key, type_, dest=pysqlite_opts)
680
681 if pysqlite_opts.get("uri", False):
682 uri_opts = dict(opts)
683 # here, we are actually separating the parameters that go to
684 # sqlite3/pysqlite vs. those that go the SQLite URI. What if
685 # two names conflict? again, this seems to be not the case right
686 # now, and in the case that new names are added to
687 # either side which overlap, again the sqlite3/pysqlite parameters
688 # can be passed through connect_args instead of in the URL.
689 # If SQLite native URIs add a parameter like "timeout" that
690 # we already have listed here for the python driver, then we need
691 # to adjust for that here.
692 for key, type_ in pysqlite_args:
693 uri_opts.pop(key, None)
694 filename: str = url.database # type: ignore[assignment]
695 if uri_opts:
696 # sorting of keys is for unit test support
697 filename += "?" + (
698 "&".join(
699 "%s=%s" % (key, uri_opts[key])
700 for key in sorted(uri_opts)
701 )
702 )
703 else:
704 filename = url.database or ":memory:"
705 if filename != ":memory:":
706 filename = os.path.abspath(filename)
707
708 pysqlite_opts.setdefault(
709 "check_same_thread", not self._is_url_file_db(url)
710 )
711
712 return ([filename], pysqlite_opts)
713
714 def is_disconnect(
715 self,
716 e: DBAPIModule.Error,
717 connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]],
718 cursor: Optional[DBAPICursor],
719 ) -> bool:
720 self.dbapi = cast("DBAPIModule", self.dbapi)
721 return isinstance(
722 e, self.dbapi.ProgrammingError
723 ) and "Cannot operate on a closed database." in str(e)
724
725
726dialect = SQLiteDialect_pysqlite
727
728
729class _SQLiteDialect_pysqlite_numeric(SQLiteDialect_pysqlite):
730 """numeric dialect for testing only
731
732 internal use only. This dialect is **NOT** supported by SQLAlchemy
733 and may change at any time.
734
735 """
736
737 supports_statement_cache = True
738 default_paramstyle = "numeric"
739 driver = "pysqlite_numeric"
740
741 _first_bind = ":1"
742 _not_in_statement_regexp: Optional[Pattern[str]] = None
743
744 def __init__(self, *arg: Any, **kw: Any) -> None:
745 kw.setdefault("paramstyle", "numeric")
746 super().__init__(*arg, **kw)
747
748 def create_connect_args(self, url: URL) -> ConnectArgsType:
749 arg, opts = super().create_connect_args(url)
750 opts["factory"] = self._fix_sqlite_issue_99953()
751 return arg, opts
752
753 def _fix_sqlite_issue_99953(self) -> Any:
754 import sqlite3
755
756 first_bind = self._first_bind
757 if self._not_in_statement_regexp:
758 nis = self._not_in_statement_regexp
759
760 def _test_sql(sql: str) -> None:
761 m = nis.search(sql)
762 assert not m, f"Found {nis.pattern!r} in {sql!r}"
763
764 else:
765
766 def _test_sql(sql: str) -> None:
767 pass
768
769 def _numeric_param_as_dict(
770 parameters: Any,
771 ) -> Union[dict[str, Any], tuple[Any, ...]]:
772 if parameters:
773 assert isinstance(parameters, tuple)
774 return {
775 str(idx): value for idx, value in enumerate(parameters, 1)
776 }
777 else:
778 return ()
779
780 class SQLiteFix99953Cursor(sqlite3.Cursor):
781 def execute(self, sql: str, parameters: Any = ()) -> Self:
782 _test_sql(sql)
783 if first_bind in sql:
784 parameters = _numeric_param_as_dict(parameters)
785 return super().execute(sql, parameters)
786
787 def executemany(self, sql: str, parameters: Any) -> Self:
788 _test_sql(sql)
789 if first_bind in sql:
790 parameters = [
791 _numeric_param_as_dict(p) for p in parameters
792 ]
793 return super().executemany(sql, parameters)
794
795 class SQLiteFix99953Connection(sqlite3.Connection):
796 _CursorT = TypeVar("_CursorT", bound=sqlite3.Cursor)
797
798 def cursor(
799 self,
800 factory: Optional[
801 Callable[[sqlite3.Connection], _CursorT]
802 ] = None,
803 ) -> _CursorT:
804 if factory is None:
805 factory = SQLiteFix99953Cursor # type: ignore[assignment]
806 return super().cursor(factory=factory) # type: ignore[return-value] # noqa[E501]
807
808 def execute(
809 self, sql: str, parameters: Any = ()
810 ) -> sqlite3.Cursor:
811 _test_sql(sql)
812 if first_bind in sql:
813 parameters = _numeric_param_as_dict(parameters)
814 return super().execute(sql, parameters)
815
816 def executemany(self, sql: str, parameters: Any) -> sqlite3.Cursor:
817 _test_sql(sql)
818 if first_bind in sql:
819 parameters = [
820 _numeric_param_as_dict(p) for p in parameters
821 ]
822 return super().executemany(sql, parameters)
823
824 return SQLiteFix99953Connection
825
826
827class _SQLiteDialect_pysqlite_dollar(_SQLiteDialect_pysqlite_numeric):
828 """numeric dialect that uses $ for testing only
829
830 internal use only. This dialect is **NOT** supported by SQLAlchemy
831 and may change at any time.
832
833 """
834
835 supports_statement_cache = True
836 default_paramstyle = "numeric_dollar"
837 driver = "pysqlite_dollar"
838
839 _first_bind = "$1"
840 _not_in_statement_regexp = re.compile(r"[^\d]:\d+")
841
842 def __init__(self, *arg: Any, **kw: Any) -> None:
843 kw.setdefault("paramstyle", "numeric_dollar")
844 super().__init__(*arg, **kw)