diff options
Diffstat (limited to 'lib')
31 files changed, 1171 insertions, 484 deletions
diff --git a/lib/sqlalchemy/connectors/pyodbc.py b/lib/sqlalchemy/connectors/pyodbc.py index 4d684acbb..18c1085aa 100644 --- a/lib/sqlalchemy/connectors/pyodbc.py +++ b/lib/sqlalchemy/connectors/pyodbc.py @@ -174,18 +174,20 @@ class PyODBCConnector(Connector): ] ) - def get_isolation_level_values(self, dbapi_conn): - return super().get_isolation_level_values(dbapi_conn) + ["AUTOCOMMIT"] + def get_isolation_level_values(self, dbapi_connection): + return super().get_isolation_level_values(dbapi_connection) + [ + "AUTOCOMMIT" + ] - def set_isolation_level(self, connection, level): + def set_isolation_level(self, dbapi_connection, level): # adjust for ConnectionFairy being present # allows attribute set e.g. "connection.autocommit = True" # to work properly - if hasattr(connection, "dbapi_connection"): - connection = connection.dbapi_connection if level == "AUTOCOMMIT": - connection.autocommit = True + dbapi_connection.autocommit = True else: - connection.autocommit = False - super(PyODBCConnector, self).set_isolation_level(connection, level) + dbapi_connection.autocommit = False + super(PyODBCConnector, self).set_isolation_level( + dbapi_connection, level + ) diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 353c78c76..f0a7364a3 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -2766,25 +2766,25 @@ class MSDialect(default.DefaultDialect): ] ) - def get_isolation_level_values(self, dbapi_conn): + def get_isolation_level_values(self, dbapi_connection): return list(self._isolation_lookup) - def set_isolation_level(self, connection, level): - cursor = connection.cursor() - cursor.execute("SET TRANSACTION ISOLATION LEVEL %s" % level) + def set_isolation_level(self, dbapi_connection, level): + cursor = dbapi_connection.cursor() + cursor.execute(f"SET TRANSACTION ISOLATION LEVEL {level}") cursor.close() if level == "SNAPSHOT": - connection.commit() + dbapi_connection.commit() - def get_isolation_level(self, connection): + def get_isolation_level(self, dbapi_connection): last_error = None views = ("sys.dm_exec_sessions", "sys.dm_pdw_nodes_exec_sessions") for view in views: - cursor = connection.cursor() + cursor = dbapi_connection.cursor() try: cursor.execute( - """ + f""" SELECT CASE transaction_isolation_level WHEN 0 THEN NULL WHEN 1 THEN 'READ UNCOMMITTED' @@ -2792,10 +2792,9 @@ class MSDialect(default.DefaultDialect): WHEN 3 THEN 'REPEATABLE READ' WHEN 4 THEN 'SERIALIZABLE' WHEN 5 THEN 'SNAPSHOT' END AS TRANSACTION_ISOLATION_LEVEL - FROM %s + FROM {view} where session_id = @@SPID """ - % view ) val = cursor.fetchone()[0] except self.dbapi.Error as err: @@ -2811,12 +2810,12 @@ class MSDialect(default.DefaultDialect): # DefaultDialect, so the warning here is all that displays util.warn( "Could not fetch transaction isolation level, " - "tried views: %s; final error was: %s" % (views, last_error) + f"tried views: {views}; final error was: {last_error}" ) raise NotImplementedError( "Can't fetch isolation level on this particular " - "SQL Server version. tried views: %s; final error was: %s" - % (views, last_error) + f"SQL Server version. tried views: {views}; final " + f"error was: {last_error}" ) def initialize(self, connection): diff --git a/lib/sqlalchemy/dialects/mssql/pymssql.py b/lib/sqlalchemy/dialects/mssql/pymssql.py index 18bee1890..a9dc97d54 100644 --- a/lib/sqlalchemy/dialects/mssql/pymssql.py +++ b/lib/sqlalchemy/dialects/mssql/pymssql.py @@ -125,16 +125,18 @@ class MSDialect_pymssql(MSDialect): else: return False - def get_isolation_level_values(self, dbapi_conn): - return super().get_isolation_level_values(dbapi_conn) + ["AUTOCOMMIT"] + def get_isolation_level_values(self, dbapi_connection): + return super().get_isolation_level_values(dbapi_connection) + [ + "AUTOCOMMIT" + ] - def set_isolation_level(self, connection, level): + def set_isolation_level(self, dbapi_connection, level): if level == "AUTOCOMMIT": - connection.autocommit(True) + dbapi_connection.autocommit(True) else: - connection.autocommit(False) + dbapi_connection.autocommit(False) super(MSDialect_pymssql, self).set_isolation_level( - connection, level + dbapi_connection, level ) diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index f77d839f3..fef1ec81a 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -2404,14 +2404,14 @@ class MySQLDialect(default.DefaultDialect): "REPEATABLE READ", ) - def set_isolation_level(self, dbapi_conn, level): - cursor = dbapi_conn.cursor() - cursor.execute("SET SESSION TRANSACTION ISOLATION LEVEL %s" % level) + def set_isolation_level(self, dbapi_connection, level): + cursor = dbapi_connection.cursor() + cursor.execute(f"SET SESSION TRANSACTION ISOLATION LEVEL {level}") cursor.execute("COMMIT") cursor.close() - def get_isolation_level(self, connection): - cursor = connection.cursor() + def get_isolation_level(self, dbapi_connection): + cursor = dbapi_connection.cursor() if self._is_mysql and self.server_version_info >= (5, 7, 20): cursor.execute("SELECT @@transaction_isolation") else: diff --git a/lib/sqlalchemy/dialects/mysql/mysqldb.py b/lib/sqlalchemy/dialects/mysql/mysqldb.py index 1e57c779d..40f1207d0 100644 --- a/lib/sqlalchemy/dialects/mysql/mysqldb.py +++ b/lib/sqlalchemy/dialects/mysql/mysqldb.py @@ -308,7 +308,7 @@ class MySQLDialect_mysqldb(MySQLDialect): else: return cset_name() - def get_isolation_level_values(self, dbapi_conn): + def get_isolation_level_values(self, dbapi_connection): return ( "SERIALIZABLE", "READ UNCOMMITTED", @@ -317,13 +317,13 @@ class MySQLDialect_mysqldb(MySQLDialect): "AUTOCOMMIT", ) - def set_isolation_level(self, dbapi_conn, level): + def set_isolation_level(self, dbapi_connection, level): if level == "AUTOCOMMIT": - dbapi_conn.autocommit(True) + dbapi_connection.autocommit(True) else: - dbapi_conn.autocommit(False) + dbapi_connection.autocommit(False) super(MySQLDialect_mysqldb, self).set_isolation_level( - dbapi_conn, level + dbapi_connection, level ) diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index 7df2422b3..63131bf95 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -1576,7 +1576,7 @@ class OracleDialect(default.DefaultDialect): # use the default return None - def get_isolation_level_values(self, dbapi_conn): + def get_isolation_level_values(self, dbapi_connection): return ["READ COMMITTED", "SERIALIZABLE"] def get_default_isolation_level(self, dbapi_conn): diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index a1d093886..eecf8567c 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -972,7 +972,7 @@ class OracleDialect_cx_oracle(OracleDialect): super(OracleDialect_cx_oracle, self).initialize(connection) self._detect_decimal_char(connection) - def get_isolation_level(self, connection): + def get_isolation_level(self, dbapi_connection): # sources: # general idea of transaction id, have to start one, etc. @@ -984,7 +984,7 @@ class OracleDialect_cx_oracle(OracleDialect): # Oracle tuple comparison without using IN: # https://www.sql-workbench.eu/comparison/tuple_comparison.html - with connection.cursor() as cursor: + with dbapi_connection.cursor() as cursor: # this is the only way to ensure a transaction is started without # actually running DML. There's no way to see the configured # isolation level without getting it from v$transaction which @@ -1019,21 +1019,19 @@ class OracleDialect_cx_oracle(OracleDialect): return result - def get_isolation_level_values(self, dbapi_conn): - return super().get_isolation_level_values(dbapi_conn) + ["AUTOCOMMIT"] + def get_isolation_level_values(self, dbapi_connection): + return super().get_isolation_level_values(dbapi_connection) + [ + "AUTOCOMMIT" + ] - def set_isolation_level(self, connection, level): - if hasattr(connection, "dbapi_connection"): - dbapi_connection = connection.dbapi_connection - else: - dbapi_connection = connection + def set_isolation_level(self, dbapi_connection, level): if level == "AUTOCOMMIT": dbapi_connection.autocommit = True else: dbapi_connection.autocommit = False - connection.rollback() - with connection.cursor() as cursor: - cursor.execute("ALTER SESSION SET ISOLATION_LEVEL=%s" % level) + dbapi_connection.rollback() + with dbapi_connection.cursor() as cursor: + cursor.execute(f"ALTER SESSION SET ISOLATION_LEVEL={level}") def _detect_decimal_char(self, connection): # we have the option to change this setting upon connect, diff --git a/lib/sqlalchemy/dialects/postgresql/_psycopg_common.py b/lib/sqlalchemy/dialects/postgresql/_psycopg_common.py index d82d5f009..a3a378947 100644 --- a/lib/sqlalchemy/dialects/postgresql/_psycopg_common.py +++ b/lib/sqlalchemy/dialects/postgresql/_psycopg_common.py @@ -150,7 +150,7 @@ class _PGDialect_common_psycopg(PGDialect): # requires that "dsn" be present as a blank string. return ([""], opts) - def get_isolation_level_values(self, dbapi_conn): + def get_isolation_level_values(self, dbapi_connection): return ( "AUTOCOMMIT", "READ COMMITTED", diff --git a/lib/sqlalchemy/dialects/postgresql/asyncpg.py b/lib/sqlalchemy/dialects/postgresql/asyncpg.py index 1fdb46b6f..4951107bc 100644 --- a/lib/sqlalchemy/dialects/postgresql/asyncpg.py +++ b/lib/sqlalchemy/dialects/postgresql/asyncpg.py @@ -871,11 +871,11 @@ class PGDialect_asyncpg(PGDialect): "SERIALIZABLE": "serializable", } - def get_isolation_level_values(self, dbapi_conn): + def get_isolation_level_values(self, dbapi_connection): return list(self._isolation_lookup) - def set_isolation_level(self, connection, level): - connection.set_isolation_level(self._isolation_lookup[level]) + def set_isolation_level(self, dbapi_connection, level): + dbapi_connection.set_isolation_level(self._isolation_lookup[level]) def set_readonly(self, connection, value): connection.readonly = value diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index d1d881dc3..614c84b56 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -3251,17 +3251,17 @@ class PGDialect(default.DefaultDialect): "REPEATABLE READ", ) - def set_isolation_level(self, connection, level): - cursor = connection.cursor() + def set_isolation_level(self, dbapi_connection, level): + cursor = dbapi_connection.cursor() cursor.execute( "SET SESSION CHARACTERISTICS AS TRANSACTION " - "ISOLATION LEVEL %s" % level + f"ISOLATION LEVEL {level}" ) cursor.execute("COMMIT") cursor.close() - def get_isolation_level(self, connection): - cursor = connection.cursor() + def get_isolation_level(self, dbapi_connection): + cursor = dbapi_connection.cursor() cursor.execute("show transaction isolation level") val = cursor.fetchone()[0] cursor.close() diff --git a/lib/sqlalchemy/dialects/postgresql/pg8000.py b/lib/sqlalchemy/dialects/postgresql/pg8000.py index ede953195..1904a1ae1 100644 --- a/lib/sqlalchemy/dialects/postgresql/pg8000.py +++ b/lib/sqlalchemy/dialects/postgresql/pg8000.py @@ -446,7 +446,7 @@ class PGDialect_pg8000(PGDialect): # connection was closed normally return "connection is closed" in str(e) - def get_isolation_level_values(self, dbapi_conn): + def get_isolation_level_values(self, dbapi_connection): return ( "AUTOCOMMIT", "READ COMMITTED", @@ -455,21 +455,17 @@ class PGDialect_pg8000(PGDialect): "SERIALIZABLE", ) - def set_isolation_level(self, connection, level): + def set_isolation_level(self, dbapi_connection, level): level = level.replace("_", " ") - # adjust for ConnectionFairy possibly being present - if hasattr(connection, "dbapi_connection"): - connection = connection.dbapi_connection - if level == "AUTOCOMMIT": - connection.autocommit = True + dbapi_connection.autocommit = True else: - connection.autocommit = False - cursor = connection.cursor() + dbapi_connection.autocommit = False + cursor = dbapi_connection.cursor() cursor.execute( "SET SESSION CHARACTERISTICS AS TRANSACTION " - "ISOLATION LEVEL %s" % level + f"ISOLATION LEVEL {level}" ) cursor.execute("COMMIT") cursor.close() @@ -516,13 +512,13 @@ class PGDialect_pg8000(PGDialect): return val == "on" - def set_client_encoding(self, connection, client_encoding): - # adjust for ConnectionFairy possibly being present - if hasattr(connection, "dbapi_connection"): - connection = connection.dbapi_connection - - cursor = connection.cursor() - cursor.execute("SET CLIENT_ENCODING TO '" + client_encoding + "'") + def _set_client_encoding(self, dbapi_connection, client_encoding): + cursor = dbapi_connection.cursor() + cursor.execute( + f"""SET CLIENT_ENCODING TO '{ + client_encoding.replace("'", "''") + }'""" + ) cursor.execute("COMMIT") cursor.close() @@ -556,7 +552,7 @@ class PGDialect_pg8000(PGDialect): if self.client_encoding is not None: def on_connect(conn): - self.set_client_encoding(conn, self.client_encoding) + self._set_client_encoding(conn, self.client_encoding) fns.append(on_connect) diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg.py b/lib/sqlalchemy/dialects/postgresql/psycopg.py index c2017c975..4219b4fff 100644 --- a/lib/sqlalchemy/dialects/postgresql/psycopg.py +++ b/lib/sqlalchemy/dialects/postgresql/psycopg.py @@ -322,9 +322,6 @@ class PGDialect_psycopg(_PGDialect_common_psycopg): connection.isolation_level = isolation_level def get_isolation_level(self, dbapi_connection): - if hasattr(dbapi_connection, "dbapi_connection"): - dbapi_connection = dbapi_connection.dbapi_connection - status_before = dbapi_connection.info.transaction_status value = super().get_isolation_level(dbapi_connection) @@ -334,15 +331,14 @@ class PGDialect_psycopg(_PGDialect_common_psycopg): dbapi_connection.rollback() return value - def set_isolation_level(self, connection, level): - connection = getattr(connection, "dbapi_connection", connection) + def set_isolation_level(self, dbapi_connection, level): if level == "AUTOCOMMIT": self._do_isolation_level( - connection, autocommit=True, isolation_level=None + dbapi_connection, autocommit=True, isolation_level=None ) else: self._do_isolation_level( - connection, + dbapi_connection, autocommit=False, isolation_level=self._isolation_lookup[level], ) diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2.py b/lib/sqlalchemy/dialects/postgresql/psycopg2.py index 3d9f90a29..0bbad3257 100644 --- a/lib/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/lib/sqlalchemy/dialects/postgresql/psycopg2.py @@ -597,7 +597,8 @@ class PGDialect_psycopg2(_PGDialect_common_psycopg): super(PGDialect_psycopg2, self).initialize(connection) self._has_native_hstore = ( self.use_native_hstore - and self._hstore_oids(connection.connection) is not None + and self._hstore_oids(connection.connection.dbapi_connection) + is not None ) # PGDialect.initialize() checks server version for <= 8.2 and sets @@ -639,8 +640,8 @@ class PGDialect_psycopg2(_PGDialect_common_psycopg): "SERIALIZABLE": extensions.ISOLATION_LEVEL_SERIALIZABLE, } - def set_isolation_level(self, connection, level): - connection.set_isolation_level(self._isolation_lookup[level]) + def set_isolation_level(self, dbapi_connection, level): + dbapi_connection.set_isolation_level(self._isolation_lookup[level]) def set_readonly(self, connection, value): connection.readonly = value @@ -660,47 +661,47 @@ class PGDialect_psycopg2(_PGDialect_common_psycopg): fns = [] if self.client_encoding is not None: - def on_connect(conn): - conn.set_client_encoding(self.client_encoding) + def on_connect(dbapi_conn): + dbapi_conn.set_client_encoding(self.client_encoding) fns.append(on_connect) if self.dbapi and self.use_native_uuid: - def on_connect(conn): - extras.register_uuid(None, conn) + def on_connect(dbapi_conn): + extras.register_uuid(None, dbapi_conn) fns.append(on_connect) if self.dbapi and self.use_native_hstore: - def on_connect(conn): - hstore_oids = self._hstore_oids(conn) + def on_connect(dbapi_conn): + hstore_oids = self._hstore_oids(dbapi_conn) if hstore_oids is not None: oid, array_oid = hstore_oids kw = {"oid": oid} kw["array_oid"] = array_oid - extras.register_hstore(conn, **kw) + extras.register_hstore(dbapi_conn, **kw) fns.append(on_connect) if self.dbapi and self._json_deserializer: - def on_connect(conn): + def on_connect(dbapi_conn): extras.register_default_json( - conn, loads=self._json_deserializer + dbapi_conn, loads=self._json_deserializer ) extras.register_default_jsonb( - conn, loads=self._json_deserializer + dbapi_conn, loads=self._json_deserializer ) fns.append(on_connect) if fns: - def on_connect(conn): + def on_connect(dbapi_conn): for fn in fns: - fn(conn) + fn(dbapi_conn) return on_connect else: @@ -781,11 +782,10 @@ class PGDialect_psycopg2(_PGDialect_common_psycopg): ) @util.memoized_instancemethod - def _hstore_oids(self, conn): + def _hstore_oids(self, dbapi_connection): + extras = self._psycopg2_extras - if hasattr(conn, "dbapi_connection"): - conn = conn.dbapi_connection - oids = extras.HstoreAdapter.get_oids(conn) + oids = extras.HstoreAdapter.get_oids(dbapi_connection) if oids is not None and oids[0]: return oids[0:2] else: diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index 3dea23e18..0c7f8d839 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -1915,18 +1915,18 @@ class SQLiteDialect(default.DefaultDialect): {"READ UNCOMMITTED": 1, "SERIALIZABLE": 0} ) - def get_isolation_level_values(self, dbapi_conn): + def get_isolation_level_values(self, dbapi_connection): return list(self._isolation_lookup) - def set_isolation_level(self, connection, level): + def set_isolation_level(self, dbapi_connection, level): isolation_level = self._isolation_lookup[level] - cursor = connection.cursor() - cursor.execute("PRAGMA read_uncommitted = %d" % isolation_level) + cursor = dbapi_connection.cursor() + cursor.execute(f"PRAGMA read_uncommitted = {isolation_level}") cursor.close() - def get_isolation_level(self, connection): - cursor = connection.cursor() + def get_isolation_level(self, dbapi_connection): + cursor = dbapi_connection.cursor() cursor.execute("PRAGMA read_uncommitted") res = cursor.fetchone() if res: diff --git a/lib/sqlalchemy/dialects/sqlite/pysqlite.py b/lib/sqlalchemy/dialects/sqlite/pysqlite.py index 45a35be65..944d714a3 100644 --- a/lib/sqlalchemy/dialects/sqlite/pysqlite.py +++ b/lib/sqlalchemy/dialects/sqlite/pysqlite.py @@ -489,18 +489,14 @@ class SQLiteDialect_pysqlite(SQLiteDialect): } ) - def set_isolation_level(self, connection, level): - if hasattr(connection, "dbapi_connection"): - dbapi_connection = connection.dbapi_connection - else: - dbapi_connection = connection + def set_isolation_level(self, dbapi_connection, level): if level == "AUTOCOMMIT": dbapi_connection.isolation_level = None else: dbapi_connection.isolation_level = "" return super(SQLiteDialect_pysqlite, self).set_isolation_level( - connection, level + dbapi_connection, level ) def on_connect(self): @@ -509,11 +505,7 @@ class SQLiteDialect_pysqlite(SQLiteDialect): return None return re.search(a, b) is not None - def set_regexp(connection): - if hasattr(connection, "dbapi_connection"): - dbapi_connection = connection.dbapi_connection - else: - dbapi_connection = connection + def set_regexp(dbapi_connection): dbapi_connection.create_function( "regexp", 2, diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 5f4c5be47..50e43ff2c 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -35,7 +35,6 @@ from .cursor import ResultProxy from .interfaces import AdaptedConnection from .interfaces import BindTyping from .interfaces import Compiled -from .interfaces import Connectable from .interfaces import CreateEnginePlugin from .interfaces import Dialect from .interfaces import ExceptionContext diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index bfadcbce6..fbd8fe7df 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -6,9 +6,13 @@ # the MIT License: https://www.opensource.org/licenses/mit-license.php import contextlib import sys +import typing +from typing import Any +from typing import Mapping +from typing import Optional +from typing import Union from .interfaces import BindTyping -from .interfaces import Connectable from .interfaces import ConnectionEventsTarget from .interfaces import ExceptionContext from .util import _distill_params_20 @@ -20,6 +24,11 @@ from .. import util from ..sql import compiler from ..sql import util as sql_util +if typing.TYPE_CHECKING: + from .interfaces import Dialect + from .url import URL + from ..pool import Pool + from ..pool import PoolProxiedConnection """Defines :class:`_engine.Connection` and :class:`_engine.Engine`. @@ -29,7 +38,7 @@ _EMPTY_EXECUTION_OPTS = util.immutabledict() NO_OPTIONS = util.immutabledict() -class Connection(Connectable): +class Connection(ConnectionEventsTarget): """Provides high-level functionality for a wrapped DB-API connection. The :class:`_engine.Connection` object is procured by calling @@ -364,7 +373,7 @@ class Connection(Connectable): return self._dbapi_connection is None and not self.closed @property - def connection(self): + def connection(self) -> "PoolProxiedConnection": """The underlying DB-API connection managed by this Connection. This is a SQLAlchemy connection-pool proxied connection @@ -422,7 +431,9 @@ class Connection(Connectable): """ try: - return self.dialect.get_isolation_level(self.connection) + return self.dialect.get_isolation_level( + self.connection.dbapi_connection + ) except BaseException as e: self._handle_dbapi_exception(e, None, None, None, None) @@ -2296,14 +2307,14 @@ class Engine(ConnectionEventsTarget, log.Identified): def __init__( self, - pool, - dialect, - url, - logging_name=None, - echo=None, - query_cache_size=500, - execution_options=None, - hide_parameters=False, + pool: "Pool", + dialect: "Dialect", + url: "URL", + logging_name: Optional[str] = None, + echo: Union[None, str, bool] = None, + query_cache_size: int = 500, + execution_options: Optional[Mapping[str, Any]] = None, + hide_parameters: bool = False, ): self.pool = pool self.url = url diff --git a/lib/sqlalchemy/engine/create.py b/lib/sqlalchemy/engine/create.py index 8fcba7503..c344cbe33 100644 --- a/lib/sqlalchemy/engine/create.py +++ b/lib/sqlalchemy/engine/create.py @@ -61,7 +61,7 @@ def create_engine(url, **kwargs): constructs:: engine = create_engine("mysql+mysqldb://scott:tiger@hostname/dbname", - encoding='latin1', echo=True) + pool_recycle=3600, echo=True) The string form of the URL is ``dialect[+driver]://user:password@host/dbname[?key=value..]``, where @@ -589,6 +589,7 @@ def create_engine(url, **kwargs): engine = engineclass(pool, dialect, u, **engine_args) if _initialize: + do_on_connect = dialect.on_connect_url(u) if do_on_connect: @@ -604,7 +605,9 @@ def create_engine(url, **kwargs): def first_connect(dbapi_connection, connection_record): c = base.Connection( engine, - connection=dbapi_connection, + connection=poollib._AdhocProxiedConnection( + dbapi_connection, connection_record + ), _has_events=False, # reconnecting will be a reentrant condition, so if the # connection goes away, Connection is then closed diff --git a/lib/sqlalchemy/engine/cursor.py b/lib/sqlalchemy/engine/cursor.py index 7f2b8b412..8247987fa 100644 --- a/lib/sqlalchemy/engine/cursor.py +++ b/lib/sqlalchemy/engine/cursor.py @@ -435,11 +435,6 @@ class CursorResultMetaData(ResultMetaData): dialect = context.dialect translate_colname = context._translate_colname - description_decoder = ( - dialect._description_decoder - if dialect.description_encoding - else None - ) normalize_name = ( dialect.normalize_name if dialect.requires_name_normalize else None ) @@ -451,9 +446,6 @@ class CursorResultMetaData(ResultMetaData): colname = rec[0] coltype = rec[1] - if description_decoder: - colname = description_decoder(colname) - if translate_colname: colname, untranslated = translate_colname(colname) diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 64500b41b..9574e9980 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -110,8 +110,6 @@ class DefaultDialect(interfaces.Dialect): # *not* the FLOAT type however. supports_native_decimal = False - description_encoding = None - name = "default" # length at which to truncate @@ -221,6 +219,11 @@ class DefaultDialect(interfaces.Dialect): NO_CACHE_KEY = NO_CACHE_KEY NO_DIALECT_SUPPORT = NO_DIALECT_SUPPORT + # TODO: this is not to be part of 2.0. implement rudimentary binary + # literals for SQLite, PostgreSQL, MySQL only within + # _Binary.literal_processor + _legacy_binary_type_literal_encoding = "utf-8" + @util.deprecated_params( empty_in_strategy=( "1.4", @@ -242,7 +245,6 @@ class DefaultDialect(interfaces.Dialect): ) def __init__( self, - encoding="utf-8", paramstyle=None, isolation_level=None, dbapi=None, @@ -274,7 +276,6 @@ class DefaultDialect(interfaces.Dialect): ) self.bind_typing = interfaces.BindTyping.SETINPUTSIZES - self.encoding = encoding self.positional = False self._ischema = None self.dbapi = dbapi @@ -387,7 +388,7 @@ class DefaultDialect(interfaces.Dialect): try: self.default_isolation_level = self.get_default_isolation_level( - connection.connection + connection.connection.dbapi_connection ) except NotImplementedError: self.default_isolation_level = None diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index faaf073ab..90e0ea5d2 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -8,9 +8,410 @@ """Define core interfaces used by the engine system.""" from enum import Enum - +from typing import Any +from typing import Callable +from typing import Dict +from typing import List +from typing import Mapping +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import Type +from typing import TYPE_CHECKING +from typing import Union + +from ..pool import PoolProxiedConnection from ..sql.compiler import Compiled # noqa from ..sql.compiler import TypeCompiler # noqa +from ..util.typing import _TypeToInstance +from ..util.typing import NotRequired +from ..util.typing import Protocol +from ..util.typing import TypedDict + +if TYPE_CHECKING: + from .base import Connection + from .base import Engine + from .url import URL + from ..sql.compiler import DDLCompiler + from ..sql.compiler import IdentifierPreparer + from ..sql.compiler import SQLCompiler + from ..sql.type_api import TypeEngine + + +class DBAPIConnection(Protocol): + """protocol representing a :pep:`249` database connection. + + .. versionadded:: 2.0 + + .. seealso:: + + `Connection Objects <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_ + - in :pep:`249` + + """ # noqa: E501 + + def close(self) -> None: + ... + + def commit(self) -> None: + ... + + def cursor(self) -> "DBAPICursor": + ... + + def rollback(self) -> None: + ... + + +class DBAPIType(Protocol): + """protocol representing a :pep:`249` database type. + + .. versionadded:: 2.0 + + .. seealso:: + + `Type Objects <https://www.python.org/dev/peps/pep-0249/#type-objects>`_ + - in :pep:`249` + + """ # noqa: E501 + + +class DBAPICursor(Protocol): + """protocol representing a :pep:`249` database cursor. + + .. versionadded:: 2.0 + + .. seealso:: + + `Cursor Objects <https://www.python.org/dev/peps/pep-0249/#cursor-objects>`_ + - in :pep:`249` + + """ # noqa: E501 + + @property + def description( + self, + ) -> Sequence[ + Tuple[ + str, + "DBAPIType", + Optional[int], + Optional[int], + Optional[int], + Optional[int], + Optional[bool], + ] + ]: + """The description attribute of the Cursor. + + .. seealso:: + + `cursor.description <https://www.python.org/dev/peps/pep-0249/#description>`_ + - in :pep:`249` + + + """ # noqa: E501 + ... + + @property + def rowcount(self) -> int: + ... + + arraysize: int + + def close(self) -> None: + ... + + def execute( + self, + operation: Any, + parameters: Optional[Union[Sequence[Any], Mapping[str, Any]]], + ) -> Any: + ... + + def executemany( + self, + operation: Any, + parameters: Sequence[Union[Sequence[Any], Mapping[str, Any]]], + ) -> Any: + ... + + def fetchone(self) -> Optional[Any]: + ... + + def fetchmany(self, size: int = ...) -> Sequence[Any]: + ... + + def fetchall(self) -> Sequence[Any]: + ... + + def setinputsizes(self, sizes: Sequence[Any]) -> None: + ... + + def setoutputsize(self, size: Any, column: Any) -> None: + ... + + def callproc(self, procname: str, parameters: Sequence[Any] = ...) -> Any: + ... + + def nextset(self) -> Optional[bool]: + ... + + +class ReflectedIdentity(TypedDict): + """represent the reflected IDENTITY structure of a column, corresponding + to the :class:`_schema.Identity` construct. + + The :class:`.ReflectedIdentity` structure is part of the + :class:`.ReflectedColumn` structure, which is returned by the + :meth:`.Inspector.get_columns` method. + + """ + + always: bool + """type of identity column""" + + on_null: bool + """indicates ON NULL""" + + start: int + """starting index of the sequence""" + + increment: int + """increment value of the sequence""" + + minvalue: int + """the minimum value of the sequence.""" + + maxvalue: int + """the maximum value of the sequence.""" + + nominvalue: bool + """no minimum value of the sequence.""" + + nomaxvalue: bool + """no maximum value of the sequence.""" + + cycle: bool + """allows the sequence to wrap around when the maxvalue + or minvalue has been reached.""" + + cache: Optional[int] + """number of future values in the + sequence which are calculated in advance.""" + + order: bool + """if true, renders the ORDER keyword.""" + + +class ReflectedComputed(TypedDict): + """Represent the reflected elements of a computed column, corresponding + to the :class:`_schema.Computed` construct. + + The :class:`.ReflectedComputed` structure is part of the + :class:`.ReflectedColumn` structure, which is returned by the + :meth:`.Inspector.get_columns` method. + + """ + + sqltext: str + """the expression used to generate this column returned + as a string SQL expression""" + + persisted: bool + """indicates if the value is stored or computed on demand""" + + +class ReflectedColumn(TypedDict): + """Dictionary representing the reflected elements corresponding to + a :class:`_schema.Column` object. + + The :class:`.ReflectedColumn` structure is returned by the + :class:`.Inspector.get_columns` method. + + """ + + name: str + """column name""" + + type: "TypeEngine" + """column type represented as a :class:`.TypeEngine` instance.""" + + nullable: bool + """column nullability""" + + default: str + """column default expression as a SQL string""" + + autoincrement: NotRequired[bool] + """database-dependent autoincrement flag. + + This flag indicates if the column has a database-side "autoincrement" + flag of some kind. Within SQLAlchemy, other kinds of columns may + also act as an "autoincrement" column without necessarily having + such a flag on them. + + See :paramref:`_schema.Column.autoincrement` for more background on + "autoincrement". + + """ + + comment: NotRequired[Optional[str]] + """comment for the column, if present""" + + computed: NotRequired[Optional[ReflectedComputed]] + """indicates this column is computed at insert (possibly update) time by + the database.""" + + identity: NotRequired[Optional[ReflectedIdentity]] + """indicates this column is an IDENTITY column""" + + dialect_options: NotRequired[Dict[str, Any]] + """Additional dialect-specific options detected for this reflected + object""" + + +class ReflectedCheckConstraint(TypedDict): + """Dictionary representing the reflected elements corresponding to + :class:`.CheckConstraint`. + + The :class:`.ReflectedCheckConstraint` structure is returned by the + :meth:`.Inspector.get_check_constraints` method. + + """ + + name: Optional[str] + """constraint name""" + + sqltext: str + """the check constraint's SQL expression""" + + dialect_options: NotRequired[Dict[str, Any]] + """Additional dialect-specific options detected for this reflected + object""" + + +class ReflectedUniqueConstraint(TypedDict): + """Dictionary representing the reflected elements corresponding to + :class:`.UniqueConstraint`. + + The :class:`.ReflectedUniqueConstraint` structure is returned by the + :meth:`.Inspector.get_unique_constraints` method. + + """ + + name: Optional[str] + """constraint name""" + + column_names: List[str] + """column names which comprise the constraint""" + + dialect_options: NotRequired[Dict[str, Any]] + """Additional dialect-specific options detected for this reflected + object""" + + +class ReflectedPrimaryKeyConstraint(TypedDict): + """Dictionary representing the reflected elements corresponding to + :class:`.PrimaryKeyConstraint`. + + The :class:`.ReflectedPrimaryKeyConstraint` structure is returned by the + :meth:`.Inspector.get_pk_constraint` method. + + """ + + name: Optional[str] + """constraint name""" + + constrained_columns: List[str] + """column names which comprise the constraint""" + + dialect_options: NotRequired[Dict[str, Any]] + """Additional dialect-specific options detected for this reflected + object""" + + +class ReflectedForeignKeyConstraint(TypedDict): + """Dictionary representing the reflected elements corresponding to + :class:`.ForeignKeyConstraint`. + + The :class:`.ReflectedForeignKeyConstraint` structure is returned by + the :meth:`.Inspector.get_foreign_keys` method. + + """ + + name: Optional[str] + """constraint name""" + + constrained_columns: List[str] + """local column names which comprise the constraint""" + + referred_schema: Optional[str] + """schema name of the table being referenced""" + + referred_table: str + """name of the table being referenced""" + + referred_columns: List[str] + """referenced column names""" + + dialect_options: NotRequired[Dict[str, Any]] + """Additional dialect-specific options detected for this reflected + object""" + + +class ReflectedIndex(TypedDict): + """Dictionary representing the reflected elements corresponding to + :class:`.Index`. + + The :class:`.ReflectedIndex` structure is returned by the + :meth:`.Inspector.get_indexes` method. + + """ + + name: Optional[str] + """constraint name""" + + column_names: List[str] + """column names which the index refers towards""" + + unique: bool + """whether or not the index has a unique flag""" + + duplicates_constraint: NotRequired[bool] + """boolean indicating this index mirrors a unique constraint of the same + name""" + + include_columns: NotRequired[List[str]] + """columns to include in the INCLUDE clause for supporting databases. + + .. deprecated:: 2.0 + + Legacy value, will be replaced with + ``d["dialect_options"][<dialect name>]["include"]`` + + """ + + column_sorting: NotRequired[Dict[str, Tuple[str]]] + """optional dict mapping column names to tuple of sort keywords, + which may include ``asc``, ``desc``, ``nulls_first``, ``nulls_last``.""" + + dialect_options: NotRequired[Dict[str, Any]] + """Additional dialect-specific options detected for this reflected + object""" + + +class ReflectedTableComment(TypedDict): + """Dictionary representing the reflected comment corresponding to + the :attr:`_schema.Table.comment` attribute. + + The :class:`.ReflectedTableComment` structure is returned by the + :meth:`.Inspector.get_table_comment` method. + + """ + + text: str + """text of the comment""" class BindTyping(Enum): @@ -75,120 +476,157 @@ class Dialect: directly. Instead, subclass :class:`.default.DefaultDialect` or descendant class. - All dialects include the following attributes. There are many other - attributes that may be supported as well: + """ - ``name`` - identifying name for the dialect from a DBAPI-neutral point of view + name: str + """identifying name for the dialect from a DBAPI-neutral point of view (i.e. 'sqlite') + """ - ``driver`` - identifying name for the dialect's DBAPI + driver: str + """identifying name for the dialect's DBAPI""" - ``positional`` - True if the paramstyle for this Dialect is positional. + positional: bool + """True if the paramstyle for this Dialect is positional.""" - ``paramstyle`` - the paramstyle to be used (some DB-APIs support multiple + paramstyle: str + """the paramstyle to be used (some DB-APIs support multiple paramstyles). + """ - ``encoding`` - type of encoding to use for unicode, usually defaults to - 'utf-8'. + statement_compiler: Type["SQLCompiler"] + """a :class:`.Compiled` class used to compile SQL statements""" - ``statement_compiler`` - a :class:`.Compiled` class used to compile SQL statements + ddl_compiler: Type["DDLCompiler"] + """a :class:`.Compiled` class used to compile DDL statements""" - ``ddl_compiler`` - a :class:`.Compiled` class used to compile DDL statements + type_compiler: _TypeToInstance["TypeCompiler"] + """a :class:`.Compiled` class used to compile SQL type objects""" - ``server_version_info`` - a tuple containing a version number for the DB backend in use. - This value is only available for supporting dialects, and is - typically populated during the initial connection to the database. + preparer: Type["IdentifierPreparer"] + """a :class:`.IdentifierPreparer` class used to + quote identifiers. + """ - ``default_schema_name`` - the name of the default schema. This value is only available for - supporting dialects, and is typically populated during the - initial connection to the database. + identifier_preparer: "IdentifierPreparer" + """This element will refer to an instance of :class:`.IdentifierPreparer` + once a :class:`.DefaultDialect` has been constructed. - ``execution_ctx_cls`` - a :class:`.ExecutionContext` class used to handle statement execution + """ - ``execute_sequence_format`` - either the 'tuple' or 'list' type, depending on what cursor.execute() - accepts for the second argument (they vary). + server_version_info: Optional[Tuple[Any, ...]] + """a tuple containing a version number for the DB backend in use. - ``preparer`` - a :class:`~sqlalchemy.sql.compiler.IdentifierPreparer` class used to - quote identifiers. + This value is only available for supporting dialects, and is + typically populated during the initial connection to the database. + """ - ``supports_alter`` - ``True`` if the database supports ``ALTER TABLE`` - used only for - generating foreign key constraints in certain circumstances + default_schema_name: Optional[str] + """the name of the default schema. This value is only available for + supporting dialects, and is typically populated during the + initial connection to the database. - ``max_identifier_length`` - The maximum length of identifier names. + """ - ``supports_sane_rowcount`` - Indicate whether the dialect properly implements rowcount for + execution_ctx_cls: Type["ExecutionContext"] + """a :class:`.ExecutionContext` class used to handle statement execution""" + + execute_sequence_format: Union[Type[Tuple[Any, ...]], Type[List[Any]]] + """either the 'tuple' or 'list' type, depending on what cursor.execute() + accepts for the second argument (they vary).""" + + supports_alter: bool + """``True`` if the database supports ``ALTER TABLE`` - used only for + generating foreign key constraints in certain circumstances + """ + + max_identifier_length: int + """The maximum length of identifier names.""" + + supports_sane_rowcount: bool + """Indicate whether the dialect properly implements rowcount for ``UPDATE`` and ``DELETE`` statements. + """ - ``supports_sane_multi_rowcount`` - Indicate whether the dialect properly implements rowcount for + supports_sane_multi_rowcount: bool + """Indicate whether the dialect properly implements rowcount for ``UPDATE`` and ``DELETE`` statements when executed via executemany. + """ + + supports_default_values: bool + """Indicates if the construct ``INSERT INTO tablename DEFAULT + VALUES`` is supported + """ - ``preexecute_autoincrement_sequences`` - True if 'implicit' primary key functions must be executed separately + preexecute_autoincrement_sequences: bool + """True if 'implicit' primary key functions must be executed separately in order to get their value. This is currently oriented towards PostgreSQL. + """ + + implicit_returning: bool + """For dialects that support RETURNING, indicate RETURNING may be used + to fetch newly generated primary key values and other defaults from + an INSERT statement automatically. + + .. seealso:: + + :paramref:`_schema.Table.implicit_returning` + + """ - ``colspecs`` - A dictionary of TypeEngine classes from sqlalchemy.types mapped + colspecs: Dict[Type["TypeEngine[Any]"], Type["TypeEngine[Any]"]] + """A dictionary of TypeEngine classes from sqlalchemy.types mapped to subclasses that are specific to the dialect class. This dictionary is class-level only and is not accessed from the dialect instance itself. + """ - ``supports_default_values`` - Indicates if the construct ``INSERT INTO tablename DEFAULT - VALUES`` is supported - - ``supports_sequences`` - Indicates if the dialect supports CREATE SEQUENCE or similar. + supports_sequences: bool + """Indicates if the dialect supports CREATE SEQUENCE or similar.""" - ``sequences_optional`` - If True, indicates if the "optional" flag on the Sequence() construct + sequences_optional: bool + """If True, indicates if the :paramref:`_schema.Sequence.optional` + parameter on the :class:`_schema.Sequence` construct should signal to not generate a CREATE SEQUENCE. Applies only to dialects that support sequences. Currently used only to allow PostgreSQL SERIAL to be used on a column that specifies Sequence() for usage on other backends. + """ - ``supports_native_enum`` - Indicates if the dialect supports a native ENUM construct. - This will prevent types.Enum from generating a CHECK - constraint when that type is used. + supports_native_enum: bool + """Indicates if the dialect supports a native ENUM construct. + This will prevent :class:`_types.Enum` from generating a CHECK + constraint when that type is used in "native" mode. + """ - ``supports_native_boolean`` - Indicates if the dialect supports a native boolean construct. - This will prevent types.Boolean from generating a CHECK + supports_native_boolean: bool + """Indicates if the dialect supports a native boolean construct. + This will prevent :class:`_types.Boolean` from generating a CHECK constraint when that type is used. + """ - ``dbapi_exception_translation_map`` - A dictionary of names that will contain as values the names of + dbapi_exception_translation_map: Dict[str, str] + """A dictionary of names that will contain as values the names of pep-249 exceptions ("IntegrityError", "OperationalError", etc) keyed to alternate class names, to support the case where a DBAPI has exception classes that aren't named as they are referred to (e.g. IntegrityError = MyException). In the vast majority of cases this dictionary is empty. + """ - .. versionadded:: 1.0.5 + supports_comments: bool + """Indicates the dialect supports comment DDL on tables and columns.""" - """ + inline_comments: bool + """Indicates the dialect supports comment DDL that's inline with the + definition of a Table or Column. If False, this implies that ALTER must + be used to set table and column comments.""" _has_events = False - supports_statement_cache = True + supports_statement_cache: bool = True """indicates if this dialect supports caching. All dialects that are compatible with statement caching should set this @@ -216,7 +654,9 @@ class Dialect: """ - def create_connect_args(self, url): + def create_connect_args( + self, url: "URL" + ) -> Tuple[Tuple[str], Mapping[str, Any]]: """Build DB-API compatible connection arguments. Given a :class:`.URL` object, returns a tuple @@ -250,7 +690,7 @@ class Dialect: raise NotImplementedError() @classmethod - def type_descriptor(cls, typeobj): + def type_descriptor(cls, typeobj: "TypeEngine") -> "TypeEngine": """Transform a generic type to a dialect-specific type. Dialect classes will usually use the @@ -264,7 +704,7 @@ class Dialect: raise NotImplementedError() - def initialize(self, connection): + def initialize(self, connection: "Connection") -> None: """Called during strategized creation of the dialect with a connection. @@ -284,88 +724,71 @@ class Dialect: pass - def get_columns(self, connection, table_name, schema=None, **kw): - """Return information about columns in `table_name`. + def get_columns( + self, + connection: "Connection", + table_name: str, + schema: Optional[str] = None, + **kw + ) -> List[ReflectedColumn]: + """Return information about columns in ``table_name``. Given a :class:`_engine.Connection`, a string - `table_name`, and an optional string `schema`, return column - information as a list of dictionaries with these keys: - - name - the column's name - - type - [sqlalchemy.types#TypeEngine] - - nullable - boolean - - default - the column's default value - - autoincrement - boolean - - sequence - a dictionary of the form - {'name' : str, 'start' :int, 'increment': int, 'minvalue': int, - 'maxvalue': int, 'nominvalue': bool, 'nomaxvalue': bool, - 'cycle': bool, 'cache': int, 'order': bool} + ``table_name``, and an optional string ``schema``, return column + information as a list of dictionaries + corresponding to the :class:`.ReflectedColumn` dictionary. - Additional column attributes may be present. """ raise NotImplementedError() - def get_pk_constraint(self, connection, table_name, schema=None, **kw): + def get_pk_constraint( + self, + connection: "Connection", + table_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> ReflectedPrimaryKeyConstraint: """Return information about the primary key constraint on table_name`. Given a :class:`_engine.Connection`, a string - `table_name`, and an optional string `schema`, return primary - key information as a dictionary with these keys: + ``table_name``, and an optional string ``schema``, return primary + key information as a dictionary corresponding to the + :class:`.ReflectedPrimaryKeyConstraint` dictionary. - constrained_columns - a list of column names that make up the primary key - - name - optional name of the primary key constraint. """ raise NotImplementedError() - def get_foreign_keys(self, connection, table_name, schema=None, **kw): - """Return information about foreign_keys in `table_name`. + def get_foreign_keys( + self, + connection: "Connection", + table_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> List[ReflectedForeignKeyConstraint]: + """Return information about foreign_keys in ``table_name``. Given a :class:`_engine.Connection`, a string - `table_name`, and an optional string `schema`, return foreign - key information as a list of dicts with these keys: - - name - the constraint's name - - constrained_columns - a list of column names that make up the foreign key - - referred_schema - the name of the referred schema + ``table_name``, and an optional string ``schema``, return foreign + key information as a list of dicts corresponding to the + :class:`.ReflectedForeignKeyConstraint` dictionary. - referred_table - the name of the referred table - - referred_columns - a list of column names in the referred table that correspond to - constrained_columns """ raise NotImplementedError() - def get_table_names(self, connection, schema=None, **kw): - """Return a list of table names for `schema`.""" + def get_table_names( + self, connection: "Connection", schema: Optional[str] = None, **kw: Any + ) -> List[str]: + """Return a list of table names for ``schema``.""" raise NotImplementedError() - def get_temp_table_names(self, connection, schema=None, **kw): + def get_temp_table_names( + self, connection: "Connection", schema: Optional[str] = None, **kw: Any + ) -> List[str]: """Return a list of temporary table names on the given connection, if supported by the underlying backend. @@ -373,7 +796,9 @@ class Dialect: raise NotImplementedError() - def get_view_names(self, connection, schema=None, **kw): + def get_view_names( + self, connection: "Connection", schema: Optional[str] = None, **kw: Any + ) -> List[str]: """Return a list of all view names available in the database. :param schema: schema name to query, if not the default schema. @@ -381,7 +806,9 @@ class Dialect: raise NotImplementedError() - def get_sequence_names(self, connection, schema=None, **kw): + def get_sequence_names( + self, connection: "Connection", schema: Optional[str] = None, **kw: Any + ) -> List[str]: """Return a list of all sequence names available in the database. :param schema: schema name to query, if not the default schema. @@ -391,7 +818,9 @@ class Dialect: raise NotImplementedError() - def get_temp_view_names(self, connection, schema=None, **kw): + def get_temp_view_names( + self, connection: "Connection", schema: Optional[str] = None, **kw: Any + ) -> List[str]: """Return a list of temporary view names on the given connection, if supported by the underlying backend. @@ -399,92 +828,102 @@ class Dialect: raise NotImplementedError() - def get_view_definition(self, connection, view_name, schema=None, **kw): + def get_view_definition( + self, + connection: "Connection", + view_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> str: """Return view definition. Given a :class:`_engine.Connection`, a string - `view_name`, and an optional string `schema`, return the view + `view_name`, and an optional string ``schema``, return the view definition. """ raise NotImplementedError() - def get_indexes(self, connection, table_name, schema=None, **kw): - """Return information about indexes in `table_name`. + def get_indexes( + self, + connection: "Connection", + table_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> List[ReflectedIndex]: + """Return information about indexes in ``table_name``. Given a :class:`_engine.Connection`, a string - `table_name` and an optional string `schema`, return index - information as a list of dictionaries with these keys: - - name - the index's name - - column_names - list of column names in order + ``table_name`` and an optional string ``schema``, return index + information as a list of dictionaries corresponding to the + :class:`.ReflectedIndex` dictionary. - unique - boolean """ raise NotImplementedError() def get_unique_constraints( - self, connection, table_name, schema=None, **kw - ): - r"""Return information about unique constraints in `table_name`. - - Given a string `table_name` and an optional string `schema`, return - unique constraint information as a list of dicts with these keys: - - name - the unique constraint's name - - column_names - list of column names in order - - \**kw - other options passed to the dialect's get_unique_constraints() - method. - - .. versionadded:: 0.9.0 + self, + connection: "Connection", + table_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> List[ReflectedUniqueConstraint]: + r"""Return information about unique constraints in ``table_name``. + + Given a string ``table_name`` and an optional string ``schema``, return + unique constraint information as a list of dicts corresponding + to the :class:`.ReflectedUniqueConstraint` dictionary. """ raise NotImplementedError() - def get_check_constraints(self, connection, table_name, schema=None, **kw): - r"""Return information about check constraints in `table_name`. - - Given a string `table_name` and an optional string `schema`, return - check constraint information as a list of dicts with these keys: + def get_check_constraints( + self, + connection: "Connection", + table_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> List[ReflectedCheckConstraint]: + r"""Return information about check constraints in ``table_name``. - * ``name`` - - the check constraint's name + Given a string ``table_name`` and an optional string ``schema``, return + check constraint information as a list of dicts corresponding + to the :class:`.ReflectedCheckConstraint` dictionary. - * ``sqltext`` - - the check constraint's SQL expression + """ - * ``**kw`` - - other options passed to the dialect's get_check_constraints() - method. + raise NotImplementedError() - .. versionadded:: 1.1.0 + def get_table_options( + self, + connection: "Connection", + table_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> Dict[str, Any]: + r"""Return the "options" for the table identified by ``table_name`` + as a dictionary. """ - raise NotImplementedError() - - def get_table_comment(self, connection, table_name, schema=None, **kw): - r"""Return the "comment" for the table identified by `table_name`. + def get_table_comment( + self, + connection: "Connection", + table_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> ReflectedTableComment: + r"""Return the "comment" for the table identified by ``table_name``. - Given a string `table_name` and an optional string `schema`, return - table comment information as a dictionary with this key: + Given a string ``table_name`` and an optional string ``schema``, return + table comment information as a dictionary corresponding to the + :class:`.ReflectedTableComment` dictionary. - text - text of the comment - Raises ``NotImplementedError`` for dialects that don't support - comments. + :raise: ``NotImplementedError`` for dialects that don't support + comments. .. versionadded:: 1.2 @@ -492,7 +931,7 @@ class Dialect: raise NotImplementedError() - def normalize_name(self, name): + def normalize_name(self, name: str) -> str: """convert the given name to lowercase if it is detected as case insensitive. @@ -502,7 +941,7 @@ class Dialect: """ raise NotImplementedError() - def denormalize_name(self, name): + def denormalize_name(self, name: str) -> str: """convert the given name to a case insensitive identifier for the backend if it is an all-lowercase name. @@ -512,7 +951,13 @@ class Dialect: """ raise NotImplementedError() - def has_table(self, connection, table_name, schema=None, **kw): + def has_table( + self, + connection: "Connection", + table_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> bool: """For internal dialect use, check the existence of a particular table or view in the database. @@ -543,11 +988,17 @@ class Dialect: raise NotImplementedError() - def has_index(self, connection, table_name, index_name, schema=None): + def has_index( + self, + connection: "Connection", + table_name: str, + index_name: str, + schema: Optional[str] = None, + ) -> bool: """Check the existence of a particular index name in the database. Given a :class:`_engine.Connection` object, a string - `table_name` and string index name, return True if an index of the + ``table_name`` and string index name, return True if an index of the given name on the given table exists, false otherwise. The :class:`.DefaultDialect` implements this in terms of the @@ -561,7 +1012,13 @@ class Dialect: raise NotImplementedError() - def has_sequence(self, connection, sequence_name, schema=None, **kw): + def has_sequence( + self, + connection: "Connection", + sequence_name: str, + schema: Optional[str] = None, + **kw: Any + ) -> bool: """Check the existence of a particular sequence in the database. Given a :class:`_engine.Connection` object and a string @@ -571,7 +1028,7 @@ class Dialect: raise NotImplementedError() - def _get_server_version_info(self, connection): + def _get_server_version_info(self, connection: "Connection") -> Any: """Retrieve the server version info from the given connection. This is used by the default implementation to populate the @@ -582,7 +1039,7 @@ class Dialect: raise NotImplementedError() - def _get_default_schema_name(self, connection): + def _get_default_schema_name(self, connection: "Connection") -> str: """Return the string name of the currently selected schema from the given connection. @@ -594,7 +1051,7 @@ class Dialect: raise NotImplementedError() - def do_begin(self, dbapi_connection): + def do_begin(self, dbapi_connection: PoolProxiedConnection) -> None: """Provide an implementation of ``connection.begin()``, given a DB-API connection. @@ -609,7 +1066,7 @@ class Dialect: raise NotImplementedError() - def do_rollback(self, dbapi_connection): + def do_rollback(self, dbapi_connection: PoolProxiedConnection) -> None: """Provide an implementation of ``connection.rollback()``, given a DB-API connection. @@ -620,7 +1077,7 @@ class Dialect: raise NotImplementedError() - def do_commit(self, dbapi_connection): + def do_commit(self, dbapi_connection: PoolProxiedConnection) -> None: """Provide an implementation of ``connection.commit()``, given a DB-API connection. @@ -631,7 +1088,7 @@ class Dialect: raise NotImplementedError() - def do_close(self, dbapi_connection): + def do_close(self, dbapi_connection: PoolProxiedConnection) -> None: """Provide an implementation of ``connection.close()``, given a DBAPI connection. @@ -644,7 +1101,12 @@ class Dialect: raise NotImplementedError() - def do_set_input_sizes(self, cursor, list_of_tuples, context): + def do_set_input_sizes( + self, + cursor: DBAPICursor, + list_of_tuples: List[Tuple[str, Any, "TypeEngine"]], + context: "ExecutionContext", + ) -> Any: """invoke the cursor.setinputsizes() method with appropriate arguments This hook is called if the :attr:`.Dialect.bind_typing` attribute is @@ -667,7 +1129,7 @@ class Dialect: """ raise NotImplementedError() - def create_xid(self): + def create_xid(self) -> Any: """Create a two-phase transaction ID. This id will be passed to do_begin_twophase(), @@ -677,7 +1139,7 @@ class Dialect: raise NotImplementedError() - def do_savepoint(self, connection, name): + def do_savepoint(self, connection: "Connection", name: str) -> None: """Create a savepoint with the given name. :param connection: a :class:`_engine.Connection`. @@ -687,7 +1149,9 @@ class Dialect: raise NotImplementedError() - def do_rollback_to_savepoint(self, connection, name): + def do_rollback_to_savepoint( + self, connection: "Connection", name: str + ) -> None: """Rollback a connection to the named savepoint. :param connection: a :class:`_engine.Connection`. @@ -697,7 +1161,9 @@ class Dialect: raise NotImplementedError() - def do_release_savepoint(self, connection, name): + def do_release_savepoint( + self, connection: "Connection", name: str + ) -> None: """Release the named savepoint on a connection. :param connection: a :class:`_engine.Connection`. @@ -706,7 +1172,7 @@ class Dialect: raise NotImplementedError() - def do_begin_twophase(self, connection, xid): + def do_begin_twophase(self, connection: "Connection", xid: Any) -> None: """Begin a two phase transaction on the given connection. :param connection: a :class:`_engine.Connection`. @@ -716,7 +1182,7 @@ class Dialect: raise NotImplementedError() - def do_prepare_twophase(self, connection, xid): + def do_prepare_twophase(self, connection: "Connection", xid: Any) -> None: """Prepare a two phase transaction on the given connection. :param connection: a :class:`_engine.Connection`. @@ -727,8 +1193,12 @@ class Dialect: raise NotImplementedError() def do_rollback_twophase( - self, connection, xid, is_prepared=True, recover=False - ): + self, + connection: "Connection", + xid: Any, + is_prepared: bool = True, + recover: bool = False, + ) -> None: """Rollback a two phase transaction on the given connection. :param connection: a :class:`_engine.Connection`. @@ -742,8 +1212,12 @@ class Dialect: raise NotImplementedError() def do_commit_twophase( - self, connection, xid, is_prepared=True, recover=False - ): + self, + connection: "Connection", + xid: Any, + is_prepared: bool = True, + recover: bool = False, + ) -> None: """Commit a two phase transaction on the given connection. @@ -757,7 +1231,7 @@ class Dialect: raise NotImplementedError() - def do_recover_twophase(self, connection): + def do_recover_twophase(self, connection: "Connection") -> None: """Recover list of uncommitted prepared two phase transaction identifiers on the given connection. @@ -767,20 +1241,35 @@ class Dialect: raise NotImplementedError() - def do_executemany(self, cursor, statement, parameters, context=None): + def do_executemany( + self, + cursor: DBAPICursor, + statement: str, + parameters: List[Union[Dict[str, Any], Tuple[Any]]], + context: Optional["ExecutionContext"] = None, + ) -> None: """Provide an implementation of ``cursor.executemany(statement, parameters)``.""" raise NotImplementedError() - def do_execute(self, cursor, statement, parameters, context=None): + def do_execute( + self, + cursor: DBAPICursor, + statement: str, + parameters: Union[Mapping[str, Any], Tuple[Any]], + context: Optional["ExecutionContext"] = None, + ): """Provide an implementation of ``cursor.execute(statement, parameters)``.""" raise NotImplementedError() def do_execute_no_params( - self, cursor, statement, parameters, context=None + self, + cursor: DBAPICursor, + statement: str, + context: Optional["ExecutionContext"] = None, ): """Provide an implementation of ``cursor.execute(statement)``. @@ -790,13 +1279,18 @@ class Dialect: raise NotImplementedError() - def is_disconnect(self, e, connection, cursor): + def is_disconnect( + self, + e: Exception, + connection: Optional[PoolProxiedConnection], + cursor: DBAPICursor, + ) -> bool: """Return True if the given DB-API error indicates an invalid connection""" raise NotImplementedError() - def connect(self, *cargs, **cparams): + def connect(self, *cargs: Any, **cparams: Any) -> Any: r"""Establish a connection using this dialect's DBAPI. The default implementation of this method is:: @@ -829,7 +1323,7 @@ class Dialect: """ - def on_connect_url(self, url): + def on_connect_url(self, url: "URL") -> Optional[Callable[[Any], Any]]: """return a callable which sets up a newly created DBAPI connection. This method is a new hook that supersedes the @@ -890,7 +1384,7 @@ class Dialect: """ return self.on_connect() - def on_connect(self): + def on_connect(self) -> Optional[Callable[[Any], Any]]: """return a callable which sets up a newly created DBAPI connection. The callable should accept a single argument "conn" which is the @@ -943,7 +1437,7 @@ class Dialect: """ return None - def reset_isolation_level(self, dbapi_conn): + def reset_isolation_level(self, dbapi_connection: DBAPIConnection) -> None: """Given a DBAPI connection, revert its isolation to the default. Note that this is a dialect-level method which is used as part @@ -970,7 +1464,9 @@ class Dialect: raise NotImplementedError() - def set_isolation_level(self, dbapi_conn, level): + def set_isolation_level( + self, dbapi_connection: DBAPIConnection, level: str + ) -> None: """Given a DBAPI connection, set its isolation level. Note that this is a dialect-level method which is used as part @@ -1002,7 +1498,7 @@ class Dialect: raise NotImplementedError() - def get_isolation_level(self, dbapi_conn): + def get_isolation_level(self, dbapi_connection: DBAPIConnection) -> str: """Given a DBAPI connection, return its isolation level. When working with a :class:`_engine.Connection` object, @@ -1035,7 +1531,7 @@ class Dialect: raise NotImplementedError() - def get_default_isolation_level(self, dbapi_conn): + def get_default_isolation_level(self, dbapi_conn: Any) -> str: """Given a DBAPI connection, return its isolation level, or a default isolation level if one cannot be retrieved. @@ -1055,7 +1551,7 @@ class Dialect: """ raise NotImplementedError() - def get_isolation_level_values(self, dbapi_conn): + def get_isolation_level_values(self, dbapi_conn: Any) -> List[str]: """return a sequence of string isolation level names that are accepted by this dialect. @@ -1098,7 +1594,7 @@ class Dialect: raise NotImplementedError() @classmethod - def get_dialect_cls(cls, url): + def get_dialect_cls(cls, url: "URL") -> Type: """Given a URL, return the :class:`.Dialect` that will be used. This is a hook that allows an external plugin to provide functionality @@ -1114,7 +1610,7 @@ class Dialect: return cls @classmethod - def get_async_dialect_cls(cls, url): + def get_async_dialect_cls(cls, url: "URL") -> None: """Given a URL, return the :class:`.Dialect` that will be used by an async engine. @@ -1133,7 +1629,7 @@ class Dialect: return cls.get_dialect_cls(url) @classmethod - def load_provisioning(cls): + def load_provisioning(cls) -> None: """set up the provision.py module for this dialect. For dialects that include a provision.py module that sets up @@ -1162,7 +1658,7 @@ class Dialect: """ @classmethod - def engine_created(cls, engine): + def engine_created(cls, engine: "Engine") -> None: """A convenience hook called before returning the final :class:`_engine.Engine`. @@ -1180,7 +1676,7 @@ class Dialect: """ - def get_driver_connection(self, connection): + def get_driver_connection(self, connection: PoolProxiedConnection) -> Any: """Returns the connection object as returned by the external driver package. @@ -1649,45 +2145,6 @@ class ConnectionEventsTarget: """ -class Connectable(ConnectionEventsTarget): - """Interface for an object which supports execution of SQL constructs. - - This is the base for :class:`_engine.Connection` and similar objects. - - .. versionchanged:: 2.0 :class:`_engine.Connectable` is no longer the - base class for :class:`_engine.Engine`, replaced with - :class:`_engine.ConnectionEventsTarget`. - - """ - - engine = None - """The :class:`_engine.Engine` instance referred to by this - :class:`.Connectable`. - - """ - - dialect = None - """The :class:`_engine.Dialect` instance referred to by this - :class:`.Connectable`. - - """ - - def execute(self, object_, *multiparams, **params): - """Executes the given construct and returns a - :class:`_result.Result`. - - """ - raise NotImplementedError() - - def scalar(self, object_, *multiparams, **params): - """Executes and returns the first column of the first row. - - The underlying cursor is closed after execution. - - """ - raise NotImplementedError() - - class ExceptionContext: """Encapsulate information about an error condition in progress. diff --git a/lib/sqlalchemy/engine/mock.py b/lib/sqlalchemy/engine/mock.py index 731dacc33..d2a9584a2 100644 --- a/lib/sqlalchemy/engine/mock.py +++ b/lib/sqlalchemy/engine/mock.py @@ -7,12 +7,11 @@ from operator import attrgetter -from . import base from . import url as _url from .. import util -class MockConnection(base.Connectable): +class MockConnection: def __init__(self, dialect, execute): self._dialect = dialect self.execute = execute diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 7abc404f0..562130f77 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -27,7 +27,6 @@ methods such as get_table_names, get_columns, etc. import contextlib -from .base import Connectable from .base import Connection from .base import Engine from .. import exc @@ -96,7 +95,7 @@ class Inspector: def __init__(self, bind): """Initialize a new :class:`_reflection.Inspector`. - :param bind: a :class:`~sqlalchemy.engine.Connectable`, + :param bind: a :class:`~sqlalchemy.engine.Connection`, which is typically an instance of :class:`~sqlalchemy.engine.Engine` or :class:`~sqlalchemy.engine.Connection`. @@ -153,10 +152,8 @@ class Inspector: """Construct a new dialect-specific Inspector object from the given engine or connection. - :param bind: a :class:`~sqlalchemy.engine.Connectable`, - which is typically an instance of - :class:`~sqlalchemy.engine.Engine` or - :class:`~sqlalchemy.engine.Connection`. + :param bind: a :class:`~sqlalchemy.engine.Connection` + or :class:`~sqlalchemy.engine.Engine`. This method differs from direct a direct constructor call of :class:`_reflection.Inspector` in that the @@ -170,13 +167,6 @@ class Inspector: """ return cls._construct(cls._init_legacy, bind) - @inspection._inspects(Connectable) - def _connectable_insp(bind): - # this method should not be used unless some unusual case - # has subclassed "Connectable" - - return Inspector._construct(Inspector._init_legacy, bind) - @inspection._inspects(Engine) def _engine_insp(bind): return Inspector._construct(Inspector._init_engine, bind) diff --git a/lib/sqlalchemy/pool/__init__.py b/lib/sqlalchemy/pool/__init__.py index 5b4f4ebb1..d1db0c226 100644 --- a/lib/sqlalchemy/pool/__init__.py +++ b/lib/sqlalchemy/pool/__init__.py @@ -18,10 +18,12 @@ SQLAlchemy connection pool. """ from . import events +from .base import _AdhocProxiedConnection from .base import _ConnectionFairy from .base import _ConnectionRecord from .base import _finalize_fairy from .base import Pool +from .base import PoolProxiedConnection from .base import reset_commit from .base import reset_none from .base import reset_rollback @@ -38,6 +40,7 @@ from .impl import StaticPool __all__ = [ "Pool", + "PoolProxiedConnection", "reset_commit", "reset_none", "reset_rollback", diff --git a/lib/sqlalchemy/pool/base.py b/lib/sqlalchemy/pool/base.py index 1a3dcd0e4..77e4761e8 100644 --- a/lib/sqlalchemy/pool/base.py +++ b/lib/sqlalchemy/pool/base.py @@ -12,6 +12,10 @@ from collections import deque import time +from typing import Any +from typing import Dict +from typing import Optional +from typing import TYPE_CHECKING import weakref from .. import event @@ -19,6 +23,8 @@ from .. import exc from .. import log from .. import util +if TYPE_CHECKING: + from ..engine.interfaces import DBAPIConnection reset_rollback = util.symbol("reset_rollback") reset_commit = util.symbol("reset_commit") @@ -781,14 +787,210 @@ def _finalize_fairy( _strong_ref_connection_records = {} -class _ConnectionFairy: +class PoolProxiedConnection: + """interface for the wrapper connection that is used by the connection + pool. + + :class:`.PoolProxiedConnection` is basically the public-facing interface + for the :class:`._ConnectionFairy` implemenatation object, users familiar + with :class:`._ConnectionFairy` can consider this object to be + equivalent. + + .. versionadded:: 2.0 + + """ + + __slots__ = () + + @util.memoized_property + def dbapi_connection(self) -> "DBAPIConnection": + """A reference to the actual DBAPI connection being tracked. + + .. seealso:: + + :attr:`.PoolProxiedConnection.driver_connection` + + :attr:`.PoolProxiedConnection.dbapi_connection` + + :ref:`faq_dbapi_connection` + + """ + raise NotImplementedError() + + @property + def driver_connection(self) -> Any: + """The connection object as returned by the driver after a connect. + + .. seealso:: + + :attr:`.PoolProxiedConnection.dbapi_connection` + + :attr:`._ConnectionRecord.driver_connection` + + :ref:`faq_dbapi_connection` + + """ + raise NotImplementedError() + + @property + def is_valid(self) -> bool: + """Return True if this :class:`.PoolProxiedConnection` still refers + to an active DBAPI connection.""" + + raise NotImplementedError() + + @util.memoized_property + def info(self) -> Dict[str, Any]: + """Info dictionary associated with the underlying DBAPI connection + referred to by this :class:`.ConnectionFairy`, allowing user-defined + data to be associated with the connection. + + The data here will follow along with the DBAPI connection including + after it is returned to the connection pool and used again + in subsequent instances of :class:`._ConnectionFairy`. It is shared + with the :attr:`._ConnectionRecord.info` and + :attr:`_engine.Connection.info` + accessors. + + The dictionary associated with a particular DBAPI connection is + discarded when the connection itself is discarded. + + """ + + raise NotImplementedError() + + @property + def record_info(self) -> Dict[str, Any]: + """Info dictionary associated with the :class:`._ConnectionRecord + container referred to by this :class:`.PoolProxiedConnection`. + + Unlike the :attr:`.PoolProxiedConnection.info` dictionary, the lifespan + of this dictionary is persistent across connections that are + disconnected and/or invalidated within the lifespan of a + :class:`._ConnectionRecord`. + + """ + + raise NotImplementedError() + + def invalidate( + self, e: Optional[Exception] = None, soft: bool = False + ) -> None: + """Mark this connection as invalidated. + + This method can be called directly, and is also called as a result + of the :meth:`_engine.Connection.invalidate` method. When invoked, + the DBAPI connection is immediately closed and discarded from + further use by the pool. The invalidation mechanism proceeds + via the :meth:`._ConnectionRecord.invalidate` internal method. + + :param e: an exception object indicating a reason for the invalidation. + + :param soft: if True, the connection isn't closed; instead, this + connection will be recycled on next checkout. + + .. seealso:: + + :ref:`pool_connection_invalidation` + + + """ + raise NotImplementedError() + + def detach(self) -> None: + """Separate this connection from its Pool. + + This means that the connection will no longer be returned to the + pool when closed, and will instead be literally closed. The + containing ConnectionRecord is separated from the DB-API connection, + and will create a new connection when next used. + + Note that any overall connection limiting constraints imposed by a + Pool implementation may be violated after a detach, as the detached + connection is removed from the pool's knowledge and control. + + """ + + raise NotImplementedError() + + def close(self) -> None: + """Release this connection back to the pool. + + The :meth:`.PoolProxiedConnection.close` method shadows the + :pep:`249` ``.close()`` method, altering its behavior to instead + :term:`release` the proxied connection back to the connection pool. + + Upon release to the pool, whether the connection stays "opened" and + pooled in the Python process, versus actually closed out and removed + from the Python process, is based on the pool implementation in use and + its configuration and current state. + + """ + raise NotImplementedError() + + +class _AdhocProxiedConnection(PoolProxiedConnection): + """provides the :class:`.PoolProxiedConnection` interface for cases where + the DBAPI connection is not actually proxied. + + This is used by the engine internals to pass a consistent + :class:`.PoolProxiedConnection` object to consuming dialects in response to + pool events that may not always have the :class:`._ConnectionFairy` + available. + + """ + + __slots__ = ("dbapi_connection", "_connection_record") + + def __init__(self, dbapi_connection, connection_record): + self.dbapi_connection = dbapi_connection + self._connection_record = connection_record + + @property + def driver_connection(self): + return self._connection_record.driver_connection + + @property + def connection(self): + """An alias to :attr:`._ConnectionFairy.dbapi_connection`. + + This alias is deprecated, please use the new name. + + .. deprecated:: 1.4.24 + + """ + return self._dbapi_connection + + @property + def is_valid(self): + raise AttributeError("is_valid not implemented by this proxy") + + @property + def record_info(self): + return self._connection_record.record_info + + def cursor(self, *args, **kwargs): + """Return a new DBAPI cursor for the underlying connection. + + This method is a proxy for the ``connection.cursor()`` DBAPI + method. + + """ + return self.dbapi_connection.cursor(*args, **kwargs) + + def __getattr__(self, key): + return getattr(self.dbapi_connection, key) + + +class _ConnectionFairy(PoolProxiedConnection): """Proxies a DBAPI connection and provides return-on-dereference support. This is an internal object used by the :class:`_pool.Pool` implementation to provide context management to a DBAPI connection delivered by - that :class:`_pool.Pool`. + that :class:`_pool.Pool`. The public facing interface for this class + is described by the :class:`.PoolProxiedConnection` class. The name "fairy" is inspired by the fact that the :class:`._ConnectionFairy` object's lifespan is transitory, as it lasts @@ -807,21 +1009,6 @@ class _ConnectionFairy: self._connection_record = connection_record self._echo = echo - dbapi_connection = None - """A reference to the actual DBAPI connection being tracked. - - .. versionadded:: 1.4.24 - - .. seealso:: - - :attr:`._ConnectionFairy.driver_connection` - - :attr:`._ConnectionRecord.dbapi_connection` - - :ref:`faq_dbapi_connection` - - """ - _connection_record = None """A reference to the :class:`._ConnectionRecord` object associated with the DBAPI connection. @@ -953,6 +1140,9 @@ class _ConnectionFairy: # try to checkin a second time. del fairy + # never called, this is for code linters + raise + attempts -= 1 pool.logger.info("Reconnection attempts exhausted on checkout") @@ -1011,15 +1201,7 @@ class _ConnectionFairy: referred to by this :class:`.ConnectionFairy`, allowing user-defined data to be associated with the connection. - The data here will follow along with the DBAPI connection including - after it is returned to the connection pool and used again - in subsequent instances of :class:`._ConnectionFairy`. It is shared - with the :attr:`._ConnectionRecord.info` and - :attr:`_engine.Connection.info` - accessors. - - The dictionary associated with a particular DBAPI connection is - discarded when the connection itself is discarded. + See :attr:`.PoolProxiedConnection.info` for full description. """ return self._connection_record.info @@ -1029,12 +1211,7 @@ class _ConnectionFairy: """Info dictionary associated with the :class:`._ConnectionRecord container referred to by this :class:`.ConnectionFairy`. - Unlike the :attr:`._ConnectionFairy.info` dictionary, the lifespan - of this dictionary is persistent across connections that are - disconnected and/or invalidated within the lifespan of a - :class:`._ConnectionRecord`. - - .. versionadded:: 1.1 + See :attr:`.PoolProxiedConnection.record_info` for full description. """ if self._connection_record: @@ -1045,18 +1222,7 @@ class _ConnectionFairy: def invalidate(self, e=None, soft=False): """Mark this connection as invalidated. - This method can be called directly, and is also called as a result - of the :meth:`_engine.Connection.invalidate` method. When invoked, - the DBAPI connection is immediately closed and discarded from - further use by the pool. The invalidation mechanism proceeds - via the :meth:`._ConnectionRecord.invalidate` internal method. - - :param e: an exception object indicating a reason for the invalidation. - - :param soft: if True, the connection isn't closed; instead, this - connection will be recycled on next checkout. - - .. versionadded:: 1.0.3 + See :attr:`.PoolProxiedConnection.invalidate` for full description. .. seealso:: @@ -1088,14 +1254,8 @@ class _ConnectionFairy: def detach(self): """Separate this connection from its Pool. - This means that the connection will no longer be returned to the - pool when closed, and will instead be literally closed. The - containing ConnectionRecord is separated from the DB-API connection, - and will create a new connection when next used. + See :meth:`.PoolProxiedConnection.detach` for full description. - Note that any overall connection limiting constraints imposed by a - Pool implementation may be violated after a detach, as the detached - connection is removed from the pool's knowledge and control. """ if self._connection_record is not None: @@ -1111,6 +1271,11 @@ class _ConnectionFairy: self._pool.dispatch.detach(self.dbapi_connection, rec) def close(self): + """Release this connection back to the pool. + + See :meth:`.PoolProxiedConnection.close` for full description. + + """ self._counter -= 1 if self._counter == 0: self._checkin() diff --git a/lib/sqlalchemy/pool/events.py b/lib/sqlalchemy/pool/events.py index 57e3893b0..27e5ef5c6 100644 --- a/lib/sqlalchemy/pool/events.py +++ b/lib/sqlalchemy/pool/events.py @@ -7,7 +7,7 @@ from .base import Pool from .. import event -from ..engine.base import Engine +from .. import util class PoolEvents(event.Events): @@ -42,8 +42,11 @@ class PoolEvents(event.Events): _target_class_doc = "SomeEngineOrPool" _dispatch_target = Pool + @util.preload_module("sqlalchemy.engine") @classmethod def _accept_with(cls, target): + Engine = util.preloaded.engine.Engine + if isinstance(target, type): if issubclass(target, Engine): return Pool diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 427251a88..d3477655c 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -714,7 +714,11 @@ class _Binary(TypeEngine): def literal_processor(self, dialect): def process(value): - value = value.decode(dialect.encoding).replace("'", "''") + # TODO: this is useless for real world scenarios; implement + # real binary literals + value = value.decode( + dialect._legacy_binary_type_literal_encoding + ).replace("'", "''") return "'%s'" % value return process diff --git a/lib/sqlalchemy/testing/engines.py b/lib/sqlalchemy/testing/engines.py index e17c09be7..52c2d3cbf 100644 --- a/lib/sqlalchemy/testing/engines.py +++ b/lib/sqlalchemy/testing/engines.py @@ -7,6 +7,10 @@ import collections import re +import typing +from typing import Any +from typing import Dict +from typing import Optional import warnings import weakref @@ -15,6 +19,13 @@ from .util import decorator from .util import gc_collect from .. import event from .. import pool +from ..util.typing import Literal + + +if typing.TYPE_CHECKING: + from ..engine import Engine + from ..engine.url import URL + from ..ext.asyncio import AsyncEngine class ConnectionKiller: @@ -264,14 +275,32 @@ def reconnecting_engine(url=None, options=None): return engine +@typing.overload +def testing_engine( + url: Optional["URL"] = None, + options: Optional[Dict[str, Any]] = None, + asyncio: Literal[False] = False, + transfer_staticpool: bool = False, +) -> "Engine": + ... + + +@typing.overload +def testing_engine( + url: Optional["URL"] = None, + options: Optional[Dict[str, Any]] = None, + asyncio: Literal[True] = True, + transfer_staticpool: bool = False, +) -> "AsyncEngine": + ... + + def testing_engine( url=None, options=None, asyncio=False, transfer_staticpool=False, ): - """Produce an engine configured by --options with optional overrides.""" - if asyncio: from sqlalchemy.ext.asyncio import create_async_engine as create_engine else: diff --git a/lib/sqlalchemy/testing/suite/test_dialect.py b/lib/sqlalchemy/testing/suite/test_dialect.py index 28fd99876..daaea085d 100644 --- a/lib/sqlalchemy/testing/suite/test_dialect.py +++ b/lib/sqlalchemy/testing/suite/test_dialect.py @@ -115,7 +115,9 @@ class IsolationLevelTest(fixtures.TestBase): eq_(conn.get_isolation_level(), non_default) - conn.dialect.reset_isolation_level(conn.connection) + conn.dialect.reset_isolation_level( + conn.connection.dbapi_connection + ) eq_(conn.get_isolation_level(), existing) @@ -223,7 +225,7 @@ class AutocommitIsolationTest(fixtures.TablesTest): c2 = conn.execution_options(isolation_level="AUTOCOMMIT") self._test_conn_autocommits(c2, True) - c2.dialect.reset_isolation_level(c2.connection) + c2.dialect.reset_isolation_level(c2.connection.dbapi_connection) self._test_conn_autocommits(conn, False) diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py index e1291de36..dfa5fa825 100644 --- a/lib/sqlalchemy/util/compat.py +++ b/lib/sqlalchemy/util/compat.py @@ -14,6 +14,7 @@ import operator import platform import sys +py311 = sys.version_info >= (3, 11) py39 = sys.version_info >= (3, 9) py38 = sys.version_info >= (3, 8) pypy = platform.python_implementation() == "PyPy" diff --git a/lib/sqlalchemy/util/typing.py b/lib/sqlalchemy/util/typing.py new file mode 100644 index 000000000..801c4a110 --- /dev/null +++ b/lib/sqlalchemy/util/typing.py @@ -0,0 +1,43 @@ +from typing import Any +from typing import Generic +from typing import overload +from typing import Type +from typing import TypeVar + +from . import compat + +if compat.py38: + from typing import Literal + from typing import Protocol + from typing import TypedDict +else: + from typing_extensions import Literal # noqa + from typing_extensions import Protocol # noqa + from typing_extensions import TypedDict # noqa + + +if compat.py311: + from typing import NotRequired # noqa +else: + from typing_extensions import NotRequired # noqa + + +_T = TypeVar("_T") + + +class _TypeToInstance(Generic[_T]): + @overload + def __get__(self, instance: None, owner: Any) -> Type[_T]: + ... + + @overload + def __get__(self, instance: object, owner: Any) -> _T: + ... + + @overload + def __set__(self, instance: None, value: Type[_T]) -> None: + ... + + @overload + def __set__(self, instance: object, value: _T) -> None: + ... |