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.. 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 re-use 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
393from __future__ import annotations
394
395import math
396import os
397import re
398from typing import Any
399from typing import Callable
400from typing import cast
401from typing import Optional
402from typing import Pattern
403from typing import TYPE_CHECKING
404from typing import TypeVar
405from typing import Union
406
407from .base import DATE
408from .base import DATETIME
409from .base import SQLiteDialect
410from ... import exc
411from ... import pool
412from ... import types as sqltypes
413from ... import util
414from ...util.typing import Self
415
416if TYPE_CHECKING:
417 from ...engine.interfaces import ConnectArgsType
418 from ...engine.interfaces import DBAPIConnection
419 from ...engine.interfaces import DBAPICursor
420 from ...engine.interfaces import DBAPIModule
421 from ...engine.interfaces import IsolationLevel
422 from ...engine.interfaces import VersionInfoType
423 from ...engine.url import URL
424 from ...pool.base import PoolProxiedConnection
425 from ...sql.type_api import _BindProcessorType
426 from ...sql.type_api import _ResultProcessorType
427
428
429class _SQLite_pysqliteTimeStamp(DATETIME):
430 def bind_processor( # type: ignore[override]
431 self, dialect: SQLiteDialect
432 ) -> Optional[_BindProcessorType[Any]]:
433 if dialect.native_datetime:
434 return None
435 else:
436 return DATETIME.bind_processor(self, dialect)
437
438 def result_processor( # type: ignore[override]
439 self, dialect: SQLiteDialect, coltype: object
440 ) -> Optional[_ResultProcessorType[Any]]:
441 if dialect.native_datetime:
442 return None
443 else:
444 return DATETIME.result_processor(self, dialect, coltype)
445
446
447class _SQLite_pysqliteDate(DATE):
448 def bind_processor( # type: ignore[override]
449 self, dialect: SQLiteDialect
450 ) -> Optional[_BindProcessorType[Any]]:
451 if dialect.native_datetime:
452 return None
453 else:
454 return DATE.bind_processor(self, dialect)
455
456 def result_processor( # type: ignore[override]
457 self, dialect: SQLiteDialect, coltype: object
458 ) -> Optional[_ResultProcessorType[Any]]:
459 if dialect.native_datetime:
460 return None
461 else:
462 return DATE.result_processor(self, dialect, coltype)
463
464
465class SQLiteDialect_pysqlite(SQLiteDialect):
466 default_paramstyle = "qmark"
467 supports_statement_cache = True
468 returns_native_bytes = True
469
470 colspecs = util.update_copy(
471 SQLiteDialect.colspecs,
472 {
473 sqltypes.Date: _SQLite_pysqliteDate,
474 sqltypes.TIMESTAMP: _SQLite_pysqliteTimeStamp,
475 },
476 )
477
478 description_encoding = None
479
480 driver = "pysqlite"
481
482 @classmethod
483 def import_dbapi(cls) -> DBAPIModule:
484 from sqlite3 import dbapi2 as sqlite
485
486 return cast("DBAPIModule", sqlite)
487
488 @classmethod
489 def _is_url_file_db(cls, url: URL) -> bool:
490 if (url.database and url.database != ":memory:") and (
491 url.query.get("mode", None) != "memory"
492 ):
493 return True
494 else:
495 return False
496
497 @classmethod
498 def get_pool_class(cls, url: URL) -> type[pool.Pool]:
499 if cls._is_url_file_db(url):
500 return pool.QueuePool
501 else:
502 return pool.SingletonThreadPool
503
504 def _get_server_version_info(self, connection: Any) -> VersionInfoType:
505 return self.dbapi.sqlite_version_info # type: ignore
506
507 _isolation_lookup = SQLiteDialect._isolation_lookup.union(
508 {
509 "AUTOCOMMIT": None,
510 }
511 )
512
513 def set_isolation_level(
514 self, dbapi_connection: DBAPIConnection, level: IsolationLevel
515 ) -> None:
516 if level == "AUTOCOMMIT":
517 dbapi_connection.isolation_level = None
518 else:
519 dbapi_connection.isolation_level = ""
520 return super().set_isolation_level(dbapi_connection, level)
521
522 def detect_autocommit_setting(self, dbapi_conn: DBAPIConnection) -> bool:
523 return dbapi_conn.isolation_level is None
524
525 def on_connect(self) -> Callable[[DBAPIConnection], None]:
526 def regexp(a: str, b: Optional[str]) -> Optional[bool]:
527 if b is None:
528 return None
529 return re.search(a, b) is not None
530
531 if self._get_server_version_info(None) >= (3, 9):
532 # sqlite must be greater than 3.8.3 for deterministic=True
533 # https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_function
534 # the check is more conservative since there were still issues
535 # with following 3.8 sqlite versions
536 create_func_kw = {"deterministic": True}
537 else:
538 create_func_kw = {}
539
540 def set_regexp(dbapi_connection: DBAPIConnection) -> None:
541 dbapi_connection.create_function(
542 "regexp", 2, regexp, **create_func_kw
543 )
544
545 def floor_func(dbapi_connection: DBAPIConnection) -> None:
546 # NOTE: floor is optionally present in sqlite 3.35+ , however
547 # as it is normally non-present we deliver floor() unconditionally
548 # for now.
549 # https://www.sqlite.org/lang_mathfunc.html
550 dbapi_connection.create_function(
551 "floor", 1, math.floor, **create_func_kw
552 )
553
554 fns = [set_regexp, floor_func]
555
556 def connect(conn: DBAPIConnection) -> None:
557 for fn in fns:
558 fn(conn)
559
560 return connect
561
562 def create_connect_args(self, url: URL) -> ConnectArgsType:
563 if url.username or url.password or url.host or url.port:
564 raise exc.ArgumentError(
565 "Invalid SQLite URL: %s\n"
566 "Valid SQLite URL forms are:\n"
567 " sqlite:///:memory: (or, sqlite://)\n"
568 " sqlite:///relative/path/to/file.db\n"
569 " sqlite:////absolute/path/to/file.db" % (url,)
570 )
571
572 # theoretically, this list can be augmented, at least as far as
573 # parameter names accepted by sqlite3/pysqlite, using
574 # inspect.getfullargspec(). for the moment this seems like overkill
575 # as these parameters don't change very often, and as always,
576 # parameters passed to connect_args will always go to the
577 # sqlite3/pysqlite driver.
578 pysqlite_args = [
579 ("uri", bool),
580 ("timeout", float),
581 ("isolation_level", str),
582 ("detect_types", int),
583 ("check_same_thread", bool),
584 ("cached_statements", int),
585 ]
586 opts = url.query
587 pysqlite_opts: dict[str, Any] = {}
588 for key, type_ in pysqlite_args:
589 util.coerce_kw_type(opts, key, type_, dest=pysqlite_opts)
590
591 if pysqlite_opts.get("uri", False):
592 uri_opts = dict(opts)
593 # here, we are actually separating the parameters that go to
594 # sqlite3/pysqlite vs. those that go the SQLite URI. What if
595 # two names conflict? again, this seems to be not the case right
596 # now, and in the case that new names are added to
597 # either side which overlap, again the sqlite3/pysqlite parameters
598 # can be passed through connect_args instead of in the URL.
599 # If SQLite native URIs add a parameter like "timeout" that
600 # we already have listed here for the python driver, then we need
601 # to adjust for that here.
602 for key, type_ in pysqlite_args:
603 uri_opts.pop(key, None)
604 filename: str = url.database # type: ignore[assignment]
605 if uri_opts:
606 # sorting of keys is for unit test support
607 filename += "?" + (
608 "&".join(
609 "%s=%s" % (key, uri_opts[key])
610 for key in sorted(uri_opts)
611 )
612 )
613 else:
614 filename = url.database or ":memory:"
615 if filename != ":memory:":
616 filename = os.path.abspath(filename)
617
618 pysqlite_opts.setdefault(
619 "check_same_thread", not self._is_url_file_db(url)
620 )
621
622 return ([filename], pysqlite_opts)
623
624 def is_disconnect(
625 self,
626 e: DBAPIModule.Error,
627 connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]],
628 cursor: Optional[DBAPICursor],
629 ) -> bool:
630 self.dbapi = cast("DBAPIModule", self.dbapi)
631 return isinstance(
632 e, self.dbapi.ProgrammingError
633 ) and "Cannot operate on a closed database." in str(e)
634
635
636dialect = SQLiteDialect_pysqlite
637
638
639class _SQLiteDialect_pysqlite_numeric(SQLiteDialect_pysqlite):
640 """numeric dialect for testing only
641
642 internal use only. This dialect is **NOT** supported by SQLAlchemy
643 and may change at any time.
644
645 """
646
647 supports_statement_cache = True
648 default_paramstyle = "numeric"
649 driver = "pysqlite_numeric"
650
651 _first_bind = ":1"
652 _not_in_statement_regexp: Optional[Pattern[str]] = None
653
654 def __init__(self, *arg: Any, **kw: Any) -> None:
655 kw.setdefault("paramstyle", "numeric")
656 super().__init__(*arg, **kw)
657
658 def create_connect_args(self, url: URL) -> ConnectArgsType:
659 arg, opts = super().create_connect_args(url)
660 opts["factory"] = self._fix_sqlite_issue_99953()
661 return arg, opts
662
663 def _fix_sqlite_issue_99953(self) -> Any:
664 import sqlite3
665
666 first_bind = self._first_bind
667 if self._not_in_statement_regexp:
668 nis = self._not_in_statement_regexp
669
670 def _test_sql(sql: str) -> None:
671 m = nis.search(sql)
672 assert not m, f"Found {nis.pattern!r} in {sql!r}"
673
674 else:
675
676 def _test_sql(sql: str) -> None:
677 pass
678
679 def _numeric_param_as_dict(
680 parameters: Any,
681 ) -> Union[dict[str, Any], tuple[Any, ...]]:
682 if parameters:
683 assert isinstance(parameters, tuple)
684 return {
685 str(idx): value for idx, value in enumerate(parameters, 1)
686 }
687 else:
688 return ()
689
690 class SQLiteFix99953Cursor(sqlite3.Cursor):
691 def execute(self, sql: str, parameters: Any = ()) -> Self:
692 _test_sql(sql)
693 if first_bind in sql:
694 parameters = _numeric_param_as_dict(parameters)
695 return super().execute(sql, parameters)
696
697 def executemany(self, sql: str, parameters: Any) -> Self:
698 _test_sql(sql)
699 if first_bind in sql:
700 parameters = [
701 _numeric_param_as_dict(p) for p in parameters
702 ]
703 return super().executemany(sql, parameters)
704
705 class SQLiteFix99953Connection(sqlite3.Connection):
706 _CursorT = TypeVar("_CursorT", bound=sqlite3.Cursor)
707
708 def cursor(
709 self,
710 factory: Optional[
711 Callable[[sqlite3.Connection], _CursorT]
712 ] = None,
713 ) -> _CursorT:
714 if factory is None:
715 factory = SQLiteFix99953Cursor # type: ignore[assignment]
716 return super().cursor(factory=factory) # type: ignore[return-value] # noqa[E501]
717
718 def execute(
719 self, sql: str, parameters: Any = ()
720 ) -> sqlite3.Cursor:
721 _test_sql(sql)
722 if first_bind in sql:
723 parameters = _numeric_param_as_dict(parameters)
724 return super().execute(sql, parameters)
725
726 def executemany(self, sql: str, parameters: Any) -> sqlite3.Cursor:
727 _test_sql(sql)
728 if first_bind in sql:
729 parameters = [
730 _numeric_param_as_dict(p) for p in parameters
731 ]
732 return super().executemany(sql, parameters)
733
734 return SQLiteFix99953Connection
735
736
737class _SQLiteDialect_pysqlite_dollar(_SQLiteDialect_pysqlite_numeric):
738 """numeric dialect that uses $ for testing only
739
740 internal use only. This dialect is **NOT** supported by SQLAlchemy
741 and may change at any time.
742
743 """
744
745 supports_statement_cache = True
746 default_paramstyle = "numeric_dollar"
747 driver = "pysqlite_dollar"
748
749 _first_bind = "$1"
750 _not_in_statement_regexp = re.compile(r"[^\d]:\d+")
751
752 def __init__(self, *arg: Any, **kw: Any) -> None:
753 kw.setdefault("paramstyle", "numeric_dollar")
754 super().__init__(*arg, **kw)