main commit
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-10-16 16:30:25 +09:00
parent 91c7e04474
commit 537e7b363f
1146 changed files with 45926 additions and 77196 deletions

View File

@@ -1,5 +1,5 @@
# dialects/sqlite/__init__.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# sqlite/__init__.py
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under

View File

@@ -1,9 +1,10 @@
# dialects/sqlite/aiosqlite.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# sqlite/aiosqlite.py
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
# mypy: ignore-errors
r"""
@@ -30,7 +31,6 @@ This dialect should normally be used only with the
:func:`_asyncio.create_async_engine` engine creation function::
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine("sqlite+aiosqlite:///filename")
The URL passes through all arguments to the ``pysqlite`` driver, so all
@@ -49,71 +49,45 @@ in Python and use them directly in SQLite queries as described here: :ref:`pysql
Serializable isolation / Savepoints / Transactional DDL (asyncio version)
-------------------------------------------------------------------------
A newly revised version of this important section is now available
at the top level of the SQLAlchemy SQLite documentation, in the section
:ref:`sqlite_transactions`.
Similarly to pysqlite, aiosqlite does not support SAVEPOINT feature.
The solution is similar to :ref:`pysqlite_serializable`. This is achieved by the event listeners in async::
.. _aiosqlite_pooling:
from sqlalchemy import create_engine, event
from sqlalchemy.ext.asyncio import create_async_engine
Pooling Behavior
----------------
engine = create_async_engine("sqlite+aiosqlite:///myfile.db")
The SQLAlchemy ``aiosqlite`` DBAPI establishes the connection pool differently
based on the kind of SQLite database that's requested:
@event.listens_for(engine.sync_engine, "connect")
def do_connect(dbapi_connection, connection_record):
# disable aiosqlite's emitting of the BEGIN statement entirely.
# also stops it from emitting COMMIT before any DDL.
dbapi_connection.isolation_level = None
* When a ``:memory:`` SQLite database is specified, the dialect by default
will use :class:`.StaticPool`. This pool maintains a single
connection, so that all access to the engine
use the same ``:memory:`` database.
* When a file-based database is specified, the dialect will use
:class:`.AsyncAdaptedQueuePool` as the source of connections.
@event.listens_for(engine.sync_engine, "begin")
def do_begin(conn):
# emit our own BEGIN
conn.exec_driver_sql("BEGIN")
.. versionchanged:: 2.0.38
SQLite file database engines now use :class:`.AsyncAdaptedQueuePool` by default.
Previously, :class:`.NullPool` were used. The :class:`.NullPool` class
may be used by specifying it via the
:paramref:`_sa.create_engine.poolclass` parameter.
.. warning:: When using the above recipe, it is advised to not use the
:paramref:`.Connection.execution_options.isolation_level` setting on
:class:`_engine.Connection` and :func:`_sa.create_engine`
with the SQLite driver,
as this function necessarily will also alter the ".isolation_level" setting.
""" # noqa
from __future__ import annotations
import asyncio
from collections import deque
from functools import partial
from types import ModuleType
from typing import Any
from typing import cast
from typing import Deque
from typing import Iterator
from typing import NoReturn
from typing import Optional
from typing import Sequence
from typing import TYPE_CHECKING
from typing import Union
from .base import SQLiteExecutionContext
from .pysqlite import SQLiteDialect_pysqlite
from ... import pool
from ... import util
from ...connectors.asyncio import AsyncAdapt_dbapi_module
from ...engine import AdaptedConnection
from ...util.concurrency import await_fallback
from ...util.concurrency import await_only
if TYPE_CHECKING:
from ...connectors.asyncio import AsyncIODBAPIConnection
from ...connectors.asyncio import AsyncIODBAPICursor
from ...engine.interfaces import _DBAPICursorDescription
from ...engine.interfaces import _DBAPIMultiExecuteParams
from ...engine.interfaces import _DBAPISingleExecuteParams
from ...engine.interfaces import DBAPIConnection
from ...engine.interfaces import DBAPICursor
from ...engine.interfaces import DBAPIModule
from ...engine.url import URL
from ...pool.base import PoolProxiedConnection
class AsyncAdapt_aiosqlite_cursor:
# TODO: base on connectors/asyncio.py
@@ -132,26 +106,21 @@ class AsyncAdapt_aiosqlite_cursor:
server_side = False
def __init__(self, adapt_connection: AsyncAdapt_aiosqlite_connection):
def __init__(self, adapt_connection):
self._adapt_connection = adapt_connection
self._connection = adapt_connection._connection
self.await_ = adapt_connection.await_
self.arraysize = 1
self.rowcount = -1
self.description: Optional[_DBAPICursorDescription] = None
self._rows: Deque[Any] = deque()
self.description = None
self._rows = []
def close(self) -> None:
self._rows.clear()
def execute(
self,
operation: Any,
parameters: Optional[_DBAPISingleExecuteParams] = None,
) -> Any:
def close(self):
self._rows[:] = []
def execute(self, operation, parameters=None):
try:
_cursor: AsyncIODBAPICursor = self.await_(self._connection.cursor()) # type: ignore[arg-type] # noqa: E501
_cursor = self.await_(self._connection.cursor())
if parameters is None:
self.await_(_cursor.execute(operation))
@@ -163,7 +132,7 @@ class AsyncAdapt_aiosqlite_cursor:
self.lastrowid = self.rowcount = -1
if not self.server_side:
self._rows = deque(self.await_(_cursor.fetchall()))
self._rows = self.await_(_cursor.fetchall())
else:
self.description = None
self.lastrowid = _cursor.lastrowid
@@ -172,17 +141,13 @@ class AsyncAdapt_aiosqlite_cursor:
if not self.server_side:
self.await_(_cursor.close())
else:
self._cursor = _cursor # type: ignore[misc]
self._cursor = _cursor
except Exception as error:
self._adapt_connection._handle_exception(error)
def executemany(
self,
operation: Any,
seq_of_parameters: _DBAPIMultiExecuteParams,
) -> Any:
def executemany(self, operation, seq_of_parameters):
try:
_cursor: AsyncIODBAPICursor = self.await_(self._connection.cursor()) # type: ignore[arg-type] # noqa: E501
_cursor = self.await_(self._connection.cursor())
self.await_(_cursor.executemany(operation, seq_of_parameters))
self.description = None
self.lastrowid = _cursor.lastrowid
@@ -191,29 +156,30 @@ class AsyncAdapt_aiosqlite_cursor:
except Exception as error:
self._adapt_connection._handle_exception(error)
def setinputsizes(self, *inputsizes: Any) -> None:
def setinputsizes(self, *inputsizes):
pass
def __iter__(self) -> Iterator[Any]:
def __iter__(self):
while self._rows:
yield self._rows.popleft()
yield self._rows.pop(0)
def fetchone(self) -> Optional[Any]:
def fetchone(self):
if self._rows:
return self._rows.popleft()
return self._rows.pop(0)
else:
return None
def fetchmany(self, size: Optional[int] = None) -> Sequence[Any]:
def fetchmany(self, size=None):
if size is None:
size = self.arraysize
rr = self._rows
return [rr.popleft() for _ in range(min(size, len(rr)))]
retval = self._rows[0:size]
self._rows[:] = self._rows[size:]
return retval
def fetchall(self) -> Sequence[Any]:
retval = list(self._rows)
self._rows.clear()
def fetchall(self):
retval = self._rows[:]
self._rows[:] = []
return retval
@@ -224,27 +190,24 @@ class AsyncAdapt_aiosqlite_ss_cursor(AsyncAdapt_aiosqlite_cursor):
server_side = True
def __init__(self, *arg: Any, **kw: Any) -> None:
def __init__(self, *arg, **kw):
super().__init__(*arg, **kw)
self._cursor: Optional[AsyncIODBAPICursor] = None
self._cursor = None
def close(self) -> None:
def close(self):
if self._cursor is not None:
self.await_(self._cursor.close())
self._cursor = None
def fetchone(self) -> Optional[Any]:
assert self._cursor is not None
def fetchone(self):
return self.await_(self._cursor.fetchone())
def fetchmany(self, size: Optional[int] = None) -> Sequence[Any]:
assert self._cursor is not None
def fetchmany(self, size=None):
if size is None:
size = self.arraysize
return self.await_(self._cursor.fetchmany(size=size))
def fetchall(self) -> Sequence[Any]:
assert self._cursor is not None
def fetchall(self):
return self.await_(self._cursor.fetchall())
@@ -252,24 +215,22 @@ class AsyncAdapt_aiosqlite_connection(AdaptedConnection):
await_ = staticmethod(await_only)
__slots__ = ("dbapi",)
def __init__(self, dbapi: Any, connection: AsyncIODBAPIConnection) -> None:
def __init__(self, dbapi, connection):
self.dbapi = dbapi
self._connection = connection
@property
def isolation_level(self) -> Optional[str]:
return cast(str, self._connection.isolation_level)
def isolation_level(self):
return self._connection.isolation_level
@isolation_level.setter
def isolation_level(self, value: Optional[str]) -> None:
def isolation_level(self, value):
# aiosqlite's isolation_level setter works outside the Thread
# that it's supposed to, necessitating setting check_same_thread=False.
# for improved stability, we instead invent our own awaitable version
# using aiosqlite's async queue directly.
def set_iso(
connection: AsyncAdapt_aiosqlite_connection, value: Optional[str]
) -> None:
def set_iso(connection, value):
connection.isolation_level = value
function = partial(set_iso, self._connection._conn, value)
@@ -278,38 +239,38 @@ class AsyncAdapt_aiosqlite_connection(AdaptedConnection):
self._connection._tx.put_nowait((future, function))
try:
self.await_(future)
return self.await_(future)
except Exception as error:
self._handle_exception(error)
def create_function(self, *args: Any, **kw: Any) -> None:
def create_function(self, *args, **kw):
try:
self.await_(self._connection.create_function(*args, **kw))
except Exception as error:
self._handle_exception(error)
def cursor(self, server_side: bool = False) -> AsyncAdapt_aiosqlite_cursor:
def cursor(self, server_side=False):
if server_side:
return AsyncAdapt_aiosqlite_ss_cursor(self)
else:
return AsyncAdapt_aiosqlite_cursor(self)
def execute(self, *args: Any, **kw: Any) -> Any:
def execute(self, *args, **kw):
return self.await_(self._connection.execute(*args, **kw))
def rollback(self) -> None:
def rollback(self):
try:
self.await_(self._connection.rollback())
except Exception as error:
self._handle_exception(error)
def commit(self) -> None:
def commit(self):
try:
self.await_(self._connection.commit())
except Exception as error:
self._handle_exception(error)
def close(self) -> None:
def close(self):
try:
self.await_(self._connection.close())
except ValueError:
@@ -325,7 +286,7 @@ class AsyncAdapt_aiosqlite_connection(AdaptedConnection):
except Exception as error:
self._handle_exception(error)
def _handle_exception(self, error: Exception) -> NoReturn:
def _handle_exception(self, error):
if (
isinstance(error, ValueError)
and error.args[0] == "no active connection"
@@ -343,14 +304,14 @@ class AsyncAdaptFallback_aiosqlite_connection(AsyncAdapt_aiosqlite_connection):
await_ = staticmethod(await_fallback)
class AsyncAdapt_aiosqlite_dbapi(AsyncAdapt_dbapi_module):
def __init__(self, aiosqlite: ModuleType, sqlite: ModuleType):
class AsyncAdapt_aiosqlite_dbapi:
def __init__(self, aiosqlite, sqlite):
self.aiosqlite = aiosqlite
self.sqlite = sqlite
self.paramstyle = "qmark"
self._init_dbapi_attributes()
def _init_dbapi_attributes(self) -> None:
def _init_dbapi_attributes(self):
for name in (
"DatabaseError",
"Error",
@@ -369,7 +330,7 @@ class AsyncAdapt_aiosqlite_dbapi(AsyncAdapt_dbapi_module):
for name in ("Binary",):
setattr(self, name, getattr(self.sqlite, name))
def connect(self, *arg: Any, **kw: Any) -> AsyncAdapt_aiosqlite_connection:
def connect(self, *arg, **kw):
async_fallback = kw.pop("async_fallback", False)
creator_fn = kw.pop("async_creator_fn", None)
@@ -393,7 +354,7 @@ class AsyncAdapt_aiosqlite_dbapi(AsyncAdapt_dbapi_module):
class SQLiteExecutionContext_aiosqlite(SQLiteExecutionContext):
def create_server_side_cursor(self) -> DBAPICursor:
def create_server_side_cursor(self):
return self._dbapi_connection.cursor(server_side=True)
@@ -408,25 +369,19 @@ class SQLiteDialect_aiosqlite(SQLiteDialect_pysqlite):
execution_ctx_cls = SQLiteExecutionContext_aiosqlite
@classmethod
def import_dbapi(cls) -> AsyncAdapt_aiosqlite_dbapi:
def import_dbapi(cls):
return AsyncAdapt_aiosqlite_dbapi(
__import__("aiosqlite"), __import__("sqlite3")
)
@classmethod
def get_pool_class(cls, url: URL) -> type[pool.Pool]:
def get_pool_class(cls, url):
if cls._is_url_file_db(url):
return pool.AsyncAdaptedQueuePool
return pool.NullPool
else:
return pool.StaticPool
def is_disconnect(
self,
e: DBAPIModule.Error,
connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]],
cursor: Optional[DBAPICursor],
) -> bool:
self.dbapi = cast("DBAPIModule", self.dbapi)
def is_disconnect(self, e, connection, cursor):
if isinstance(
e, self.dbapi.OperationalError
) and "no active connection" in str(e):
@@ -434,10 +389,8 @@ class SQLiteDialect_aiosqlite(SQLiteDialect_pysqlite):
return super().is_disconnect(e, connection, cursor)
def get_driver_connection(
self, connection: DBAPIConnection
) -> AsyncIODBAPIConnection:
return connection._connection # type: ignore[no-any-return]
def get_driver_connection(self, connection):
return connection._connection
dialect = SQLiteDialect_aiosqlite

