pgtern 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pgtern/__init__.py +46 -0
- pgtern/_binary.py +226 -0
- pgtern/_composite.py +103 -0
- pgtern/_conn.py +907 -0
- pgtern/_copy.py +186 -0
- pgtern/_cursor.py +261 -0
- pgtern/_dsn.py +182 -0
- pgtern/_protocol.py +258 -0
- pgtern/_query.py +69 -0
- pgtern/_scram.py +101 -0
- pgtern/_types.py +442 -0
- pgtern/adapters.py +85 -0
- pgtern/aio.py +1287 -0
- pgtern/errors.py +248 -0
- pgtern/hooks.py +53 -0
- pgtern/pipeline.py +202 -0
- pgtern/pool.py +309 -0
- pgtern/py.typed +0 -0
- pgtern/rows.py +31 -0
- pgtern/sql.py +200 -0
- pgtern/types/__init__.py +30 -0
- pgtern/types/json.py +5 -0
- pgtern-0.3.0.dist-info/METADATA +265 -0
- pgtern-0.3.0.dist-info/RECORD +27 -0
- pgtern-0.3.0.dist-info/WHEEL +5 -0
- pgtern-0.3.0.dist-info/licenses/LICENSE +202 -0
- pgtern-0.3.0.dist-info/top_level.txt +1 -0
pgtern/_conn.py
ADDED
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Blocking-socket PostgreSQL connection built on the sans-I/O protocol layer."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import collections
|
|
6
|
+
import enum
|
|
7
|
+
import hashlib
|
|
8
|
+
import itertools
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
import select
|
|
12
|
+
import socket
|
|
13
|
+
import ssl
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
from contextlib import ExitStack
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal
|
|
18
|
+
|
|
19
|
+
from . import _binary
|
|
20
|
+
from . import _protocol as P
|
|
21
|
+
from . import _query, _types, errors, hooks
|
|
22
|
+
from ._cursor import Cursor, ServerCursor
|
|
23
|
+
from ._dsn import ConnParams, build_params
|
|
24
|
+
from ._scram import MECHANISM, ScramClient
|
|
25
|
+
from .rows import tuple_row
|
|
26
|
+
from .sql import Composable, quote_ident
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from .pipeline import Pipeline
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger("pgtern")
|
|
32
|
+
|
|
33
|
+
_IDLE, _INTRANS, _INERROR, _BROKEN = "I", "T", "E", "?"
|
|
34
|
+
PRIME_SQL = ('SELECT t.oid, n.nspname, t.typname, t.typelem, t.typcategory FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE t.oid IN ({ids}) OR t.oid IN (SELECT typelem FROM pg_type WHERE oid IN ({ids}) AND typelem <> 0)')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TransactionStatus(enum.Enum):
|
|
38
|
+
IDLE = "I"
|
|
39
|
+
INTRANS = "T"
|
|
40
|
+
INERROR = "E"
|
|
41
|
+
UNKNOWN = "?"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_VERSION_RE = re.compile(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_version(text: str) -> int:
|
|
48
|
+
m = _VERSION_RE.match(text)
|
|
49
|
+
if not m:
|
|
50
|
+
return 0
|
|
51
|
+
major, minor, patch = int(m.group(1)), int(m.group(2) or 0), int(m.group(3) or 0)
|
|
52
|
+
return major * 10000 + minor if major >= 10 else major * 10000 + minor * 100 + patch
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ConnectionInfo:
|
|
56
|
+
def __init__(self, conn: "Connection") -> None:
|
|
57
|
+
self._conn = conn
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def user(self) -> str:
|
|
61
|
+
return self._conn._params.user
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def dbname(self) -> str:
|
|
65
|
+
return self._conn._params.dbname
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def host(self) -> str:
|
|
69
|
+
return self._conn._host
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def port(self) -> int:
|
|
73
|
+
return self._conn._port
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def server_version(self) -> int:
|
|
77
|
+
return _parse_version(self._conn._server_params.get("server_version", ""))
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def backend_pid(self) -> int:
|
|
81
|
+
return self._conn._backend_pid
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def encoding(self) -> str:
|
|
85
|
+
return self._conn._server_params.get("client_encoding", "UTF8")
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def transaction_status(self) -> TransactionStatus:
|
|
89
|
+
return TransactionStatus(self._conn._tx_status)
|
|
90
|
+
|
|
91
|
+
def parameter_status(self, name: str) -> str | None:
|
|
92
|
+
return self._conn._server_params.get(name)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _ssl_context(params: ConnParams, mode: str) -> ssl.SSLContext:
|
|
96
|
+
if mode in ("require", "prefer", "allow"): # encrypt without verifying the server
|
|
97
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
98
|
+
ctx.check_hostname = False
|
|
99
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
100
|
+
else:
|
|
101
|
+
root = params.sslrootcert
|
|
102
|
+
ctx = ssl.create_default_context(cafile=root if root and root != "system" else None)
|
|
103
|
+
ctx.check_hostname = mode == "verify-full"
|
|
104
|
+
ctx.verify_mode = ssl.CERT_REQUIRED
|
|
105
|
+
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
106
|
+
if params.sslcert:
|
|
107
|
+
ctx.load_cert_chain(params.sslcert, params.sslkey)
|
|
108
|
+
return ctx
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Transaction:
|
|
112
|
+
"""``with conn.transaction():`` BEGIN/COMMIT, or SAVEPOINT/RELEASE when already in a transaction."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, conn: "Connection", savepoint_name: str | None = None,
|
|
115
|
+
force_rollback: bool = False) -> None:
|
|
116
|
+
self._conn = conn
|
|
117
|
+
self._name = savepoint_name
|
|
118
|
+
self._force_rollback = force_rollback
|
|
119
|
+
self._savepoint: str | None = None
|
|
120
|
+
|
|
121
|
+
def __enter__(self) -> "Transaction":
|
|
122
|
+
conn = self._conn
|
|
123
|
+
with conn._lock:
|
|
124
|
+
conn._check_open()
|
|
125
|
+
if conn._tx_status == _IDLE:
|
|
126
|
+
self._savepoint = None
|
|
127
|
+
conn._simple("BEGIN")
|
|
128
|
+
else:
|
|
129
|
+
self._savepoint = self._name or f"_pgtern_{next(conn._savepoint_ids)}"
|
|
130
|
+
conn._simple(f"SAVEPOINT {quote_ident(self._savepoint)}")
|
|
131
|
+
conn._tx_stack.append(self)
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> Literal[False]:
|
|
135
|
+
conn = self._conn
|
|
136
|
+
with conn._lock:
|
|
137
|
+
if conn._tx_stack and conn._tx_stack[-1] is self:
|
|
138
|
+
conn._tx_stack.pop()
|
|
139
|
+
if conn.closed:
|
|
140
|
+
return False
|
|
141
|
+
commit = exc_type is None and not self._force_rollback
|
|
142
|
+
try:
|
|
143
|
+
if self._savepoint is None:
|
|
144
|
+
conn._simple("COMMIT" if commit else "ROLLBACK")
|
|
145
|
+
else:
|
|
146
|
+
sp = quote_ident(self._savepoint)
|
|
147
|
+
conn._simple(f"RELEASE {sp}" if commit else f"ROLLBACK TO {sp}; RELEASE {sp}")
|
|
148
|
+
except Exception:
|
|
149
|
+
if exc_type is None:
|
|
150
|
+
raise
|
|
151
|
+
log.warning("error while ending a failed transaction block", exc_info=True)
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Connection:
|
|
156
|
+
"""A synchronous connection. Create it with :func:`pgtern.connect`."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, params: ConnParams, *, autocommit: bool = False,
|
|
159
|
+
row_factory: Callable | None = None) -> None:
|
|
160
|
+
self._params = params
|
|
161
|
+
self._autocommit = autocommit
|
|
162
|
+
self.row_factory = row_factory or tuple_row
|
|
163
|
+
self._lock = threading.RLock()
|
|
164
|
+
self._sock: socket.socket | None = None
|
|
165
|
+
self._closed = True
|
|
166
|
+
self._parser = P.MessageParser()
|
|
167
|
+
self._pending: collections.deque[tuple[int, bytes]] = collections.deque()
|
|
168
|
+
self._server_params: dict[str, str] = {}
|
|
169
|
+
self._backend_pid = 0
|
|
170
|
+
self._tx_status = _BROKEN
|
|
171
|
+
self._tx_stack: list[Transaction] = []
|
|
172
|
+
self._savepoint_ids = itertools.count(1)
|
|
173
|
+
self._host = ""
|
|
174
|
+
self._port = 0
|
|
175
|
+
self._dates_ok = True
|
|
176
|
+
self._notice_handlers: list[Callable[[errors.Diagnostic], None]] = []
|
|
177
|
+
self._notify_handlers: list[Callable[[P.Notify], None]] = []
|
|
178
|
+
self._loader_cache: dict[int, Any] = {}
|
|
179
|
+
self._pipeline: Any = None
|
|
180
|
+
self._notifies: collections.deque[P.Notify] = collections.deque()
|
|
181
|
+
self.info = ConnectionInfo(self)
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------- opening
|
|
184
|
+
@classmethod
|
|
185
|
+
def connect(cls, conninfo: str = "", *, autocommit: bool = False,
|
|
186
|
+
row_factory: Callable | None = None, **kwargs: Any) -> "Connection":
|
|
187
|
+
conn = cls(build_params(conninfo, **kwargs), autocommit=autocommit, row_factory=row_factory)
|
|
188
|
+
conn._open()
|
|
189
|
+
return conn
|
|
190
|
+
|
|
191
|
+
def _open(self) -> None:
|
|
192
|
+
modes = ["disable", "require"] if self._params.sslmode == "allow" else [self._params.sslmode]
|
|
193
|
+
last: errors.OperationalError | None = None
|
|
194
|
+
for host, port in self._params.hosts:
|
|
195
|
+
for mode in modes:
|
|
196
|
+
try:
|
|
197
|
+
self._connect_host(host, port, mode)
|
|
198
|
+
return
|
|
199
|
+
except errors.OperationalError as exc:
|
|
200
|
+
last = exc
|
|
201
|
+
self._drop_socket()
|
|
202
|
+
assert last is not None
|
|
203
|
+
raise last
|
|
204
|
+
|
|
205
|
+
def _drop_socket(self) -> None:
|
|
206
|
+
sock, self._sock = self._sock, None
|
|
207
|
+
self._closed = True
|
|
208
|
+
self._parser = P.MessageParser()
|
|
209
|
+
self._pending.clear()
|
|
210
|
+
if sock is not None:
|
|
211
|
+
try:
|
|
212
|
+
sock.close()
|
|
213
|
+
except OSError:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
def _connect_host(self, host: str, port: int, mode: str) -> None:
|
|
217
|
+
p = self._params
|
|
218
|
+
unix = host.startswith("/")
|
|
219
|
+
try:
|
|
220
|
+
if unix:
|
|
221
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
222
|
+
sock.settimeout(p.connect_timeout)
|
|
223
|
+
sock.connect(f"{host}/.s.PGSQL.{port}")
|
|
224
|
+
else:
|
|
225
|
+
sock = socket.create_connection((host, port), timeout=p.connect_timeout)
|
|
226
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
227
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
228
|
+
except OSError as exc:
|
|
229
|
+
raise errors.OperationalError(
|
|
230
|
+
f'connection to server at "{host}", port {port} failed: {exc}') from exc
|
|
231
|
+
self._sock = sock
|
|
232
|
+
self._host, self._port = host, port
|
|
233
|
+
self._closed = False
|
|
234
|
+
self._parser = P.MessageParser()
|
|
235
|
+
self._pending.clear()
|
|
236
|
+
self._server_params = {}
|
|
237
|
+
if not unix and mode != "disable":
|
|
238
|
+
try:
|
|
239
|
+
self._sock = self._negotiate_tls(sock, host, mode)
|
|
240
|
+
except (ssl.SSLError, ConnectionError) as exc:
|
|
241
|
+
self._drop_socket()
|
|
242
|
+
if mode == "prefer":
|
|
243
|
+
return self._connect_host(host, port, "disable")
|
|
244
|
+
raise errors.OperationalError(
|
|
245
|
+
f'SSL connection to server at "{host}", port {port} failed: {exc}') from exc
|
|
246
|
+
try:
|
|
247
|
+
self._startup()
|
|
248
|
+
except socket.timeout as exc: # pragma: no cover - mapped in _recv
|
|
249
|
+
raise errors.OperationalError(f"timeout during connection startup: {exc}") from exc
|
|
250
|
+
if self._sock is not None:
|
|
251
|
+
self._sock.settimeout(None)
|
|
252
|
+
self._dates_ok = self._server_params.get("DateStyle", "ISO").upper().startswith("ISO")
|
|
253
|
+
|
|
254
|
+
def _negotiate_tls(self, sock: socket.socket, host: str, mode: str) -> socket.socket:
|
|
255
|
+
sock.sendall(P.SSL_REQUEST)
|
|
256
|
+
answer = sock.recv(1)
|
|
257
|
+
if answer == b"S":
|
|
258
|
+
return _ssl_context(self._params, mode).wrap_socket(sock, server_hostname=host)
|
|
259
|
+
if answer == b"N":
|
|
260
|
+
if mode in ("require", "verify-ca", "verify-full"):
|
|
261
|
+
raise errors.OperationalError("server does not support SSL, but SSL was required")
|
|
262
|
+
return sock
|
|
263
|
+
raise errors.OperationalError(f"received an invalid response to the SSL request: {answer!r}")
|
|
264
|
+
|
|
265
|
+
def _password(self) -> str:
|
|
266
|
+
if self._params.password is None:
|
|
267
|
+
raise errors.OperationalError("the server requested a password, but none was provided")
|
|
268
|
+
return self._params.password
|
|
269
|
+
|
|
270
|
+
def _startup(self) -> None:
|
|
271
|
+
p = self._params
|
|
272
|
+
startup = {"user": p.user, "database": p.dbname, "client_encoding": "UTF8", "DateStyle": "ISO"}
|
|
273
|
+
if p.application_name:
|
|
274
|
+
startup["application_name"] = p.application_name
|
|
275
|
+
if p.options:
|
|
276
|
+
startup["options"] = p.options
|
|
277
|
+
self._send(P.startup_message(startup))
|
|
278
|
+
scram: ScramClient | None = None
|
|
279
|
+
while True:
|
|
280
|
+
tag, payload = self._next_message()
|
|
281
|
+
if tag == 0x52: # R Authentication*
|
|
282
|
+
code = int.from_bytes(payload[:4], "big")
|
|
283
|
+
if code == 0:
|
|
284
|
+
continue
|
|
285
|
+
if code == 3:
|
|
286
|
+
self._send(P.password_message(self._password().encode()))
|
|
287
|
+
elif code == 5:
|
|
288
|
+
inner = hashlib.md5((self._password() + p.user).encode(), usedforsecurity=False).hexdigest()
|
|
289
|
+
outer = hashlib.md5(inner.encode() + payload[4:8], usedforsecurity=False).hexdigest()
|
|
290
|
+
self._send(P.password_message(b"md5" + outer.encode()))
|
|
291
|
+
elif code == 10:
|
|
292
|
+
mechanisms = [m for m in payload[4:].split(b"\0") if m]
|
|
293
|
+
if MECHANISM.encode() not in mechanisms:
|
|
294
|
+
raise errors.NotSupportedError(
|
|
295
|
+
"the server offers no supported SASL mechanism: "
|
|
296
|
+
+ ", ".join(m.decode() for m in mechanisms))
|
|
297
|
+
scram = ScramClient(self._password())
|
|
298
|
+
self._send(P.sasl_initial_response(MECHANISM, scram.client_first()))
|
|
299
|
+
elif code == 11 and scram is not None:
|
|
300
|
+
self._send(P.sasl_response(scram.client_final(payload[4:])))
|
|
301
|
+
elif code == 12 and scram is not None:
|
|
302
|
+
scram.verify_server_final(payload[4:])
|
|
303
|
+
else:
|
|
304
|
+
raise errors.NotSupportedError(f"authentication method {code} is not supported")
|
|
305
|
+
elif tag == 0x45: # E
|
|
306
|
+
raise errors.error_from_fields(P.parse_error_fields(payload))
|
|
307
|
+
elif tag == 0x53: # S ParameterStatus
|
|
308
|
+
name, value = P.parse_parameter_status(payload)
|
|
309
|
+
self._server_params[name] = value
|
|
310
|
+
elif tag == 0x4B: # K BackendKeyData
|
|
311
|
+
self._backend_pid = int.from_bytes(payload[:4], "big")
|
|
312
|
+
elif tag == 0x4E: # N
|
|
313
|
+
self._notice(payload)
|
|
314
|
+
elif tag == 0x5A: # Z ReadyForQuery
|
|
315
|
+
self._tx_status = chr(payload[0])
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
# ---------------------------------------------------------------- I/O
|
|
319
|
+
def _mark_broken(self) -> None:
|
|
320
|
+
self._closed = True
|
|
321
|
+
self._tx_status = _BROKEN
|
|
322
|
+
sock, self._sock = self._sock, None
|
|
323
|
+
if sock is not None:
|
|
324
|
+
try:
|
|
325
|
+
sock.close()
|
|
326
|
+
except OSError:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
def _send(self, data: bytes) -> None:
|
|
330
|
+
sock = self._sock
|
|
331
|
+
if sock is None:
|
|
332
|
+
raise errors.OperationalError("the connection is closed")
|
|
333
|
+
try:
|
|
334
|
+
sock.sendall(data)
|
|
335
|
+
except (OSError, ssl.SSLError) as exc:
|
|
336
|
+
self._mark_broken()
|
|
337
|
+
raise errors.OperationalError(f"connection failed while sending: {exc}") from exc
|
|
338
|
+
|
|
339
|
+
def _next_message(self) -> tuple[int, bytes]:
|
|
340
|
+
while not self._pending:
|
|
341
|
+
sock = self._sock
|
|
342
|
+
if sock is None:
|
|
343
|
+
raise errors.OperationalError("the connection is closed")
|
|
344
|
+
try:
|
|
345
|
+
data = sock.recv(65536)
|
|
346
|
+
except (OSError, ssl.SSLError) as exc:
|
|
347
|
+
self._mark_broken()
|
|
348
|
+
raise errors.OperationalError(f"connection failed while receiving: {exc}") from exc
|
|
349
|
+
if not data:
|
|
350
|
+
self._mark_broken()
|
|
351
|
+
raise errors.OperationalError("server closed the connection unexpectedly")
|
|
352
|
+
self._pending.extend(self._parser.feed(data))
|
|
353
|
+
return self._pending.popleft()
|
|
354
|
+
|
|
355
|
+
def _notice(self, payload: bytes) -> None:
|
|
356
|
+
diag = errors.Diagnostic(P.parse_error_fields(payload))
|
|
357
|
+
if self._notice_handlers:
|
|
358
|
+
for handler in self._notice_handlers:
|
|
359
|
+
handler(diag)
|
|
360
|
+
else:
|
|
361
|
+
log.debug("server notice: %s", diag.message_primary)
|
|
362
|
+
|
|
363
|
+
def add_notice_handler(self, handler: Callable[[errors.Diagnostic], None]) -> None:
|
|
364
|
+
self._notice_handlers.append(handler)
|
|
365
|
+
|
|
366
|
+
def add_notify_handler(self, handler: Callable[[P.Notify], None]) -> None:
|
|
367
|
+
"""Call ``handler(notify)`` for every notification received while running other statements."""
|
|
368
|
+
self._notify_handlers.append(handler)
|
|
369
|
+
|
|
370
|
+
def _on_notification(self, payload: bytes) -> None:
|
|
371
|
+
note = P.parse_notification(payload)
|
|
372
|
+
if self._notify_handlers:
|
|
373
|
+
for handler in self._notify_handlers:
|
|
374
|
+
handler(note)
|
|
375
|
+
else:
|
|
376
|
+
self._notifies.append(note)
|
|
377
|
+
|
|
378
|
+
def _handle_async_message(self, tag: int, payload: bytes) -> None:
|
|
379
|
+
"""Messages that may arrive at any time: notices, parameter changes, notifications."""
|
|
380
|
+
if tag == 0x4E:
|
|
381
|
+
self._notice(payload)
|
|
382
|
+
elif tag == 0x53:
|
|
383
|
+
name, value = P.parse_parameter_status(payload)
|
|
384
|
+
self._server_params[name] = value
|
|
385
|
+
elif tag == 0x41:
|
|
386
|
+
self._on_notification(payload)
|
|
387
|
+
|
|
388
|
+
def notifies(self, *, timeout: float | None = None, stop_after: int | None = None):
|
|
389
|
+
"""Yield :class:`Notify` objects as they arrive (``LISTEN channel`` first).
|
|
390
|
+
|
|
391
|
+
Stops after ``timeout`` seconds without a new notification (``None`` waits forever) or
|
|
392
|
+
after ``stop_after`` notifications. Holds the connection while waiting.
|
|
393
|
+
"""
|
|
394
|
+
count = 0
|
|
395
|
+
while stop_after is None or count < stop_after:
|
|
396
|
+
with self._lock:
|
|
397
|
+
self._check_open()
|
|
398
|
+
if not self._notifies:
|
|
399
|
+
self._wait_for_data(timeout)
|
|
400
|
+
note = self._notifies.popleft() if self._notifies else None
|
|
401
|
+
if note is None:
|
|
402
|
+
return
|
|
403
|
+
count += 1
|
|
404
|
+
yield note
|
|
405
|
+
|
|
406
|
+
def _wait_for_data(self, timeout: float | None) -> None:
|
|
407
|
+
"""Block until something notifies us, the timeout passes, or the server goes away."""
|
|
408
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
409
|
+
while not self._notifies:
|
|
410
|
+
if not self._pending:
|
|
411
|
+
sock = self._sock
|
|
412
|
+
if sock is None:
|
|
413
|
+
raise errors.OperationalError("the connection is closed")
|
|
414
|
+
ready = bool(getattr(sock, "pending", lambda: 0)())
|
|
415
|
+
if not ready:
|
|
416
|
+
wait = None if deadline is None else max(0.0, deadline - time.monotonic())
|
|
417
|
+
try:
|
|
418
|
+
ready = bool(select.select([sock], [], [], wait)[0])
|
|
419
|
+
except (OSError, ValueError) as exc:
|
|
420
|
+
self._mark_broken()
|
|
421
|
+
raise errors.OperationalError(f"connection failed while waiting: {exc}") from exc
|
|
422
|
+
if not ready:
|
|
423
|
+
return
|
|
424
|
+
try:
|
|
425
|
+
data = sock.recv(65536)
|
|
426
|
+
except (OSError, ssl.SSLError) as exc:
|
|
427
|
+
self._mark_broken()
|
|
428
|
+
raise errors.OperationalError(f"connection failed while receiving: {exc}") from exc
|
|
429
|
+
if not data:
|
|
430
|
+
self._mark_broken()
|
|
431
|
+
raise errors.OperationalError("server closed the connection unexpectedly")
|
|
432
|
+
self._pending.extend(self._parser.feed(data))
|
|
433
|
+
while self._pending:
|
|
434
|
+
tag, payload = self._pending.popleft()
|
|
435
|
+
if tag == 0x45:
|
|
436
|
+
self._mark_broken()
|
|
437
|
+
raise errors.error_from_fields(P.parse_error_fields(payload))
|
|
438
|
+
self._handle_async_message(tag, payload)
|
|
439
|
+
if deadline is not None and time.monotonic() >= deadline and not self._notifies:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
def _read_cycle(self) -> list[P.Result]:
|
|
443
|
+
"""Read messages up to and including ReadyForQuery; raise the server error, if any."""
|
|
444
|
+
collector = P.QueryCollector()
|
|
445
|
+
error: dict[str, str] | None = None
|
|
446
|
+
unsupported = False
|
|
447
|
+
while True:
|
|
448
|
+
tag, payload = self._next_message()
|
|
449
|
+
if tag == 0x5A: # Z
|
|
450
|
+
self._tx_status = chr(payload[0])
|
|
451
|
+
break
|
|
452
|
+
if tag == 0x45: # E
|
|
453
|
+
fields = P.parse_error_fields(payload)
|
|
454
|
+
if error is None:
|
|
455
|
+
error = fields
|
|
456
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
457
|
+
self._mark_broken()
|
|
458
|
+
raise errors.error_from_fields(fields)
|
|
459
|
+
elif tag == 0x4E:
|
|
460
|
+
self._notice(payload)
|
|
461
|
+
elif tag == 0x53:
|
|
462
|
+
name, value = P.parse_parameter_status(payload)
|
|
463
|
+
self._server_params[name] = value
|
|
464
|
+
elif tag == 0x41: # A NotificationResponse
|
|
465
|
+
self._on_notification(payload)
|
|
466
|
+
elif tag == 0x47: # G CopyInResponse: abort it cleanly
|
|
467
|
+
unsupported = True
|
|
468
|
+
self._send(P.copy_fail("COPY needs cursor.copy()"))
|
|
469
|
+
elif tag in (0x48, 0x57, 0x64, 0x63): # H W d c: drain CopyOut data
|
|
470
|
+
unsupported = True
|
|
471
|
+
else:
|
|
472
|
+
collector.handle(tag, payload)
|
|
473
|
+
if unsupported:
|
|
474
|
+
raise errors.NotSupportedError("COPY must be run with cursor.copy(), not execute()")
|
|
475
|
+
if error is not None:
|
|
476
|
+
raise errors.error_from_fields(error)
|
|
477
|
+
return collector.results
|
|
478
|
+
|
|
479
|
+
def _check_open(self) -> None:
|
|
480
|
+
if self._closed:
|
|
481
|
+
raise errors.OperationalError("the connection is closed")
|
|
482
|
+
|
|
483
|
+
def _check_alive(self) -> bool:
|
|
484
|
+
"""Cheap liveness check for an idle connection: no round trip, just a non-blocking read."""
|
|
485
|
+
with self._lock:
|
|
486
|
+
sock = self._sock
|
|
487
|
+
if self._closed or sock is None:
|
|
488
|
+
return False
|
|
489
|
+
try:
|
|
490
|
+
sock.settimeout(0.0)
|
|
491
|
+
try:
|
|
492
|
+
data = sock.recv(65536)
|
|
493
|
+
except (BlockingIOError, ssl.SSLWantReadError, ssl.SSLWantWriteError):
|
|
494
|
+
return True
|
|
495
|
+
except (OSError, ssl.SSLError):
|
|
496
|
+
self._mark_broken()
|
|
497
|
+
return False
|
|
498
|
+
if not data:
|
|
499
|
+
self._mark_broken()
|
|
500
|
+
return False
|
|
501
|
+
self._pending.extend(self._parser.feed(data))
|
|
502
|
+
while self._pending:
|
|
503
|
+
tag, payload = self._pending[0]
|
|
504
|
+
if tag == 0x4E:
|
|
505
|
+
self._notice(payload)
|
|
506
|
+
elif tag == 0x53:
|
|
507
|
+
name, value = P.parse_parameter_status(payload)
|
|
508
|
+
self._server_params[name] = value
|
|
509
|
+
elif tag == 0x41:
|
|
510
|
+
self._on_notification(payload)
|
|
511
|
+
elif tag == 0x45:
|
|
512
|
+
self._mark_broken()
|
|
513
|
+
return False
|
|
514
|
+
else:
|
|
515
|
+
return True
|
|
516
|
+
self._pending.popleft()
|
|
517
|
+
return True
|
|
518
|
+
finally:
|
|
519
|
+
if self._sock is not None:
|
|
520
|
+
self._sock.settimeout(None)
|
|
521
|
+
|
|
522
|
+
# ---------------------------------------------------------------- execution
|
|
523
|
+
def _simple(self, sql: str) -> list[P.Result]:
|
|
524
|
+
self._send(P.query_message(sql))
|
|
525
|
+
return self._read_cycle()
|
|
526
|
+
|
|
527
|
+
def _run(self, sql: str, params: Any) -> list[P.Result]:
|
|
528
|
+
self._send_query(sql, params)
|
|
529
|
+
return self._read_cycle()
|
|
530
|
+
|
|
531
|
+
def _run_many(self, sql: str, params_seq: Iterable[Any]) -> int:
|
|
532
|
+
"""``executemany`` as one pipeline.
|
|
533
|
+
|
|
534
|
+
Every statement is sent without waiting for its reply. All of them run in one
|
|
535
|
+
transaction: a single Sync ends the batch, so with ``autocommit=True`` a failure
|
|
536
|
+
rolls the whole batch back. Replies are read through a sliding window, so the
|
|
537
|
+
server never blocks on a full socket while we are still sending. Returns the
|
|
538
|
+
total rowcount. Query hooks see one call per statement, as with ``execute``."""
|
|
539
|
+
from .pipeline import Pipeline, _encode # internal helpers; imported here to avoid a cycle
|
|
540
|
+
|
|
541
|
+
window = 64
|
|
542
|
+
pg_sql, redacted = self._query_text(sql)
|
|
543
|
+
with self._lock:
|
|
544
|
+
self._check_open()
|
|
545
|
+
self._check_no_pipeline()
|
|
546
|
+
# Outside a transaction the batch gets one: with autocommit it is BEGIN..COMMIT (so a failure,
|
|
547
|
+
# even one before the Sync, rolls it all back); without it, the usual implicit transaction.
|
|
548
|
+
own_tx = self._autocommit and self._tx_status == _IDLE
|
|
549
|
+
if own_tx or (not self._autocommit and self._tx_status == _IDLE):
|
|
550
|
+
self._send(P.query_message("BEGIN")) # BEGIN is its own round trip, as in _send_query
|
|
551
|
+
self._read_cycle()
|
|
552
|
+
total = 0
|
|
553
|
+
in_flight: collections.deque = collections.deque() # (hook stack, info) per unread reply
|
|
554
|
+
failure: BaseException | None = None
|
|
555
|
+
sent_sync = False
|
|
556
|
+
|
|
557
|
+
def enter_hooks() -> tuple[ExitStack, Any]:
|
|
558
|
+
stack = ExitStack()
|
|
559
|
+
info = None
|
|
560
|
+
if hooks._HOOKS:
|
|
561
|
+
info = hooks.QueryInfo(redacted, self._host, self._port, self._params.dbname, self._params.user)
|
|
562
|
+
for hook in tuple(hooks._HOOKS):
|
|
563
|
+
try:
|
|
564
|
+
stack.enter_context(hook(info))
|
|
565
|
+
except Exception:
|
|
566
|
+
log.exception("a pgtern query hook failed on entry; ignoring it")
|
|
567
|
+
return stack, info
|
|
568
|
+
|
|
569
|
+
def read_one() -> errors.Error | None:
|
|
570
|
+
nonlocal total
|
|
571
|
+
stack, info = in_flight.popleft()
|
|
572
|
+
results, error = Pipeline._read_reply(self)
|
|
573
|
+
if results is not None:
|
|
574
|
+
rc = results[-1].rowcount if results else -1
|
|
575
|
+
if info is not None:
|
|
576
|
+
info.rowcount = rc
|
|
577
|
+
if rc > 0:
|
|
578
|
+
total += rc
|
|
579
|
+
if error is None:
|
|
580
|
+
stack.__exit__(None, None, None)
|
|
581
|
+
else:
|
|
582
|
+
stack.__exit__(type(error), error, None)
|
|
583
|
+
return error
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
for params in params_seq:
|
|
587
|
+
body = _encode(pg_sql, params) # no Sync: the batch ends with one
|
|
588
|
+
in_flight.append(enter_hooks())
|
|
589
|
+
self._send(body)
|
|
590
|
+
if len(in_flight) > window:
|
|
591
|
+
self._send(P.FLUSH) # make the server send the replies it has produced so far
|
|
592
|
+
while len(in_flight) > window // 2:
|
|
593
|
+
failure = read_one()
|
|
594
|
+
if failure is not None:
|
|
595
|
+
break # statements sent after a failure are skipped by the server
|
|
596
|
+
if failure is not None:
|
|
597
|
+
break
|
|
598
|
+
self._send(P.SYNC)
|
|
599
|
+
sent_sync = True
|
|
600
|
+
while failure is None and in_flight:
|
|
601
|
+
failure = read_one()
|
|
602
|
+
except BaseException as exc:
|
|
603
|
+
failure = exc
|
|
604
|
+
if self._sock is not None:
|
|
605
|
+
# a bad parameter or a dropped socket mid-batch: end the batch so the connection stays usable
|
|
606
|
+
try:
|
|
607
|
+
if not sent_sync:
|
|
608
|
+
self._send(P.SYNC)
|
|
609
|
+
while True:
|
|
610
|
+
tag, payload = self._next_message()
|
|
611
|
+
if tag == 0x5A:
|
|
612
|
+
self._tx_status = chr(payload[0])
|
|
613
|
+
break
|
|
614
|
+
if own_tx and self._tx_status != _IDLE:
|
|
615
|
+
self._simple("ROLLBACK")
|
|
616
|
+
except Exception:
|
|
617
|
+
self._mark_broken()
|
|
618
|
+
raise
|
|
619
|
+
finally:
|
|
620
|
+
# statements that were sent but never got a reply (skipped after a failure) end their hooks here
|
|
621
|
+
while in_flight:
|
|
622
|
+
stack, _ = in_flight.popleft()
|
|
623
|
+
if failure is None:
|
|
624
|
+
stack.__exit__(None, None, None)
|
|
625
|
+
else:
|
|
626
|
+
stack.__exit__(type(failure), failure, None)
|
|
627
|
+
# read up to ReadyForQuery
|
|
628
|
+
while True:
|
|
629
|
+
tag, payload = self._next_message()
|
|
630
|
+
if tag == 0x5A:
|
|
631
|
+
self._tx_status = chr(payload[0])
|
|
632
|
+
break
|
|
633
|
+
if tag == 0x45:
|
|
634
|
+
fields = P.parse_error_fields(payload)
|
|
635
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
636
|
+
self._mark_broken()
|
|
637
|
+
raise errors.error_from_fields(fields)
|
|
638
|
+
else:
|
|
639
|
+
self._handle_async_message(tag, payload)
|
|
640
|
+
if own_tx:
|
|
641
|
+
self._simple("ROLLBACK" if failure is not None else "COMMIT")
|
|
642
|
+
if failure is not None:
|
|
643
|
+
raise failure
|
|
644
|
+
return total
|
|
645
|
+
|
|
646
|
+
def _send_query(self, sql: str, params: Any, *, inline: bool = False) -> None:
|
|
647
|
+
begin = not self._autocommit and self._tx_status == _IDLE
|
|
648
|
+
if inline and params is not None: # client-side binding: PostgreSQL has no parameters in COPY
|
|
649
|
+
sql, params = _query.translate(sql, params, _types.render_literal)[0], None
|
|
650
|
+
body = P.build_query(sql, params, _query.translate, _types.dump)
|
|
651
|
+
if begin: # implicit BEGIN rides in the same packet as the query
|
|
652
|
+
self._send(P.query_message("BEGIN") + body)
|
|
653
|
+
try:
|
|
654
|
+
self._read_cycle()
|
|
655
|
+
except BaseException:
|
|
656
|
+
self._mark_broken()
|
|
657
|
+
raise
|
|
658
|
+
else:
|
|
659
|
+
self._send(body)
|
|
660
|
+
|
|
661
|
+
def _start_copy(self, query: Any, params: Any, stack: ExitStack) -> str:
|
|
662
|
+
"""Send a COPY statement and wait for its Copy*Response; returns ``"in"`` or ``"out"``."""
|
|
663
|
+
sql, redacted = self._query_text(query)
|
|
664
|
+
self._check_open()
|
|
665
|
+
if hooks._HOOKS:
|
|
666
|
+
info = hooks.QueryInfo(redacted, self._host, self._port, self._params.dbname, self._params.user)
|
|
667
|
+
for hook in tuple(hooks._HOOKS):
|
|
668
|
+
try:
|
|
669
|
+
stack.enter_context(hook(info))
|
|
670
|
+
except Exception:
|
|
671
|
+
log.exception("a pgtern query hook failed on entry; ignoring it")
|
|
672
|
+
self._send_query(sql, params, inline=True)
|
|
673
|
+
error: dict[str, str] | None = None
|
|
674
|
+
while True:
|
|
675
|
+
tag, payload = self._next_message()
|
|
676
|
+
if error is None and tag in (0x47, 0x48):
|
|
677
|
+
return "in" if tag == 0x47 else "out"
|
|
678
|
+
if tag == 0x45:
|
|
679
|
+
fields = P.parse_error_fields(payload)
|
|
680
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
681
|
+
self._mark_broken()
|
|
682
|
+
raise errors.error_from_fields(fields)
|
|
683
|
+
error = error or fields
|
|
684
|
+
elif tag == 0x5A:
|
|
685
|
+
self._tx_status = chr(payload[0])
|
|
686
|
+
if error is not None:
|
|
687
|
+
raise errors.error_from_fields(error)
|
|
688
|
+
raise errors.ProgrammingError("the statement is not a COPY ... FROM STDIN / TO STDOUT")
|
|
689
|
+
else:
|
|
690
|
+
self._handle_async_message(tag, payload)
|
|
691
|
+
|
|
692
|
+
def _query_text(self, query: Any) -> tuple[str, str]:
|
|
693
|
+
if isinstance(query, Composable):
|
|
694
|
+
return query.as_string(), query.as_redacted()
|
|
695
|
+
if isinstance(query, str):
|
|
696
|
+
return query, query
|
|
697
|
+
if isinstance(query, (bytes, bytearray)):
|
|
698
|
+
text = bytes(query).decode()
|
|
699
|
+
return text, text
|
|
700
|
+
raise TypeError(f"the query must be str, bytes or sql.Composable, not {type(query).__name__}")
|
|
701
|
+
|
|
702
|
+
def _prime_oids(self, oids: Iterable[int]) -> None:
|
|
703
|
+
"""Resolve user loaders for these types (one catalog query per connection and new type)."""
|
|
704
|
+
if not _types.has_loaders():
|
|
705
|
+
return
|
|
706
|
+
wanted = set(oids) - self._loader_cache.keys()
|
|
707
|
+
if not wanted:
|
|
708
|
+
return
|
|
709
|
+
ids = ", ".join(str(int(o)) for o in sorted(wanted))
|
|
710
|
+
rows = self._simple(PRIME_SQL.format(ids=ids))[0].rows
|
|
711
|
+
_types.resolve_loaders(rows, self._loader_cache, sorted(wanted))
|
|
712
|
+
|
|
713
|
+
def _prime_loaders(self, results: list[P.Result]) -> None:
|
|
714
|
+
self._prime_oids(c.type_code for r in results if r.columns for c in r.columns)
|
|
715
|
+
|
|
716
|
+
def _describe(self, sql: str) -> tuple[list[int], list[P.Column] | None]:
|
|
717
|
+
"""Ask the server for the parameter and column types of ``sql`` without running it."""
|
|
718
|
+
self._send(P.describe_statement(sql))
|
|
719
|
+
param_oids: list[int] = []
|
|
720
|
+
columns: list[P.Column] | None = None
|
|
721
|
+
error: dict[str, str] | None = None
|
|
722
|
+
while True:
|
|
723
|
+
tag, payload = self._next_message()
|
|
724
|
+
if tag == 0x5A:
|
|
725
|
+
self._tx_status = chr(payload[0])
|
|
726
|
+
break
|
|
727
|
+
if tag == 0x74: # t ParameterDescription
|
|
728
|
+
param_oids = P.parse_param_description(payload)
|
|
729
|
+
elif tag == 0x54: # T RowDescription
|
|
730
|
+
columns = P.parse_row_description(payload)
|
|
731
|
+
elif tag == 0x45:
|
|
732
|
+
fields = P.parse_error_fields(payload)
|
|
733
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
734
|
+
self._mark_broken()
|
|
735
|
+
raise errors.error_from_fields(fields)
|
|
736
|
+
error = error or fields
|
|
737
|
+
elif tag in (0x31, 0x6E): # 1 ParseComplete, n NoData
|
|
738
|
+
pass
|
|
739
|
+
else:
|
|
740
|
+
self._handle_async_message(tag, payload)
|
|
741
|
+
if error is not None:
|
|
742
|
+
raise errors.error_from_fields(error)
|
|
743
|
+
return param_oids, columns
|
|
744
|
+
|
|
745
|
+
def _binary_result_formats(self, columns: list[P.Column] | None) -> list[int] | None:
|
|
746
|
+
if columns is None:
|
|
747
|
+
return None
|
|
748
|
+
self._prime_oids(c.type_code for c in columns)
|
|
749
|
+
return [1 if _binary.scalar_decoder(c.type_code) is not None and self._loader_cache.get(c.type_code) is None
|
|
750
|
+
else 0 for c in columns]
|
|
751
|
+
|
|
752
|
+
def _run_binary(self, sql: str, params: Any) -> list[P.Result]:
|
|
753
|
+
"""Like _run, but parameters and columns use binary format where the type supports it.
|
|
754
|
+
|
|
755
|
+
Costs one extra round trip (describe) per statement, so it is opt-in with ``binary=True``.
|
|
756
|
+
"""
|
|
757
|
+
begin = not self._autocommit and self._tx_status == _IDLE
|
|
758
|
+
if begin:
|
|
759
|
+
self._simple("BEGIN")
|
|
760
|
+
values: list[Any]
|
|
761
|
+
if params is None:
|
|
762
|
+
pg_sql, values = sql, []
|
|
763
|
+
else:
|
|
764
|
+
pg_sql, values = _query.translate(sql, params)
|
|
765
|
+
param_oids, columns = self._describe(pg_sql)
|
|
766
|
+
if len(param_oids) != len(values):
|
|
767
|
+
raise errors.ProgrammingError(
|
|
768
|
+
f"the query has {len(param_oids)} parameters but {len(values)} were passed")
|
|
769
|
+
pformats, payloads = _types.binary_param_plan(param_oids, values)
|
|
770
|
+
rformats = self._binary_result_formats(columns)
|
|
771
|
+
self._send(P.extended_query(pg_sql, param_oids, payloads, pformats, rformats))
|
|
772
|
+
results = self._read_cycle()
|
|
773
|
+
for r in results:
|
|
774
|
+
if r.columns is not None:
|
|
775
|
+
r.formats = rformats
|
|
776
|
+
return results
|
|
777
|
+
|
|
778
|
+
def _check_no_pipeline(self) -> None:
|
|
779
|
+
if self._pipeline is not None:
|
|
780
|
+
raise errors.ProgrammingError(
|
|
781
|
+
"a pipeline is active on this connection: queue statements with pipe.execute() "
|
|
782
|
+
"or leave the pipeline block first")
|
|
783
|
+
|
|
784
|
+
def _execute(self, query: Any, params: Any, binary: bool = False) -> list[P.Result]:
|
|
785
|
+
sql, redacted = self._query_text(query)
|
|
786
|
+
run = self._run_binary if binary else self._run
|
|
787
|
+
with self._lock:
|
|
788
|
+
self._check_open()
|
|
789
|
+
self._check_no_pipeline()
|
|
790
|
+
if not hooks._HOOKS:
|
|
791
|
+
results = run(sql, params)
|
|
792
|
+
else:
|
|
793
|
+
info = hooks.QueryInfo(redacted, self._host, self._port, self._params.dbname, self._params.user)
|
|
794
|
+
with ExitStack() as stack:
|
|
795
|
+
for hook in tuple(hooks._HOOKS):
|
|
796
|
+
try:
|
|
797
|
+
stack.enter_context(hook(info))
|
|
798
|
+
except Exception:
|
|
799
|
+
log.exception("a pgtern query hook failed on entry; ignoring it")
|
|
800
|
+
results = run(sql, params)
|
|
801
|
+
info.rowcount = results[-1].rowcount if results else -1
|
|
802
|
+
self._prime_loaders(results)
|
|
803
|
+
return results
|
|
804
|
+
|
|
805
|
+
# ---------------------------------------------------------------- public API
|
|
806
|
+
def cursor(self, name: str | None = None, *, row_factory: Callable | None = None,
|
|
807
|
+
scrollable: bool | None = None, withhold: bool = False) -> Cursor:
|
|
808
|
+
"""A client-side cursor, or a server-side one (``DECLARE``) when ``name`` is given."""
|
|
809
|
+
if name is not None:
|
|
810
|
+
return ServerCursor(self, name, row_factory=row_factory, scrollable=scrollable, withhold=withhold)
|
|
811
|
+
return Cursor(self, row_factory=row_factory)
|
|
812
|
+
|
|
813
|
+
def execute(self, query: Any, params: Any = None, *, prepare: bool | None = None,
|
|
814
|
+
binary: bool | None = None) -> Cursor:
|
|
815
|
+
return self.cursor().execute(query, params, binary=bool(binary))
|
|
816
|
+
|
|
817
|
+
@property
|
|
818
|
+
def closed(self) -> bool:
|
|
819
|
+
return self._closed
|
|
820
|
+
|
|
821
|
+
@property
|
|
822
|
+
def autocommit(self) -> bool:
|
|
823
|
+
return self._autocommit
|
|
824
|
+
|
|
825
|
+
@autocommit.setter
|
|
826
|
+
def autocommit(self, value: bool) -> None:
|
|
827
|
+
with self._lock:
|
|
828
|
+
if bool(value) != self._autocommit and self._tx_status not in (_IDLE, _BROKEN):
|
|
829
|
+
raise errors.ProgrammingError("can't change 'autocommit' now: connection in a transaction")
|
|
830
|
+
self._autocommit = bool(value)
|
|
831
|
+
|
|
832
|
+
def commit(self) -> None:
|
|
833
|
+
with self._lock:
|
|
834
|
+
self._check_open()
|
|
835
|
+
self._check_no_pipeline()
|
|
836
|
+
if self._tx_stack:
|
|
837
|
+
raise errors.ProgrammingError(
|
|
838
|
+
"explicit commit() is forbidden inside a transaction() block; "
|
|
839
|
+
"the block commits on a successful exit")
|
|
840
|
+
if self._tx_status != _IDLE:
|
|
841
|
+
self._simple("COMMIT")
|
|
842
|
+
|
|
843
|
+
def rollback(self) -> None:
|
|
844
|
+
with self._lock:
|
|
845
|
+
self._check_open()
|
|
846
|
+
self._check_no_pipeline()
|
|
847
|
+
if self._tx_stack:
|
|
848
|
+
raise errors.ProgrammingError("explicit rollback() is forbidden inside a transaction() block")
|
|
849
|
+
if self._tx_status != _IDLE:
|
|
850
|
+
self._simple("ROLLBACK")
|
|
851
|
+
|
|
852
|
+
def transaction(self, savepoint_name: str | None = None, force_rollback: bool = False) -> Transaction:
|
|
853
|
+
self._check_no_pipeline()
|
|
854
|
+
return Transaction(self, savepoint_name, force_rollback)
|
|
855
|
+
|
|
856
|
+
def pipeline(self, batch_size: int = 64) -> "Pipeline":
|
|
857
|
+
"""Pipeline mode: queue many statements and read their replies together. See :mod:`pgtern.pipeline`."""
|
|
858
|
+
from .pipeline import Pipeline
|
|
859
|
+
return Pipeline(self, batch_size)
|
|
860
|
+
|
|
861
|
+
def _reset_for_pool(self) -> None:
|
|
862
|
+
with self._lock:
|
|
863
|
+
self._loader_cache.clear()
|
|
864
|
+
self._tx_stack.clear()
|
|
865
|
+
if not self._closed and self._tx_status != _IDLE:
|
|
866
|
+
self._simple("ROLLBACK")
|
|
867
|
+
|
|
868
|
+
def close(self) -> None:
|
|
869
|
+
with self._lock:
|
|
870
|
+
if self._closed:
|
|
871
|
+
return
|
|
872
|
+
sock = self._sock
|
|
873
|
+
self._closed = True
|
|
874
|
+
self._tx_status = _BROKEN
|
|
875
|
+
self._sock = None
|
|
876
|
+
if sock is not None:
|
|
877
|
+
try:
|
|
878
|
+
sock.sendall(P.TERMINATE)
|
|
879
|
+
except (OSError, ssl.SSLError):
|
|
880
|
+
pass
|
|
881
|
+
try:
|
|
882
|
+
sock.close()
|
|
883
|
+
except OSError:
|
|
884
|
+
pass
|
|
885
|
+
|
|
886
|
+
def __enter__(self) -> "Connection":
|
|
887
|
+
return self
|
|
888
|
+
|
|
889
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
890
|
+
try:
|
|
891
|
+
if not self._closed:
|
|
892
|
+
if exc_type is None:
|
|
893
|
+
self.commit()
|
|
894
|
+
else:
|
|
895
|
+
try:
|
|
896
|
+
self.rollback()
|
|
897
|
+
except Exception:
|
|
898
|
+
log.warning("rollback failed while leaving a connection block", exc_info=True)
|
|
899
|
+
finally:
|
|
900
|
+
self.close()
|
|
901
|
+
|
|
902
|
+
def __del__(self) -> None:
|
|
903
|
+
try:
|
|
904
|
+
if not self._closed and self._sock is not None:
|
|
905
|
+
self._sock.close()
|
|
906
|
+
except Exception:
|
|
907
|
+
pass
|