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/errors.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""DB-API 2.0 exceptions, plus one subclass per common SQLSTATE.
|
|
3
|
+
|
|
4
|
+
Server errors are raised as the class registered for their SQLSTATE
|
|
5
|
+
(``errors.UniqueViolation`` for ``23505``, ...); unknown codes fall back to the DB-API
|
|
6
|
+
class for their first two characters (``DatabaseError`` if even that is unknown).
|
|
7
|
+
``str(exc)`` starts with the server's primary message; DETAIL, HINT and CONTEXT follow
|
|
8
|
+
on later lines. The class layout follows the DB-API conventions.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Warning(Exception): # noqa: A001 - the name is required by DB-API 2.0
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_DIAG_FIELDS = {
|
|
20
|
+
"severity": "S", "severity_nonlocalized": "V", "sqlstate": "C", "message_primary": "M",
|
|
21
|
+
"message_detail": "D", "message_hint": "H", "statement_position": "P",
|
|
22
|
+
"internal_position": "p", "internal_query": "q", "context": "W", "schema_name": "s",
|
|
23
|
+
"table_name": "t", "column_name": "c", "datatype_name": "d", "constraint_name": "n",
|
|
24
|
+
"source_file": "F", "source_line": "L", "source_function": "R",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Diagnostic:
|
|
29
|
+
"""The fields of a server ErrorResponse (``diag.message_primary``, ``diag.constraint_name``, ...)."""
|
|
30
|
+
|
|
31
|
+
__slots__ = ("_fields",)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING: # the properties below are generated at import time; declared here for type checkers
|
|
34
|
+
severity: str | None
|
|
35
|
+
severity_nonlocalized: str | None
|
|
36
|
+
sqlstate: str | None
|
|
37
|
+
message_primary: str | None
|
|
38
|
+
message_detail: str | None
|
|
39
|
+
message_hint: str | None
|
|
40
|
+
statement_position: str | None
|
|
41
|
+
internal_position: str | None
|
|
42
|
+
internal_query: str | None
|
|
43
|
+
context: str | None
|
|
44
|
+
schema_name: str | None
|
|
45
|
+
table_name: str | None
|
|
46
|
+
column_name: str | None
|
|
47
|
+
datatype_name: str | None
|
|
48
|
+
constraint_name: str | None
|
|
49
|
+
source_file: str | None
|
|
50
|
+
source_line: str | None
|
|
51
|
+
source_function: str | None
|
|
52
|
+
|
|
53
|
+
def __init__(self, fields: dict[str, str]) -> None:
|
|
54
|
+
self._fields = dict(fields)
|
|
55
|
+
|
|
56
|
+
def __repr__(self) -> str:
|
|
57
|
+
return f"Diagnostic(sqlstate={self.sqlstate!r}, message_primary={self.message_primary!r})"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _field_getter(code: str) -> Callable[[Diagnostic], str | None]:
|
|
61
|
+
def get(self: Diagnostic) -> str | None:
|
|
62
|
+
return self._fields.get(code)
|
|
63
|
+
return get
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
for _name, _code in _DIAG_FIELDS.items():
|
|
67
|
+
setattr(Diagnostic, _name, property(_field_getter(_code)))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Error(Exception):
|
|
71
|
+
sqlstate: str | None = None
|
|
72
|
+
|
|
73
|
+
def __init__(self, *args: Any, diag: Diagnostic | None = None) -> None:
|
|
74
|
+
super().__init__(*args)
|
|
75
|
+
self.diag = diag if diag is not None else Diagnostic({})
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class InterfaceError(Error): ...
|
|
79
|
+
class DatabaseError(Error): ...
|
|
80
|
+
class DataError(DatabaseError): ...
|
|
81
|
+
class OperationalError(DatabaseError): ...
|
|
82
|
+
class IntegrityError(DatabaseError): ...
|
|
83
|
+
class InternalError(DatabaseError): ...
|
|
84
|
+
class ProgrammingError(DatabaseError): ...
|
|
85
|
+
class NotSupportedError(DatabaseError): ...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PipelineAborted(OperationalError):
|
|
89
|
+
"""A statement in a pipeline did not run because an earlier statement in the same batch failed."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# SQLSTATE class (first two characters) -> DB-API base class. Any class not listed here
|
|
93
|
+
# (03, 09, 0B, 0F, 0L, 0P, 72, ...) maps to plain DatabaseError. The table is checked against
|
|
94
|
+
# unknown codes are covered by tests/test_unit.py (test_unknown_sqlstates_fall_back_to_their_dbapi_class).
|
|
95
|
+
_BASE_BY_CLASS: dict[str, type[Error]] = {
|
|
96
|
+
"08": OperationalError, "0A": NotSupportedError, "20": ProgrammingError, "21": ProgrammingError,
|
|
97
|
+
"22": DataError, "23": IntegrityError, "24": InternalError, "25": InternalError,
|
|
98
|
+
"26": ProgrammingError, "27": OperationalError, "28": OperationalError, "2B": InternalError,
|
|
99
|
+
"2D": InternalError, "2F": OperationalError, "34": ProgrammingError, "38": OperationalError,
|
|
100
|
+
"39": OperationalError, "3B": OperationalError, "3D": ProgrammingError, "3F": ProgrammingError,
|
|
101
|
+
"40": OperationalError, "42": ProgrammingError, "44": ProgrammingError, "53": OperationalError,
|
|
102
|
+
"54": OperationalError, "55": OperationalError, "57": OperationalError, "58": OperationalError,
|
|
103
|
+
"F0": OperationalError, "HV": OperationalError, "P0": ProgrammingError, "XX": InternalError,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# "<sqlstate> <ClassName>". Codes ending in 000 are class-level parents of the rest of their class.
|
|
107
|
+
_SPEC = """
|
|
108
|
+
08000 ConnectionException
|
|
109
|
+
08001 SqlclientUnableToEstablishSqlconnection
|
|
110
|
+
08003 ConnectionDoesNotExist
|
|
111
|
+
08004 SqlserverRejectedEstablishmentOfSqlconnection
|
|
112
|
+
08006 ConnectionFailure
|
|
113
|
+
08P01 ProtocolViolation
|
|
114
|
+
0A000 FeatureNotSupported
|
|
115
|
+
21000 CardinalityViolation
|
|
116
|
+
22000 DataException
|
|
117
|
+
22001 StringDataRightTruncation
|
|
118
|
+
22002 NullValueNoIndicatorParameter
|
|
119
|
+
22003 NumericValueOutOfRange
|
|
120
|
+
22007 InvalidDatetimeFormat
|
|
121
|
+
22008 DatetimeFieldOverflow
|
|
122
|
+
22012 DivisionByZero
|
|
123
|
+
22021 CharacterNotInRepertoire
|
|
124
|
+
22023 InvalidParameterValue
|
|
125
|
+
22026 StringDataLengthMismatch
|
|
126
|
+
22P02 InvalidTextRepresentation
|
|
127
|
+
22P03 InvalidBinaryRepresentation
|
|
128
|
+
22P05 UntranslatableCharacter
|
|
129
|
+
23000 IntegrityConstraintViolation
|
|
130
|
+
23001 RestrictViolation
|
|
131
|
+
23502 NotNullViolation
|
|
132
|
+
23503 ForeignKeyViolation
|
|
133
|
+
23505 UniqueViolation
|
|
134
|
+
23514 CheckViolation
|
|
135
|
+
23P01 ExclusionViolation
|
|
136
|
+
24000 InvalidCursorState
|
|
137
|
+
25000 InvalidTransactionState
|
|
138
|
+
25001 ActiveSqlTransaction
|
|
139
|
+
25006 ReadOnlySqlTransaction
|
|
140
|
+
25P01 NoActiveSqlTransaction
|
|
141
|
+
25P02 InFailedSqlTransaction
|
|
142
|
+
25P03 IdleInTransactionSessionTimeout
|
|
143
|
+
26000 InvalidSqlStatementName
|
|
144
|
+
28000 InvalidAuthorizationSpecification
|
|
145
|
+
28P01 InvalidPassword
|
|
146
|
+
2BP01 DependentObjectsStillExist
|
|
147
|
+
2D000 InvalidTransactionTermination
|
|
148
|
+
34000 InvalidCursorName
|
|
149
|
+
38000 ExternalRoutineException
|
|
150
|
+
3B000 SavepointException
|
|
151
|
+
3D000 InvalidCatalogName
|
|
152
|
+
3F000 InvalidSchemaName
|
|
153
|
+
40000 TransactionRollback
|
|
154
|
+
40001 SerializationFailure
|
|
155
|
+
40002 TransactionIntegrityConstraintViolation
|
|
156
|
+
40003 StatementCompletionUnknown
|
|
157
|
+
40P01 DeadlockDetected
|
|
158
|
+
42000 SyntaxErrorOrAccessRuleViolation
|
|
159
|
+
42501 InsufficientPrivilege
|
|
160
|
+
42601 SyntaxError
|
|
161
|
+
42602 InvalidName
|
|
162
|
+
42611 InvalidColumnDefinition
|
|
163
|
+
42622 NameTooLong
|
|
164
|
+
42701 DuplicateColumn
|
|
165
|
+
42702 AmbiguousColumn
|
|
166
|
+
42703 UndefinedColumn
|
|
167
|
+
42704 UndefinedObject
|
|
168
|
+
42710 DuplicateObject
|
|
169
|
+
42712 DuplicateAlias
|
|
170
|
+
42723 DuplicateFunction
|
|
171
|
+
42725 AmbiguousFunction
|
|
172
|
+
42803 GroupingError
|
|
173
|
+
42804 DatatypeMismatch
|
|
174
|
+
42809 WrongObjectType
|
|
175
|
+
42830 InvalidForeignKey
|
|
176
|
+
42846 CannotCoerce
|
|
177
|
+
42883 UndefinedFunction
|
|
178
|
+
42939 ReservedName
|
|
179
|
+
42P01 UndefinedTable
|
|
180
|
+
42P02 UndefinedParameter
|
|
181
|
+
42P03 DuplicateCursor
|
|
182
|
+
42P04 DuplicateDatabase
|
|
183
|
+
42P05 DuplicatePreparedStatement
|
|
184
|
+
42P06 DuplicateSchema
|
|
185
|
+
42P07 DuplicateTable
|
|
186
|
+
42P08 AmbiguousParameter
|
|
187
|
+
42P09 AmbiguousAlias
|
|
188
|
+
42P10 InvalidColumnReference
|
|
189
|
+
42P16 InvalidTableDefinition
|
|
190
|
+
42P17 InvalidObjectDefinition
|
|
191
|
+
42P18 IndeterminateDatatype
|
|
192
|
+
44000 WithCheckOptionViolation
|
|
193
|
+
53000 InsufficientResources
|
|
194
|
+
53100 DiskFull
|
|
195
|
+
53200 OutOfMemory
|
|
196
|
+
53300 TooManyConnections
|
|
197
|
+
54000 ProgramLimitExceeded
|
|
198
|
+
55000 ObjectNotInPrerequisiteState
|
|
199
|
+
55006 ObjectInUse
|
|
200
|
+
55P02 CantChangeRuntimeParam
|
|
201
|
+
55P03 LockNotAvailable
|
|
202
|
+
57000 OperatorIntervention
|
|
203
|
+
57014 QueryCanceled
|
|
204
|
+
57P01 AdminShutdown
|
|
205
|
+
57P02 CrashShutdown
|
|
206
|
+
57P03 CannotConnectNow
|
|
207
|
+
57P04 DatabaseDropped
|
|
208
|
+
58000 SystemError
|
|
209
|
+
58030 IoError
|
|
210
|
+
P0000 PlpgsqlError
|
|
211
|
+
P0001 RaiseException
|
|
212
|
+
P0002 NoDataFound
|
|
213
|
+
P0003 TooManyRows
|
|
214
|
+
P0004 AssertFailure
|
|
215
|
+
XX000 InternalError_
|
|
216
|
+
XX001 DataCorrupted
|
|
217
|
+
XX002 IndexCorrupted
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
_BY_SQLSTATE: dict[str, type[Error]] = {}
|
|
221
|
+
_ENTRIES = [line.split() for line in _SPEC.strip().splitlines()]
|
|
222
|
+
|
|
223
|
+
# The hierarchy is flat: every specific class derives directly from its DB-API
|
|
224
|
+
# family (UniqueViolation -> IntegrityError).
|
|
225
|
+
for _code, _name in _ENTRIES:
|
|
226
|
+
_cls = type(_name, (_BASE_BY_CLASS.get(_code[:2], DatabaseError),),
|
|
227
|
+
{"sqlstate": _code, "__module__": __name__})
|
|
228
|
+
globals()[_name] = _cls
|
|
229
|
+
_BY_SQLSTATE[_code] = _cls
|
|
230
|
+
|
|
231
|
+
__all__ = [
|
|
232
|
+
"Warning", "Error", "InterfaceError", "DatabaseError", "DataError", "OperationalError",
|
|
233
|
+
"IntegrityError", "InternalError", "ProgrammingError", "NotSupportedError", "Diagnostic", "PipelineAborted",
|
|
234
|
+
*(name for _, name in _ENTRIES),
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def error_from_fields(fields: dict[str, str]) -> Error:
|
|
239
|
+
"""Build the right exception from the fields of a server ErrorResponse."""
|
|
240
|
+
sqlstate = fields.get("C") or "XX000"
|
|
241
|
+
cls = _BY_SQLSTATE.get(sqlstate) or _BASE_BY_CLASS.get(sqlstate[:2], DatabaseError)
|
|
242
|
+
lines = [fields.get("M", "unknown error")]
|
|
243
|
+
for key, label in (("D", "DETAIL"), ("H", "HINT"), ("W", "CONTEXT")):
|
|
244
|
+
if key in fields:
|
|
245
|
+
lines.append(f"{label}: {fields[key]}")
|
|
246
|
+
exc = cls("\n".join(lines), diag=Diagnostic(fields))
|
|
247
|
+
exc.sqlstate = sqlstate
|
|
248
|
+
return exc
|
pgtern/hooks.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Query hooks: observe every statement without monkeypatching.
|
|
3
|
+
|
|
4
|
+
A hook is a callable ``hook(info) -> context manager``. The context manager is
|
|
5
|
+
entered before the statement is sent and exited when its result (or error) has
|
|
6
|
+
been read, with the usual ``__exit__`` exception arguments. ``info.rowcount`` is
|
|
7
|
+
filled in before a successful exit.
|
|
8
|
+
|
|
9
|
+
Hooks never receive bound parameter values. ``info.statement`` is the query text;
|
|
10
|
+
for ``sql.Composed`` queries any ``sql.Literal`` is replaced by ``?``.
|
|
11
|
+
|
|
12
|
+
@contextmanager
|
|
13
|
+
def trace(info):
|
|
14
|
+
with tracer.start_as_current_span("postgresql") as span:
|
|
15
|
+
span.set_attribute("db.statement", info.statement)
|
|
16
|
+
yield
|
|
17
|
+
|
|
18
|
+
unregister = pgtern.add_query_hook(trace)
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from contextlib import AbstractContextManager
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Callable
|
|
25
|
+
|
|
26
|
+
__all__ = ["QueryInfo", "add_query_hook"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class QueryInfo:
|
|
31
|
+
statement: str
|
|
32
|
+
host: str
|
|
33
|
+
port: int
|
|
34
|
+
dbname: str
|
|
35
|
+
user: str
|
|
36
|
+
rowcount: int = -1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
QueryHook = Callable[[QueryInfo], AbstractContextManager]
|
|
40
|
+
_HOOKS: list[QueryHook] = []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def add_query_hook(hook: QueryHook) -> Callable[[], None]:
|
|
44
|
+
"""Register a hook for every connection; returns a function that unregisters it."""
|
|
45
|
+
_HOOKS.append(hook)
|
|
46
|
+
|
|
47
|
+
def remove() -> None:
|
|
48
|
+
try:
|
|
49
|
+
_HOOKS.remove(hook)
|
|
50
|
+
except ValueError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return remove
|
pgtern/pipeline.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Pipeline mode: send many statements before waiting for any reply.
|
|
3
|
+
|
|
4
|
+
with conn.pipeline() as pipe:
|
|
5
|
+
a = pipe.execute("INSERT INTO t (v) VALUES (%s)", [1])
|
|
6
|
+
b = pipe.execute("SELECT count(*) FROM t")
|
|
7
|
+
# leaving the block synchronizes: now a and b have their results
|
|
8
|
+
print(b.fetchone())
|
|
9
|
+
|
|
10
|
+
Statements are sent immediately and their replies read, in order, at a *sync point*. Sync
|
|
11
|
+
points happen when you call ``sync()``, when the block ends, and every ``batch_size``
|
|
12
|
+
statements.
|
|
13
|
+
|
|
14
|
+
* Transactions: with ``autocommit=False`` the first statement of a batch is preceded by an
|
|
15
|
+
implicit BEGIN, as usual. With ``autocommit=True`` every sync point is a commit point, so
|
|
16
|
+
statements in one batch succeed or fail together. Use ``batch_size`` or ``sync()`` to choose.
|
|
17
|
+
* Errors: when one statement fails the server skips the rest of the batch. Those statements
|
|
18
|
+
get :class:`pgtern.errors.PipelineAborted`, and the failing statement raises its own error
|
|
19
|
+
when read. Inside a transaction you must roll back afterwards, as with any failure.
|
|
20
|
+
* Only text format is used (``binary=True`` is not available in pipelines).
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from . import _protocol as P
|
|
28
|
+
from . import _query, _types, errors, hooks
|
|
29
|
+
from ._cursor import Cursor
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger("pgtern")
|
|
32
|
+
|
|
33
|
+
_IDLE = "I"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _enter_hooks(conn: Any, item: "_Item") -> tuple[hooks.QueryInfo | None, list]:
|
|
37
|
+
if item.hidden or not hooks._HOOKS:
|
|
38
|
+
return None, []
|
|
39
|
+
info = hooks.QueryInfo(item.redacted, conn._host, conn._port, conn._params.dbname, conn._params.user)
|
|
40
|
+
entered = []
|
|
41
|
+
for hook in tuple(hooks._HOOKS):
|
|
42
|
+
try:
|
|
43
|
+
cm = hook(info)
|
|
44
|
+
cm.__enter__()
|
|
45
|
+
entered.append(cm)
|
|
46
|
+
except Exception:
|
|
47
|
+
log.exception("a pgtern query hook failed on entry; ignoring it")
|
|
48
|
+
return info, entered
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _exit_hooks(entered: list, error: BaseException | None) -> None:
|
|
52
|
+
exc_info = (type(error), error, None) if error is not None else (None, None, None)
|
|
53
|
+
for cm in reversed(entered):
|
|
54
|
+
try:
|
|
55
|
+
cm.__exit__(*exc_info)
|
|
56
|
+
except Exception:
|
|
57
|
+
log.exception("a pgtern query hook failed on exit; ignoring it")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _Item:
|
|
61
|
+
__slots__ = ("cursor", "sql", "redacted", "hidden")
|
|
62
|
+
|
|
63
|
+
def __init__(self, cursor: Cursor | None, sql: str, redacted: str, hidden: bool = False) -> None:
|
|
64
|
+
self.cursor = cursor
|
|
65
|
+
self.sql = sql
|
|
66
|
+
self.redacted = redacted
|
|
67
|
+
self.hidden = hidden
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _encode(sql: str, params: Any) -> bytes:
|
|
71
|
+
if params is None:
|
|
72
|
+
return P.extended_query(sql, [], [], sync=False)
|
|
73
|
+
pg_sql, values = _query.translate(sql, params)
|
|
74
|
+
dumped = [_types.dump(v) for v in values]
|
|
75
|
+
return P.extended_query(pg_sql, [oid for oid, _ in dumped], [data for _, data in dumped], sync=False)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Pipeline:
|
|
79
|
+
"""Synchronous pipeline. Create it with :meth:`pgtern.Connection.pipeline`."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, conn: Any, batch_size: int = 64) -> None:
|
|
82
|
+
if batch_size < 1:
|
|
83
|
+
raise ValueError("batch_size must be at least 1")
|
|
84
|
+
self._conn = conn
|
|
85
|
+
self._batch_size = batch_size
|
|
86
|
+
self._queue: list[_Item] = []
|
|
87
|
+
self._begin_queued = False
|
|
88
|
+
self._open = False
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------- block
|
|
91
|
+
def __enter__(self) -> "Pipeline":
|
|
92
|
+
conn = self._conn
|
|
93
|
+
with conn._lock:
|
|
94
|
+
conn._check_open()
|
|
95
|
+
if conn._pipeline is not None:
|
|
96
|
+
raise errors.ProgrammingError("a pipeline is already active on this connection")
|
|
97
|
+
conn._pipeline = self
|
|
98
|
+
self._open = True
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
102
|
+
try:
|
|
103
|
+
if exc_type is None:
|
|
104
|
+
self.sync()
|
|
105
|
+
elif not self._conn.closed:
|
|
106
|
+
try:
|
|
107
|
+
self.sync() # read the replies so the connection stays usable
|
|
108
|
+
except Exception: # noqa: BLE001 - the original exception is what matters
|
|
109
|
+
pass
|
|
110
|
+
finally:
|
|
111
|
+
self._open = False
|
|
112
|
+
with self._conn._lock:
|
|
113
|
+
if self._conn._pipeline is self:
|
|
114
|
+
self._conn._pipeline = None
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------- queueing
|
|
117
|
+
def execute(self, query: Any, params: Any = None) -> Cursor:
|
|
118
|
+
"""Queue a statement; returns a cursor that holds its result after the next sync."""
|
|
119
|
+
conn = self._conn
|
|
120
|
+
with conn._lock:
|
|
121
|
+
conn._check_open()
|
|
122
|
+
if not self._open:
|
|
123
|
+
raise errors.ProgrammingError("the pipeline is not active (use it in a 'with' block)")
|
|
124
|
+
sql, redacted = conn._query_text(query)
|
|
125
|
+
if len(self._queue) >= self._batch_size:
|
|
126
|
+
self.sync()
|
|
127
|
+
if not conn._autocommit and conn._tx_status == _IDLE and not self._begin_queued:
|
|
128
|
+
conn._send(P.extended_query("BEGIN", [], [], sync=False))
|
|
129
|
+
self._queue.append(_Item(None, "BEGIN", "BEGIN", hidden=True))
|
|
130
|
+
self._begin_queued = True
|
|
131
|
+
cursor = Cursor(conn)
|
|
132
|
+
cursor._pipeline_pending = True
|
|
133
|
+
conn._send(_encode(sql, params))
|
|
134
|
+
self._queue.append(_Item(cursor, sql, redacted))
|
|
135
|
+
return cursor
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------- sync point
|
|
138
|
+
def sync(self) -> None:
|
|
139
|
+
"""Send Sync, read every queued reply and fill in the cursors."""
|
|
140
|
+
conn = self._conn
|
|
141
|
+
with conn._lock:
|
|
142
|
+
conn._check_open()
|
|
143
|
+
items, self._queue = self._queue, []
|
|
144
|
+
self._begin_queued = False
|
|
145
|
+
conn._send(P.SYNC)
|
|
146
|
+
failure: errors.Error | None = None
|
|
147
|
+
collected: list[tuple[Cursor, list]] = []
|
|
148
|
+
for item in items:
|
|
149
|
+
if failure is not None:
|
|
150
|
+
if item.cursor is not None:
|
|
151
|
+
item.cursor._pipeline_pending = False
|
|
152
|
+
item.cursor._pipeline_error = errors.PipelineAborted(
|
|
153
|
+
"not executed: an earlier statement in the pipeline failed")
|
|
154
|
+
continue
|
|
155
|
+
info, entered = _enter_hooks(conn, item)
|
|
156
|
+
results, error = self._read_reply(conn)
|
|
157
|
+
if info is not None and results is not None:
|
|
158
|
+
info.rowcount = results[-1].rowcount if results else -1
|
|
159
|
+
_exit_hooks(entered, error)
|
|
160
|
+
if error is not None:
|
|
161
|
+
failure = error
|
|
162
|
+
if item.cursor is not None:
|
|
163
|
+
item.cursor._pipeline_pending = False
|
|
164
|
+
item.cursor._pipeline_error = error
|
|
165
|
+
elif item.cursor is not None and results is not None:
|
|
166
|
+
collected.append((item.cursor, results))
|
|
167
|
+
while True: # the rest of a failed batch sends nothing but ReadyForQuery
|
|
168
|
+
tag, payload = conn._next_message()
|
|
169
|
+
if tag == 0x5A:
|
|
170
|
+
conn._tx_status = chr(payload[0])
|
|
171
|
+
break
|
|
172
|
+
if tag == 0x45:
|
|
173
|
+
fields = P.parse_error_fields(payload)
|
|
174
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
175
|
+
conn._mark_broken()
|
|
176
|
+
raise errors.error_from_fields(fields)
|
|
177
|
+
else:
|
|
178
|
+
conn._handle_async_message(tag, payload)
|
|
179
|
+
conn._prime_loaders([r for _, res in collected for r in res])
|
|
180
|
+
for cursor, res in collected:
|
|
181
|
+
cursor._pipeline_pending = False
|
|
182
|
+
cursor._set_results(res)
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _read_reply(conn: Any) -> tuple[list | None, errors.Error | None]:
|
|
186
|
+
"""Read one statement's messages. Returns (results, None), or (None, error)."""
|
|
187
|
+
collector = P.QueryCollector()
|
|
188
|
+
while True:
|
|
189
|
+
tag, payload = conn._next_message()
|
|
190
|
+
if tag == 0x45:
|
|
191
|
+
fields = P.parse_error_fields(payload)
|
|
192
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
193
|
+
conn._mark_broken()
|
|
194
|
+
raise errors.error_from_fields(fields)
|
|
195
|
+
return None, errors.error_from_fields(fields)
|
|
196
|
+
if tag in (0x43, 0x49): # C CommandComplete, I EmptyQueryResponse: this statement is done
|
|
197
|
+
collector.handle(tag, payload)
|
|
198
|
+
return collector.results, None
|
|
199
|
+
if tag in (0x4E, 0x53, 0x41):
|
|
200
|
+
conn._handle_async_message(tag, payload)
|
|
201
|
+
else:
|
|
202
|
+
collector.handle(tag, payload)
|