You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bazarr/libs/sqlalchemy/dialects/postgresql/psycopg.py

750 lines
22 KiB

# dialects/postgresql/psycopg.py
# Copyright (C) 2005-2024 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"""
.. dialect:: postgresql+psycopg
:name: psycopg (a.k.a. psycopg 3)
:dbapi: psycopg
:connectstring: postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...]
:url: https://pypi.org/project/psycopg/
``psycopg`` is the package and module name for version 3 of the ``psycopg``
database driver, formerly known as ``psycopg2``. This driver is different
enough from its ``psycopg2`` predecessor that SQLAlchemy supports it
via a totally separate dialect; support for ``psycopg2`` is expected to remain
for as long as that package continues to function for modern Python versions,
and also remains the default dialect for the ``postgresql://`` dialect
series.
The SQLAlchemy ``psycopg`` dialect provides both a sync and an async
implementation under the same dialect name. The proper version is
selected depending on how the engine is created:
* calling :func:`_sa.create_engine` with ``postgresql+psycopg://...`` will
automatically select the sync version, e.g.::
from sqlalchemy import create_engine
sync_engine = create_engine("postgresql+psycopg://scott:tiger@localhost/test")
* calling :func:`_asyncio.create_async_engine` with
``postgresql+psycopg://...`` will automatically select the async version,
e.g.::
from sqlalchemy.ext.asyncio import create_async_engine
asyncio_engine = create_async_engine("postgresql+psycopg://scott:tiger@localhost/test")
The asyncio version of the dialect may also be specified explicitly using the
``psycopg_async`` suffix, as::
from sqlalchemy.ext.asyncio import create_async_engine
asyncio_engine = create_async_engine("postgresql+psycopg_async://scott:tiger@localhost/test")
.. seealso::
:ref:`postgresql_psycopg2` - The SQLAlchemy ``psycopg``
dialect shares most of its behavior with the ``psycopg2`` dialect.
Further documentation is available there.
""" # noqa
from __future__ import annotations
import logging
import re
from typing import cast
from typing import TYPE_CHECKING
from . import ranges
from ._psycopg_common import _PGDialect_common_psycopg
from ._psycopg_common import _PGExecutionContext_common_psycopg
from .base import INTERVAL
from .base import PGCompiler
from .base import PGIdentifierPreparer
from .base import REGCONFIG
from .json import JSON
from .json import JSONB
from .json import JSONPathType
from .types import CITEXT
from ... import pool
from ... import util
from ...engine import AdaptedConnection
from ...sql import sqltypes
from ...util.concurrency import await_fallback
from ...util.concurrency import await_only
if TYPE_CHECKING:
from typing import Iterable
from psycopg import AsyncConnection
logger = logging.getLogger("sqlalchemy.dialects.postgresql")
class _PGString(sqltypes.String):
render_bind_cast = True
class _PGREGCONFIG(REGCONFIG):
render_bind_cast = True
class _PGJSON(JSON):
render_bind_cast = True
def bind_processor(self, dialect):
return self._make_bind_processor(None, dialect._psycopg_Json)
def result_processor(self, dialect, coltype):
return None
class _PGJSONB(JSONB):
render_bind_cast = True
def bind_processor(self, dialect):
return self._make_bind_processor(None, dialect._psycopg_Jsonb)
def result_processor(self, dialect, coltype):
return None
class _PGJSONIntIndexType(sqltypes.JSON.JSONIntIndexType):
__visit_name__ = "json_int_index"
render_bind_cast = True
class _PGJSONStrIndexType(sqltypes.JSON.JSONStrIndexType):
__visit_name__ = "json_str_index"
render_bind_cast = True
class _PGJSONPathType(JSONPathType):
pass
class _PGInterval(INTERVAL):
render_bind_cast = True
class _PGTimeStamp(sqltypes.DateTime):
render_bind_cast = True
class _PGDate(sqltypes.Date):
render_bind_cast = True
class _PGTime(sqltypes.Time):
render_bind_cast = True
class _PGInteger(sqltypes.Integer):
render_bind_cast = True
class _PGSmallInteger(sqltypes.SmallInteger):
render_bind_cast = True
class _PGNullType(sqltypes.NullType):
render_bind_cast = True
class _PGBigInteger(sqltypes.BigInteger):
render_bind_cast = True
class _PGBoolean(sqltypes.Boolean):
render_bind_cast = True
class _PsycopgRange(ranges.AbstractSingleRangeImpl):
def bind_processor(self, dialect):
psycopg_Range = cast(PGDialect_psycopg, dialect)._psycopg_Range
def to_range(value):
if isinstance(value, ranges.Range):
value = psycopg_Range(
value.lower, value.upper, value.bounds, value.empty
)
return value
return to_range
def result_processor(self, dialect, coltype):
def to_range(value):
if value is not None:
value = ranges.Range(
value._lower,
value._upper,
bounds=value._bounds if value._bounds else "[)",
empty=not value._bounds,
)
return value
return to_range
class _PsycopgMultiRange(ranges.AbstractMultiRangeImpl):
def bind_processor(self, dialect):
psycopg_Range = cast(PGDialect_psycopg, dialect)._psycopg_Range
psycopg_Multirange = cast(
PGDialect_psycopg, dialect
)._psycopg_Multirange
NoneType = type(None)
def to_range(value):
if isinstance(value, (str, NoneType, psycopg_Multirange)):
return value
return psycopg_Multirange(
[
psycopg_Range(
element.lower,
element.upper,
element.bounds,
element.empty,
)
for element in cast("Iterable[ranges.Range]", value)
]
)
return to_range
def result_processor(self, dialect, coltype):
def to_range(value):
if value is None:
return None
else:
return ranges.MultiRange(
ranges.Range(
elem._lower,
elem._upper,
bounds=elem._bounds if elem._bounds else "[)",
empty=not elem._bounds,
)
for elem in value
)
return to_range
class PGExecutionContext_psycopg(_PGExecutionContext_common_psycopg):
pass
class PGCompiler_psycopg(PGCompiler):
pass
class PGIdentifierPreparer_psycopg(PGIdentifierPreparer):
pass
def _log_notices(diagnostic):
logger.info("%s: %s", diagnostic.severity, diagnostic.message_primary)
class PGDialect_psycopg(_PGDialect_common_psycopg):
driver = "psycopg"
supports_statement_cache = True
supports_server_side_cursors = True
default_paramstyle = "pyformat"
supports_sane_multi_rowcount = True
execution_ctx_cls = PGExecutionContext_psycopg
statement_compiler = PGCompiler_psycopg
preparer = PGIdentifierPreparer_psycopg
psycopg_version = (0, 0)
_has_native_hstore = True
_psycopg_adapters_map = None
colspecs = util.update_copy(
_PGDialect_common_psycopg.colspecs,
{
sqltypes.String: _PGString,
REGCONFIG: _PGREGCONFIG,
JSON: _PGJSON,
CITEXT: CITEXT,
sqltypes.JSON: _PGJSON,
JSONB: _PGJSONB,
sqltypes.JSON.JSONPathType: _PGJSONPathType,
sqltypes.JSON.JSONIntIndexType: _PGJSONIntIndexType,
sqltypes.JSON.JSONStrIndexType: _PGJSONStrIndexType,
sqltypes.Interval: _PGInterval,
INTERVAL: _PGInterval,
sqltypes.Date: _PGDate,
sqltypes.DateTime: _PGTimeStamp,
sqltypes.Time: _PGTime,
sqltypes.Integer: _PGInteger,
sqltypes.SmallInteger: _PGSmallInteger,
sqltypes.BigInteger: _PGBigInteger,
ranges.AbstractSingleRange: _PsycopgRange,
ranges.AbstractMultiRange: _PsycopgMultiRange,
},
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.dbapi:
m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", self.dbapi.__version__)
if m:
self.psycopg_version = tuple(
int(x) for x in m.group(1, 2, 3) if x is not None
)
if self.psycopg_version < (3, 0, 2):
raise ImportError(
"psycopg version 3.0.2 or higher is required."
)
from psycopg.adapt import AdaptersMap
self._psycopg_adapters_map = adapters_map = AdaptersMap(
self.dbapi.adapters
)
if self._native_inet_types is False:
import psycopg.types.string
adapters_map.register_loader(
"inet", psycopg.types.string.TextLoader
)
adapters_map.register_loader(
"cidr", psycopg.types.string.TextLoader
)
if self._json_deserializer:
from psycopg.types.json import set_json_loads
set_json_loads(self._json_deserializer, adapters_map)
if self._json_serializer:
from psycopg.types.json import set_json_dumps
set_json_dumps(self._json_serializer, adapters_map)
def create_connect_args(self, url):
# see https://github.com/psycopg/psycopg/issues/83
cargs, cparams = super().create_connect_args(url)
if self._psycopg_adapters_map:
cparams["context"] = self._psycopg_adapters_map
if self.client_encoding is not None:
cparams["client_encoding"] = self.client_encoding
return cargs, cparams
def _type_info_fetch(self, connection, name):
from psycopg.types import TypeInfo
return TypeInfo.fetch(connection.connection.driver_connection, name)
def initialize(self, connection):
super().initialize(connection)
# PGDialect.initialize() checks server version for <= 8.2 and sets
# this flag to False if so
if not self.insert_returning:
self.insert_executemany_returning = False
# HSTORE can't be registered until we have a connection so that
# we can look up its OID, so we set up this adapter in
# initialize()
if self.use_native_hstore:
info = self._type_info_fetch(connection, "hstore")
self._has_native_hstore = info is not None
if self._has_native_hstore:
from psycopg.types.hstore import register_hstore
# register the adapter for connections made subsequent to
# this one
register_hstore(info, self._psycopg_adapters_map)
# register the adapter for this connection
register_hstore(info, connection.connection)
@classmethod
def import_dbapi(cls):
import psycopg
return psycopg
@classmethod
def get_async_dialect_cls(cls, url):
return PGDialectAsync_psycopg
@util.memoized_property
def _isolation_lookup(self):
return {
"READ COMMITTED": self.dbapi.IsolationLevel.READ_COMMITTED,
"READ UNCOMMITTED": self.dbapi.IsolationLevel.READ_UNCOMMITTED,
"REPEATABLE READ": self.dbapi.IsolationLevel.REPEATABLE_READ,
"SERIALIZABLE": self.dbapi.IsolationLevel.SERIALIZABLE,
}
@util.memoized_property
def _psycopg_Json(self):
from psycopg.types import json
return json.Json
@util.memoized_property
def _psycopg_Jsonb(self):
from psycopg.types import json
return json.Jsonb
@util.memoized_property
def _psycopg_TransactionStatus(self):
from psycopg.pq import TransactionStatus
return TransactionStatus
@util.memoized_property
def _psycopg_Range(self):
from psycopg.types.range import Range
return Range
@util.memoized_property
def _psycopg_Multirange(self):
from psycopg.types.multirange import Multirange
return Multirange
def _do_isolation_level(self, connection, autocommit, isolation_level):
connection.autocommit = autocommit
connection.isolation_level = isolation_level
def get_isolation_level(self, dbapi_connection):
status_before = dbapi_connection.info.transaction_status
value = super().get_isolation_level(dbapi_connection)
# don't rely on psycopg providing enum symbols, compare with
# eq/ne
if status_before == self._psycopg_TransactionStatus.IDLE:
dbapi_connection.rollback()
return value
def set_isolation_level(self, dbapi_connection, level):
if level == "AUTOCOMMIT":
self._do_isolation_level(
dbapi_connection, autocommit=True, isolation_level=None
)
else:
self._do_isolation_level(
dbapi_connection,
autocommit=False,
isolation_level=self._isolation_lookup[level],
)
def set_readonly(self, connection, value):
connection.read_only = value
def get_readonly(self, connection):
return connection.read_only
def on_connect(self):
def notices(conn):
conn.add_notice_handler(_log_notices)
fns = [notices]
if self.isolation_level is not None:
def on_connect(conn):
self.set_isolation_level(conn, self.isolation_level)
fns.append(on_connect)
# fns always has the notices function
def on_connect(conn):
for fn in fns:
fn(conn)
return on_connect
def is_disconnect(self, e, connection, cursor):
if isinstance(e, self.dbapi.Error) and connection is not None:
if connection.closed or connection.broken:
return True
return False
def _do_prepared_twophase(self, connection, command, recover=False):
dbapi_conn = connection.connection.dbapi_connection
if (
recover
# don't rely on psycopg providing enum symbols, compare with
# eq/ne
or dbapi_conn.info.transaction_status
!= self._psycopg_TransactionStatus.IDLE
):
dbapi_conn.rollback()
before_autocommit = dbapi_conn.autocommit
try:
if not before_autocommit:
self._do_autocommit(dbapi_conn, True)
dbapi_conn.execute(command)
finally:
if not before_autocommit:
self._do_autocommit(dbapi_conn, before_autocommit)
def do_rollback_twophase(
self, connection, xid, is_prepared=True, recover=False
):
if is_prepared:
self._do_prepared_twophase(
connection, f"ROLLBACK PREPARED '{xid}'", recover=recover
)
else:
self.do_rollback(connection.connection)
def do_commit_twophase(
self, connection, xid, is_prepared=True, recover=False
):
if is_prepared:
self._do_prepared_twophase(
connection, f"COMMIT PREPARED '{xid}'", recover=recover
)
else:
self.do_commit(connection.connection)
@util.memoized_property
def _dialect_specific_select_one(self):
return ";"
class AsyncAdapt_psycopg_cursor:
__slots__ = ("_cursor", "await_", "_rows")
_psycopg_ExecStatus = None
def __init__(self, cursor, await_) -> None:
self._cursor = cursor
self.await_ = await_
self._rows = []
def __getattr__(self, name):
return getattr(self._cursor, name)
@property
def arraysize(self):
return self._cursor.arraysize
@arraysize.setter
def arraysize(self, value):
self._cursor.arraysize = value
def close(self):
self._rows.clear()
# Normal cursor just call _close() in a non-sync way.
self._cursor._close()
def execute(self, query, params=None, **kw):
result = self.await_(self._cursor.execute(query, params, **kw))
# sqlalchemy result is not async, so need to pull all rows here
res = self._cursor.pgresult
# don't rely on psycopg providing enum symbols, compare with
# eq/ne
if res and res.status == self._psycopg_ExecStatus.TUPLES_OK:
rows = self.await_(self._cursor.fetchall())
if not isinstance(rows, list):
self._rows = list(rows)
else:
self._rows = rows
return result
def executemany(self, query, params_seq):
return self.await_(self._cursor.executemany(query, params_seq))
def __iter__(self):
# TODO: try to avoid pop(0) on a list
while self._rows:
yield self._rows.pop(0)
def fetchone(self):
if self._rows:
# TODO: try to avoid pop(0) on a list
return self._rows.pop(0)
else:
return None
def fetchmany(self, size=None):
if size is None:
size = self._cursor.arraysize
retval = self._rows[0:size]
self._rows = self._rows[size:]
return retval
def fetchall(self):
retval = self._rows
self._rows = []
return retval
class AsyncAdapt_psycopg_ss_cursor(AsyncAdapt_psycopg_cursor):
def execute(self, query, params=None, **kw):
self.await_(self._cursor.execute(query, params, **kw))
return self
def close(self):
self.await_(self._cursor.close())
def fetchone(self):
return self.await_(self._cursor.fetchone())
def fetchmany(self, size=0):
return self.await_(self._cursor.fetchmany(size))
def fetchall(self):
return self.await_(self._cursor.fetchall())
def __iter__(self):
iterator = self._cursor.__aiter__()
while True:
try:
yield self.await_(iterator.__anext__())
except StopAsyncIteration:
break
class AsyncAdapt_psycopg_connection(AdaptedConnection):
_connection: AsyncConnection
__slots__ = ()
await_ = staticmethod(await_only)
def __init__(self, connection) -> None:
self._connection = connection
def __getattr__(self, name):
return getattr(self._connection, name)
def execute(self, query, params=None, **kw):
cursor = self.await_(self._connection.execute(query, params, **kw))
return AsyncAdapt_psycopg_cursor(cursor, self.await_)
def cursor(self, *args, **kw):
cursor = self._connection.cursor(*args, **kw)
if hasattr(cursor, "name"):
return AsyncAdapt_psycopg_ss_cursor(cursor, self.await_)
else:
return AsyncAdapt_psycopg_cursor(cursor, self.await_)
def commit(self):
self.await_(self._connection.commit())
def rollback(self):
self.await_(self._connection.rollback())
def close(self):
self.await_(self._connection.close())
@property
def autocommit(self):
return self._connection.autocommit
@autocommit.setter
def autocommit(self, value):
self.set_autocommit(value)
def set_autocommit(self, value):
self.await_(self._connection.set_autocommit(value))
def set_isolation_level(self, value):
self.await_(self._connection.set_isolation_level(value))
def set_read_only(self, value):
self.await_(self._connection.set_read_only(value))
def set_deferrable(self, value):
self.await_(self._connection.set_deferrable(value))
class AsyncAdaptFallback_psycopg_connection(AsyncAdapt_psycopg_connection):
__slots__ = ()
await_ = staticmethod(await_fallback)
class PsycopgAdaptDBAPI:
def __init__(self, psycopg) -> None:
self.psycopg = psycopg
for k, v in self.psycopg.__dict__.items():
if k != "connect":
self.__dict__[k] = v
def connect(self, *arg, **kw):
async_fallback = kw.pop("async_fallback", False)
creator_fn = kw.pop(
"async_creator_fn", self.psycopg.AsyncConnection.connect
)
if util.asbool(async_fallback):
return AsyncAdaptFallback_psycopg_connection(
await_fallback(creator_fn(*arg, **kw))
)
else:
return AsyncAdapt_psycopg_connection(
await_only(creator_fn(*arg, **kw))
)
class PGDialectAsync_psycopg(PGDialect_psycopg):
is_async = True
supports_statement_cache = True
@classmethod
def import_dbapi(cls):
import psycopg
from psycopg.pq import ExecStatus
AsyncAdapt_psycopg_cursor._psycopg_ExecStatus = ExecStatus
return PsycopgAdaptDBAPI(psycopg)
@classmethod
def get_pool_class(cls, url):
async_fallback = url.query.get("async_fallback", False)
if util.asbool(async_fallback):
return pool.FallbackAsyncAdaptedQueuePool
else:
return pool.AsyncAdaptedQueuePool
def _type_info_fetch(self, connection, name):
from psycopg.types import TypeInfo
adapted = connection.connection
return adapted.await_(TypeInfo.fetch(adapted.driver_connection, name))
def _do_isolation_level(self, connection, autocommit, isolation_level):
connection.set_autocommit(autocommit)
connection.set_isolation_level(isolation_level)
def _do_autocommit(self, connection, value):
connection.set_autocommit(value)
def set_readonly(self, connection, value):
connection.set_read_only(value)
def set_deferrable(self, connection, value):
connection.set_deferrable(value)
def get_driver_connection(self, connection):
return connection._connection
dialect = PGDialect_psycopg
dialect_async = PGDialectAsync_psycopg