Skip to content

Commit f406ea9

Browse files
author
James William Pye
committed
Implement TupleError for providing details on pack/unpack failures.
This gives the user information about what failed when a row could not be packed or unpacked. It uses exception __context__'s to reference the original failure while providing the user with an appropriately general error leading them to the specific piece of data in the tuple that was invalid. Some additional cleanups on the error's details may be necessary. It's probably using too many fields, atm. Additionally, "NUMBER" and "NAME" labels are too ambiguous.
1 parent d9416bf commit f406ea9

9 files changed

Lines changed: 319 additions & 35 deletions

File tree

postgresql/documentation/driver.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,15 +545,16 @@
545545
promptly cast into a date. Of course, without the explicit cast as text, the
546546
outcome would be different::
547547
548-
>>> ps = db.prepare("SELECT $1::text::date")
548+
>>> ps = db.prepare("SELECT $1::date")
549549
>>> ps.first('yesterday')
550550
Traceback:
551551
...
552-
AttributeError: 'str' object has no attribute 'toordinal'
552+
postgresql.exceptions.TupleError
553553
554554
The function that processes the parameter expects a `datetime.date` object, and
555555
the given `str` object does not provide the necessary interfaces for the
556-
conversion.
556+
conversion, so the driver raises a TupleError from the original conversion
557+
exception.
557558
558559
559560
Inserting and DML

postgresql/driver/pq3.py

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,9 +342,48 @@ def close(self):
342342
)
343343
self.closed = True
344344

345+
def _raise_parameter_tuple_error(self, procs, tup, itemnum):
346+
# The element traceback will include the full list of parameters.
347+
param = repr(tup[itemnum])
348+
if len(param) > 80:
349+
# Be sure not to fill screen with noise.
350+
param = param[:75] + ' ...'
351+
te = pg_exc.TupleError(
352+
"failed to pack parameter for transfer",
353+
details = {
354+
'parameter': param,
355+
'type' : self.statement.sql_parameter_types[itemnum],
356+
'number' : itemnum,
357+
'hint' : "Try casting parameter to 'text', then to the target type."
358+
},
359+
)
360+
self.ife_descend(te)
361+
te.raise_exception()
362+
363+
def _raise_column_tuple_error(self, procs, tup, itemnum):
364+
'for column processing'
365+
# The element traceback will include the full list of parameters.
366+
coldata = repr(tup[itemnum])
367+
if len(coldata) > 80:
368+
# Be sure not to fill screen with noise.
369+
coldata = coldata[:75] + ' ...'
370+
te = pg_exc.TupleError(
371+
"failed to unpack column from wire data",
372+
details = {
373+
'column': coldata,
374+
'type' : self.sql_column_types[itemnum],
375+
'name' : repr(self.column_names[itemnum]),
376+
'number' : itemnum,
377+
'hint' : "Try casting the column to 'text'."
378+
},
379+
)
380+
self.ife_descend(te)
381+
te.raise_exception()
382+
345383
def _pq_parameters(self):
346384
return pg_typio.process_tuple(
347385
self.statement._input_io, self.parameters,
386+
self._raise_parameter_tuple_error
348387
)
349388

350389
def _init(self):
@@ -642,10 +681,11 @@ def _buffer_more(self, quantity, direction):
642681
return self._last_increase
643682