View File

@@ -1,5 +1,5 @@
# dialects/sqlite/dml.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# sqlite/dml.py
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
@@ -7,10 +7,6 @@
from __future__ import annotations
from typing import Any
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union
from .._typing import _OnConflictIndexElementsT
from .._typing import _OnConflictIndexWhereT
@@ -19,7 +15,6 @@ from .._typing import _OnConflictWhereT
from ... import util
from ...sql import coercions
from ...sql import roles
from ...sql import schema
from ...sql._typing import _DMLTableArgument
from ...sql.base import _exclusive_against
from ...sql.base import _generative
@@ -27,9 +22,7 @@ from ...sql.base import ColumnCollection
from ...sql.base import ReadOnlyColumnCollection
from ...sql.dml import Insert as StandardInsert
from ...sql.elements import ClauseElement
from ...sql.elements import ColumnElement
from ...sql.elements import KeyedColumnElement
from ...sql.elements import TextClause
from ...sql.expression import alias
from ...util.typing import Self
@@ -148,10 +141,11 @@ class Insert(StandardInsert):
:paramref:`.Insert.on_conflict_do_update.set_` dictionary.
:param where:
Optional argument. An expression object representing a ``WHERE``
clause that restricts the rows affected by ``DO UPDATE SET``. Rows not
meeting the ``WHERE`` condition will not be updated (effectively a
``DO NOTHING`` for those rows).
Optional argument. If present, can be a literal SQL
string or an acceptable expression for a ``WHERE`` clause
that restricts the rows affected by ``DO UPDATE SET``. Rows
not meeting the ``WHERE`` condition will not be updated
(effectively a ``DO NOTHING`` for those rows).
"""
@@ -190,10 +184,9 @@ class Insert(StandardInsert):
class OnConflictClause(ClauseElement):
stringify_dialect = "sqlite"
inferred_target_elements: Optional[List[Union[str, schema.Column[Any]]]]
inferred_target_whereclause: Optional[
Union[ColumnElement[Any], TextClause]
]
constraint_target: None
inferred_target_elements: _OnConflictIndexElementsT
inferred_target_whereclause: _OnConflictIndexWhereT
def __init__(
self,
@@ -201,22 +194,13 @@ class OnConflictClause(ClauseElement):
index_where: _OnConflictIndexWhereT = None,
):
if index_elements is not None:
self.inferred_target_elements = [
coercions.expect(roles.DDLConstraintColumnRole, column)
for column in index_elements
]
self.inferred_target_whereclause = (
coercions.expect(
roles.WhereHavingRole,
index_where,
)
if index_where is not None
else None
)
self.constraint_target = None
self.inferred_target_elements = index_elements
self.inferred_target_whereclause = index_where
else:
self.inferred_target_elements = (
self.inferred_target_whereclause
) = None
self.constraint_target = (
self.inferred_target_elements
) = self.inferred_target_whereclause = None
class OnConflictDoNothing(OnConflictClause):
@@ -226,9 +210,6 @@ class OnConflictDoNothing(OnConflictClause):
class OnConflictDoUpdate(OnConflictClause):
__visit_name__ = "on_conflict_do_update"
update_values_to_set: List[Tuple[Union[schema.Column[Any], str], Any]]
update_whereclause: Optional[ColumnElement[Any]]
def __init__(
self,
index_elements: _OnConflictIndexElementsT = None,
@@ -256,8 +237,4 @@ class OnConflictDoUpdate(OnConflictClause):
(coercions.expect(roles.DMLColumnRole, key), value)
for key, value in set_.items()
]
self.update_whereclause = (
coercions.expect(roles.WhereHavingRole, where)
if where is not None
else None
)
self.update_whereclause = where

View File

@@ -1,9 +1,3 @@
# dialects/sqlite/json.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
# mypy: ignore-errors
from ... import types as sqltypes

View File

@@ -1,9 +1,3 @@
# dialects/sqlite/provision.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
# mypy: ignore-errors
import os
@@ -52,6 +46,8 @@ def _format_url(url, driver, ident):
assert "test_schema" not in filename
tokens = re.split(r"[_\.]", filename)
new_filename = f"{driver}"
for token in tokens:
if token in _drivernames:
if driver is None:

View File

@@ -1,5 +1,5 @@
# dialects/sqlite/pysqlcipher.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# sqlite/pysqlcipher.py
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
@@ -39,7 +39,7 @@ Current dialect selection logic is:
e = create_engine(
"sqlite+pysqlcipher://:password@/dbname.db",
module=sqlcipher_compatible_driver,
module=sqlcipher_compatible_driver
)
These drivers make use of the SQLCipher engine. This system essentially
@@ -55,12 +55,12 @@ The format of the connect string is in every way the same as that
of the :mod:`~sqlalchemy.dialects.sqlite.pysqlite` driver, except that the
"password" field is now accepted, which should contain a passphrase::
e = create_engine("sqlite+pysqlcipher://:testing@/foo.db")
e = create_engine('sqlite+pysqlcipher://:testing@/foo.db')
For an absolute file path, two leading slashes should be used for the
database name::
e = create_engine("sqlite+pysqlcipher://:testing@//path/to/foo.db")
e = create_engine('sqlite+pysqlcipher://:testing@//path/to/foo.db')
A selection of additional encryption-related pragmas supported by SQLCipher
as documented at https://www.zetetic.net/sqlcipher/sqlcipher-api/ can be passed
@@ -68,9 +68,7 @@ in the query string, and will result in that PRAGMA being called for each
new connection. Currently, ``cipher``, ``kdf_iter``
``cipher_page_size`` and ``cipher_use_hmac`` are supported::
e = create_engine(
"sqlite+pysqlcipher://:testing@/foo.db?cipher=aes-256-cfb&kdf_iter=64000"
)
e = create_engine('sqlite+pysqlcipher://:testing@/foo.db?cipher=aes-256-cfb&kdf_iter=64000')
.. warning:: Previous versions of sqlalchemy did not take into consideration
the encryption-related pragmas passed in the url string, that were silently

View File

@@ -1,5 +1,5 @@
# dialects/sqlite/pysqlite.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# sqlite/pysqlite.py
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
@@ -28,9 +28,7 @@ Connect Strings
---------------
The file specification for the SQLite database is taken as the "database"
portion of the URL. Note that the format of a SQLAlchemy url is:
.. sourcecode:: text
portion of the URL. Note that the format of a SQLAlchemy url is::
driver://user:pass@host/database
@@ -39,28 +37,25 @@ the **right** of the third slash. So connecting to a relative filepath
looks like::
# relative path
e = create_engine("sqlite:///path/to/database.db")
e = create_engine('sqlite:///path/to/database.db')
An absolute path, which is denoted by starting with a slash, means you
need **four** slashes::
# absolute path
e = create_engine("sqlite:////path/to/database.db")
e = create_engine('sqlite:////path/to/database.db')
To use a Windows path, regular drive specifications and backslashes can be
used. Double backslashes are probably needed::
# absolute path on Windows
e = create_engine("sqlite:///C:\\path\\to\\database.db")
e = create_engine('sqlite:///C:\\path\\to\\database.db')
To use sqlite ``:memory:`` database specify it as the filename using
``sqlite:///:memory:``. It's also the default if no filepath is
present, specifying only ``sqlite://`` and nothing else::
The sqlite ``:memory:`` identifier is the default if no filepath is
present. Specify ``sqlite://`` and nothing else::
# in-memory database (note three slashes)
e = create_engine("sqlite:///:memory:")
# also in-memory database
e2 = create_engine("sqlite://")
# in-memory database
e = create_engine('sqlite://')
.. _pysqlite_uri_connections:
@@ -100,9 +95,7 @@ Above, the pysqlite / sqlite3 DBAPI would be passed arguments as::
sqlite3.connect(
"file:path/to/database?mode=ro&nolock=1",
check_same_thread=True,
timeout=10,
uri=True,
check_same_thread=True, timeout=10, uri=True
)
Regarding future parameters added to either the Python or native drivers. new
@@ -148,11 +141,8 @@ as follows::
def regexp(a, b):
return re.search(a, b) is not None
sqlite_connection.create_function(
"regexp",
2,
regexp,
"regexp", 2, regexp,
)
There is currently no support for regular expression flags as a separate
@@ -193,12 +183,10 @@ Keeping in mind that pysqlite's parsing option is not recommended,
nor should be necessary, for use with SQLAlchemy, usage of PARSE_DECLTYPES
can be forced if one configures "native_datetime=True" on create_engine()::
engine = create_engine(
"sqlite://",
connect_args={
"detect_types": sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES
},
native_datetime=True,
engine = create_engine('sqlite://',
connect_args={'detect_types':
sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES},
native_datetime=True
)
With this flag enabled, the DATE and TIMESTAMP types (but note - not the
@@ -253,7 +241,6 @@ Pooling may be disabled for a file based database by specifying the
parameter::
from sqlalchemy import NullPool
engine = create_engine("sqlite:///myfile.db", poolclass=NullPool)
It's been observed that the :class:`.NullPool` implementation incurs an
@@ -273,12 +260,9 @@ globally, and the ``check_same_thread`` flag can be passed to Pysqlite
as ``False``::
from sqlalchemy.pool import StaticPool
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
engine = create_engine('sqlite://',
connect_args={'check_same_thread':False},
poolclass=StaticPool)
Note that using a ``:memory:`` database in multiple threads requires a recent
version of SQLite.
@@ -297,14 +281,14 @@ needed within multiple threads for this case::
# maintain the same connection per thread
from sqlalchemy.pool import SingletonThreadPool
engine = create_engine("sqlite:///mydb.db", poolclass=SingletonThreadPool)
engine = create_engine('sqlite:///mydb.db',
poolclass=SingletonThreadPool)
# maintain the same connection across all threads
from sqlalchemy.pool import StaticPool
engine = create_engine("sqlite:///mydb.db", poolclass=StaticPool)
engine = create_engine('sqlite:///mydb.db',
poolclass=StaticPool)
Note that :class:`.SingletonThreadPool` should be configured for the number
of threads that are to be used; beyond that number, connections will be
@@ -333,14 +317,13 @@ same column, use a custom type that will check each row individually::
from sqlalchemy import String
from sqlalchemy import TypeDecorator
class MixedBinary(TypeDecorator):
impl = String
cache_ok = True
def process_result_value(self, value, dialect):
if isinstance(value, str):
value = bytes(value, "utf-8")
value = bytes(value, 'utf-8')
elif value is not None:
value = bytes(value)
@@ -354,10 +337,74 @@ Then use the above ``MixedBinary`` datatype in the place where
Serializable isolation / Savepoints / Transactional DDL
-------------------------------------------------------
A newly revised version of this important section is now available
at the top level of the SQLAlchemy SQLite documentation, in the section
:ref:`sqlite_transactions`.
In the section :ref:`sqlite_concurrency`, we refer to the pysqlite
driver's assortment of issues that prevent several features of SQLite
from working correctly. The pysqlite DBAPI driver has several
long-standing bugs which impact the correctness of its transactional
behavior. In its default mode of operation, SQLite features such as
SERIALIZABLE isolation, transactional DDL, and SAVEPOINT support are
non-functional, and in order to use these features, workarounds must
be taken.
The issue is essentially that the driver attempts to second-guess the user's
intent, failing to start transactions and sometimes ending them prematurely, in
an effort to minimize the SQLite databases's file locking behavior, even
though SQLite itself uses "shared" locks for read-only activities.
SQLAlchemy chooses to not alter this behavior by default, as it is the
long-expected behavior of the pysqlite driver; if and when the pysqlite
driver attempts to repair these issues, that will be more of a driver towards
defaults for SQLAlchemy.
The good news is that with a few events, we can implement transactional
support fully, by disabling pysqlite's feature entirely and emitting BEGIN
ourselves. This is achieved using two event listeners::
from sqlalchemy import create_engine, event
engine = create_engine("sqlite:///myfile.db")
@event.listens_for(engine, "connect")
def do_connect(dbapi_connection, connection_record):
# disable pysqlite's emitting of the BEGIN statement entirely.
# also stops it from emitting COMMIT before any DDL.
dbapi_connection.isolation_level = None
@event.listens_for(engine, "begin")
def do_begin(conn):
# emit our own BEGIN
conn.exec_driver_sql("BEGIN")
.. warning:: When using the above recipe, it is advised to not use the
:paramref:`.Connection.execution_options.isolation_level` setting on
:class:`_engine.Connection` and :func:`_sa.create_engine`
with the SQLite driver,
as this function necessarily will also alter the ".isolation_level" setting.
Above, we intercept a new pysqlite connection and disable any transactional
integration. Then, at the point at which SQLAlchemy knows that transaction
scope is to begin, we emit ``"BEGIN"`` ourselves.
When we take control of ``"BEGIN"``, we can also control directly SQLite's
locking modes, introduced at
`BEGIN TRANSACTION <https://sqlite.org/lang_transaction.html>`_,
by adding the desired locking mode to our ``"BEGIN"``::
@event.listens_for(engine, "begin")
def do_begin(conn):
conn.exec_driver_sql("BEGIN EXCLUSIVE")
.. seealso::
`BEGIN TRANSACTION <https://sqlite.org/lang_transaction.html>`_ -
on the SQLite site
`sqlite3 SELECT does not BEGIN a transaction <https://bugs.python.org/issue9924>`_ -
on the Python bug tracker
`sqlite3 module breaks transactions and potentially corrupts data <https://bugs.python.org/issue10740>`_ -
on the Python bug tracker
.. _pysqlite_udfs:
@@ -392,16 +439,12 @@ connection when it is created. That is accomplished with an event listener::
with engine.connect() as conn:
print(conn.scalar(text("SELECT UDF()")))
""" # noqa
from __future__ import annotations
import math
import os
import re
from typing import cast
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
from .base import DATE
from .base import DATETIME
@@ -411,13 +454,6 @@ from ... import pool
from ... import types as sqltypes
from ... import util
if TYPE_CHECKING:
from ...engine.interfaces import DBAPIConnection
from ...engine.interfaces import DBAPICursor
from ...engine.interfaces import DBAPIModule
from ...engine.url import URL
from ...pool.base import PoolProxiedConnection
class _SQLite_pysqliteTimeStamp(DATETIME):
def bind_processor(self, dialect):
@@ -471,7 +507,7 @@ class SQLiteDialect_pysqlite(SQLiteDialect):
return sqlite
@classmethod
def _is_url_file_db(cls, url: URL):
def _is_url_file_db(cls, url):
if (url.database and url.database != ":memory:") and (
url.query.get("mode", None) != "memory"
):
@@ -502,9 +538,6 @@ class SQLiteDialect_pysqlite(SQLiteDialect):
dbapi_connection.isolation_level = ""
return super().set_isolation_level(dbapi_connection, level)
def detect_autocommit_setting(self, dbapi_connection):
return dbapi_connection.isolation_level is None
def on_connect(self):
def regexp(a, b):
if b is None:
@@ -604,13 +637,7 @@ class SQLiteDialect_pysqlite(SQLiteDialect):
return ([filename], pysqlite_opts)
def is_disconnect(
self,
e: DBAPIModule.Error,
connection: Optional[Union[PoolProxiedConnection, DBAPIConnection]],
cursor: Optional[DBAPICursor],
) -> bool:
self.dbapi = cast("DBAPIModule", self.dbapi)
def is_disconnect(self, e, connection, cursor):
return isinstance(
e, self.dbapi.ProgrammingError
) and "Cannot operate on a closed database." in str(e)