644683
def _expansion(self):
684+
cte = self._raise_column_tuple_error
645685
return [
646686
pg_types.Row(
647687
pg_typio.process_tuple(
648-
self._output_io, y,
688+
self._output_io, y, cte
649689
),
650690
keymap = self._output_attmap
651691
)
@@ -1003,6 +1043,8 @@ def from_string(
10031043
return r
10041044

10051045
def __init__(self, statement_id, database):
1046+
if not statement_id:
1047+
raise ValueError("invalid statement identifier, " + repr(cursor_id))
10061048
self.statement_id = statement_id
10071049
self.database = database
10081050
self._pq_xact = None
@@ -1017,6 +1059,38 @@ def __repr__(self):
10171059
state = self.state,
10181060
)
10191061

1062+
def _raise_parameter_tuple_error(self, procs, tup, itemnum):
1063+
te = pg_exc.TupleError(
1064+
"failed to pack parameter for transfer",
1065+
details = {
1066+
'parameter': tup[itemnum],
1067+
'type' : self.sql_parameter_types[itemnum],
1068+
'number' : itemnum,
1069+
'arguments' : tup,
1070+
'hint' : "Try casting the parameter to 'text', then to the target type."
1071+
},
1072+
)
1073+
self.ife_descend(te)
1074+
te.raise_exception()
1075+
1076+
def _raise_column_tuple_error(self, procs, tup, itemnum):
1077+
coldata = repr(tup[itemnum])
1078+
if len(coldata) > 80:
1079+
# Be sure not to fill screen with noise.
1080+
coldata = coldata[:75] + ' ...'
1081+
te = pg_exc.TupleError(
1082+
"failed to unpack column from wire data",
1083+
details = {
1084+
'column': coldata,
1085+
'type' : self.sql_column_types[itemnum],
1086+
'number' : itemnum,
1087+
'name' : repr(self.column_names[itemnum]),
1088+
'hint' : "Try casting the column to 'text'."
1089+
},
1090+
)
1091+
self.ife_descend(te)
1092+
te.raise_exception()
1093+
10201094
@property
10211095
def state(self) -> str:
10221096
if self.closed:
@@ -1210,6 +1284,7 @@ def first(self, *parameters):
12101284
if self._input_io:
12111285
params = pg_typio.process_tuple(
12121286
self._input_io, parameters,
1287+
self._raise_parameter_tuple_error
12131288
)
12141289
else:
12151290
params = ()
@@ -1241,7 +1316,10 @@ def first(self, *parameters):
12411316

12421317
if len(self._output_io) > 1:
12431318
return pg_types.Row(
1244-
pg_typio.process_tuple(self._output_io, xt),
1319+
pg_typio.process_tuple(
1320+
self._output_io, xt,
1321+
self._raise_column_tuple_error
1322+
),
12451323
keymap = self._output_attmap
12461324
)
12471325
else:
@@ -1327,12 +1405,15 @@ def _load_bulk_tuples(self, tupleseq, tps = None):
13271405
tps = tps or 64
13281406
last = pq.element.FlushMessage
13291407
tupleseqiter = iter(tupleseq)
1408+
pte = self._raise_parameter_tuple_error
13301409
try:
13311410
while last is pq.element.FlushMessage:
13321411
c = 0
13331412
xm = []
13341413
for t in tupleseqiter:
1335-
params = pg_typio.process_tuple(self._input_io, tuple(t))
1414+
params = pg_typio.process_tuple(
1415+
self._input_io, tuple(t), pte
1416+
)
13361417
xm.extend((
13371418
pq.element.Bind(
13381419
b'',

postgresql/exceptions.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,19 @@ class AuthenticationMethodError(DriverError):
122122
Server requested an authentication method that is not supported by the
123123
driver.
124124
"""
125+
class InsecurityError(DriverError):
126+
"""
127+
Error signifying a secure channel to a server cannot be established.
128+
"""
129+
source = 'DRIVER'
130+
class TupleError(DriverError):
131+
"""
132+
Driver failed to pack or unpack a tuple.
133+
"""
134+
125135
class OperationError(DriverError):
126136
"""
127137
An invalid operation on an interface element.
128-
129-
Usually this occurs in dynamically configured instances where the action is
130-
not valid for the finalized type.
131-
132-
For instance, calling the seek() method on a cursor who's query is a COPY.
133138
"""
134139

135140
##
@@ -148,12 +153,6 @@ class ClusterNotRunningError(ClusterError):
148153
class ClusterTimeoutError(ClusterError):
149154
pass
150155

151-
class InsecurityError(Error):
152-
"""
153-
Error signifying a secure channel to a server cannot be established.
154-
"""
155-
source = 'DRIVER'
156-
157156
class TransactionError(Error):
158157
pass
159158

postgresql/protocol/optimized.c

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,10 @@ parse_tuple_message(PyObject *self, PyObject *args)
160160
static PyObject *
161161
process_tuple(PyObject *self, PyObject *args)
162162
{
163-
PyObject *tup, *procs, *rob;
163+
PyObject *tup, *procs, *fail, *rob;
164164
Py_ssize_t len, i;
165165

166-
if (!PyArg_ParseTuple(args, "OO", &procs, &tup))
166+
if (!PyArg_ParseTuple(args, "OOO", &procs, &tup, &fail))
167167
return(NULL);
168168

169169
if (!PyObject_IsInstance(procs, (PyObject *) &PyTuple_Type))
@@ -201,23 +201,71 @@ process_tuple(PyObject *self, PyObject *args)
201201
for (i = 0; i < len; ++i)
202202
{
203203
PyObject *p, *o, *ot, *r;
204+
/*
205+
* If it's Py_None, that means it's NULL. No processing necessary.
206+
*/
204207
o = PyTuple_GET_ITEM(tup, i);
205208
if (o == Py_None)
206209
{
207210
Py_INCREF(Py_None);
208211
PyTuple_SET_ITEM(rob, i, Py_None);
209212
continue;
210213
}
214+
211215
p = PyTuple_GET_ITEM(procs, i);
216+
/*
217+
* Temp tuple for applying *args to p.
218+
*/
212219
ot = PyTuple_New(1);
213220
PyTuple_SET_ITEM(ot, 0, o);
214221
Py_INCREF(o);
222+
215223
r = PyObject_CallObject(p, ot);
216224
Py_DECREF(ot);
217225
if (r == NULL)
218226
{
227+
/*
228+
* Exception from p(*ot)
229+
*/
219230
Py_DECREF(rob);
220231
rob = NULL;
232+
if (PyErr_ExceptionMatches(PyExc_Exception))
233+
{
234+
PyObject *failargs, *failedat;
235+
/*
236+
* It's *not* a BaseException.
237+
*/
238+
failedat = PyLong_FromSsize_t(i);
239+
if (failedat != NULL)
240+
{
241+
failargs = PyTuple_New(3);
242+
if (failargs != NULL)
243+
{
244+
PyTuple_SET_ITEM(failargs, 0, procs);
245+
Py_INCREF(procs);
246+
PyTuple_SET_ITEM(failargs, 1, tup);
247+
Py_INCREF(tup);
248+
PyTuple_SET_ITEM(failargs, 2, failedat);
249+
r = PyObject_CallObject(fail, failargs);
250+
Py_DECREF(failargs);
251+
if (r != NULL)
252+
{
253+
PyErr_SetString(PyExc_RuntimeError,
254+
"process_tuple exception handler failed to raise"
255+
);
256+
Py_DECREF(r);
257+
}
258+
}
259+
else
260+
{
261+
Py_DECREF(failedat);
262+
}
263+
}
264+
}
265+
266+
/*
267+
* Break out of loop to return(NULL);
268+
*/
221269
break;
222270
}
223271
PyTuple_SET_ITEM(rob, i, r);

0 commit comments

Comments
 (0)