Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 5c3c3cd

Browse files
committed
Enhanced custom error in PLPythonu
Patch adds a new, more rich, way to emit error message or exception from PL/Pythonu code. Author: Pavel Stehule Reviewers: Catalin Iacob, Peter Eisentraut, Jim Nasby
1 parent 5364b35 commit 5c3c3cd

File tree

7 files changed

+701
-81
lines changed

7 files changed

+701
-81
lines changed

doc/src/sgml/plpython.sgml

+49-17
Original file line numberDiff line numberDiff line change
@@ -1341,31 +1341,63 @@ $$ LANGUAGE plpythonu;
13411341
<title>Utility Functions</title>
13421342
<para>
13431343
The <literal>plpy</literal> module also provides the functions
1344-
<literal>plpy.debug(<replaceable>msg</>)</literal>,
1345-
<literal>plpy.log(<replaceable>msg</>)</literal>,
1346-
<literal>plpy.info(<replaceable>msg</>)</literal>,
1347-
<literal>plpy.notice(<replaceable>msg</>)</literal>,
1348-
<literal>plpy.warning(<replaceable>msg</>)</literal>,
1349-
<literal>plpy.error(<replaceable>msg</>)</literal>, and
1350-
<literal>plpy.fatal(<replaceable>msg</>)</literal>.<indexterm><primary>elog</><secondary>in PL/Python</></indexterm>
1351-
<function>plpy.error</function> and
1352-
<function>plpy.fatal</function> actually raise a Python exception
1353-
which, if uncaught, propagates out to the calling query, causing
1354-
the current transaction or subtransaction to be aborted.
1355-
<literal>raise plpy.Error(<replaceable>msg</>)</literal> and
1344+
<literal>plpy.debug(<replaceable>msg, **kwargs</>)</literal>,
1345+
<literal>plpy.log(<replaceable>msg, **kwargs</>)</literal>,
1346+
<literal>plpy.info(<replaceable>msg, **kwargs</>)</literal>,
1347+
<literal>plpy.notice(<replaceable>msg, **kwargs</>)</literal>,
1348+
<literal>plpy.warning(<replaceable>msg, **kwargs</>)</literal>,
1349+
<literal>plpy.error(<replaceable>msg, **kwargs</>)</literal>, and
1350+
<literal>plpy.fatal(<replaceable>msg, **kwargs</>)</literal>.
1351+
<indexterm><primary>elog</><secondary>in PL/Python</></indexterm>
1352+
<function>plpy.error</function> and <function>plpy.fatal</function>
1353+
actually raise a Python exception which, if uncaught, propagates out to
1354+
the calling query, causing the current transaction or subtransaction to
1355+
be aborted. <literal>raise plpy.Error(<replaceable>msg</>)</literal> and
13561356
<literal>raise plpy.Fatal(<replaceable>msg</>)</literal> are
1357-
equivalent to calling
1358-
<function>plpy.error</function> and
1359-
<function>plpy.fatal</function>, respectively.
1360-
The other functions only generate messages of different
1361-
priority levels.
1357+
equivalent to calling <literal>plpy.error(<replaceable>msg</>)</literal> and
1358+
<literal>plpy.fatal(<replaceable>msg</>)</literal>, respectively but
1359+
the <literal>raise</literal> form does not allow passing keyword arguments.
1360+
The other functions only generate messages of different priority levels.
13621361
Whether messages of a particular priority are reported to the client,
13631362
written to the server log, or both is controlled by the
13641363
<xref linkend="guc-log-min-messages"> and
13651364
<xref linkend="guc-client-min-messages"> configuration
13661365
variables. See <xref linkend="runtime-config"> for more information.
13671366
</para>
13681367

1368+
<para>
1369+
1370+
The <replaceable>msg</> argument is given as a positional argument. For
1371+
backward compatibility, more than one positional argument can be given. In
1372+
that case, the string representation of the tuple of positional arguments
1373+
becomes the message reported to the client.
1374+
The following keyword-only arguments are accepted:
1375+
<literal>
1376+
<replaceable>detail</replaceable>, <replaceable>hint</replaceable>,
1377+
<replaceable>sqlstate</replaceable>, <replaceable>schema</replaceable>,
1378+
<replaceable>table</replaceable>, <replaceable>column</replaceable>,
1379+
<replaceable>datatype</replaceable> , <replaceable>constraint</replaceable>
1380+
</literal>.
1381+
The string representation of the objects passed as keyword-only arguments
1382+
is used to enrich the messages reported to the client. For example:
1383+
1384+
<programlisting>
1385+
CREATE FUNCTION raise_custom_exception() RETURNS void AS $$
1386+
plpy.error("custom exception message", detail = "some info about exception", hint = "hint for users")
1387+
$$ LANGUAGE plpythonu;
1388+
1389+
postgres=# select raise_custom_exception();
1390+
ERROR: XX000: plpy.Error: custom exception message
1391+
DETAIL: some info about exception
1392+
HINT: hint for users
1393+
CONTEXT: Traceback (most recent call last):
1394+
PL/Python function "raise_custom_exception", line 2, in &lt;module&gt;
1395+
plpy.error("custom exception message", detail = "some info about exception", hint = "hint for users")
1396+
PL/Python function "raise_custom_exception"
1397+
LOCATION: PLy_elog, plpy_elog.c:132
1398+
</programlisting>
1399+
</para>
1400+
13691401
<para>
13701402
Another set of utility functions are
13711403
<literal>plpy.quote_literal(<replaceable>string</>)</literal>,

src/pl/plpython/expected/plpython_test.out

+190-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ select module_contents();
4848
Error, Fatal, SPIError, cursor, debug, error, execute, fatal, info, log, notice, prepare, quote_ident, quote_literal, quote_nullable, spiexceptions, subtransaction, warning
4949
(1 row)
5050

51-
CREATE FUNCTION elog_test() RETURNS void
51+
CREATE FUNCTION elog_test_basic() RETURNS void
5252
AS $$
5353
plpy.debug('debug')
5454
plpy.log('log')
@@ -60,7 +60,7 @@ plpy.notice('notice')
6060
plpy.warning('warning')
6161
plpy.error('error')
6262
$$ LANGUAGE plpythonu;
63-
SELECT elog_test();
63+
SELECT elog_test_basic();
6464
INFO: info
6565
INFO: 37
6666
INFO: ()
@@ -69,6 +69,193 @@ NOTICE: notice
6969
WARNING: warning
7070
ERROR: plpy.Error: error
7171
CONTEXT: Traceback (most recent call last):
72-
PL/Python function "elog_test", line 10, in <module>
72+
PL/Python function "elog_test_basic", line 10, in <module>
7373
plpy.error('error')
74+
PL/Python function "elog_test_basic"
75+
CREATE FUNCTION elog_test() RETURNS void
76+
AS $$
77+
plpy.debug('debug', detail = 'some detail')
78+
plpy.log('log', detail = 'some detail')
79+
plpy.info('info', detail = 'some detail')
80+
plpy.info()
81+
plpy.info('the question', detail = 42);
82+
plpy.info('This is message text.',
83+
detail = 'This is detail text',
84+
hint = 'This is hint text.',
85+
sqlstate = 'XX000',
86+
schema = 'any info about schema',
87+
table = 'any info about table',
88+
column = 'any info about column',
89+
datatype = 'any info about datatype',
90+
constraint = 'any info about constraint')
91+
plpy.notice('notice', detail = 'some detail')
92+
plpy.warning('warning', detail = 'some detail')
93+
plpy.error('stop on error', detail = 'some detail', hint = 'some hint')
94+
$$ LANGUAGE plpythonu;
95+
SELECT elog_test();
96+
INFO: info
97+
DETAIL: some detail
98+
INFO: ()
99+
INFO: the question
100+
DETAIL: 42
101+
INFO: This is message text.
102+
DETAIL: This is detail text
103+
HINT: This is hint text.
104+
NOTICE: notice
105+
DETAIL: some detail
106+
WARNING: warning
107+
DETAIL: some detail
108+
ERROR: plpy.Error: stop on error
109+
DETAIL: some detail
110+
HINT: some hint
111+
CONTEXT: Traceback (most recent call last):
112+
PL/Python function "elog_test", line 18, in <module>
113+
plpy.error('stop on error', detail = 'some detail', hint = 'some hint')
74114
PL/Python function "elog_test"
115+
do $$ plpy.info('other types', detail = (10,20)) $$ LANGUAGE plpythonu;
116+
INFO: other types
117+
DETAIL: (10, 20)
118+
do $$
119+
import time;
120+
from datetime import date
121+
plpy.info('other types', detail = date(2016,2,26))
122+
$$ LANGUAGE plpythonu;
123+
INFO: other types
124+
DETAIL: 2016-02-26
125+
do $$
126+
basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
127+
plpy.info('other types', detail = basket)
128+
$$ LANGUAGE plpythonu;
129+
INFO: other types
130+
DETAIL: ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
131+
-- should fail
132+
do $$ plpy.info('wrong sqlstate', sqlstate='54444A') $$ LANGUAGE plpythonu;
133+
ERROR: invalid SQLSTATE code
134+
CONTEXT: PL/Python anonymous code block
135+
do $$ plpy.info('unsupported argument', blabla='fooboo') $$ LANGUAGE plpythonu;
136+
ERROR: 'blabla' is an invalid keyword argument for this function
137+
CONTEXT: PL/Python anonymous code block
138+
do $$ plpy.info('first message', message='second message') $$ LANGUAGE plpythonu;
139+
ERROR: the message is already specified
140+
CONTEXT: PL/Python anonymous code block
141+
do $$ plpy.info('first message', 'second message', message='third message') $$ LANGUAGE plpythonu;
142+
ERROR: the message is already specified
143+
CONTEXT: PL/Python anonymous code block
144+
-- raise exception in python, handle exception in plgsql
145+
CREATE OR REPLACE FUNCTION raise_exception(_message text, _detail text DEFAULT NULL, _hint text DEFAULT NULL,
146+
_sqlstate text DEFAULT NULL,
147+
_schema text DEFAULT NULL, _table text DEFAULT NULL, _column text DEFAULT NULL,
148+
_datatype text DEFAULT NULL, _constraint text DEFAULT NULL)
149+
RETURNS void AS $$
150+
kwargs = { "message":_message, "detail":_detail, "hint":_hint,
151+
"sqlstate":_sqlstate, "schema":_schema, "table":_table,
152+
"column":_column, "datatype":_datatype, "constraint":_constraint }
153+
# ignore None values
154+
plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
155+
$$ LANGUAGE plpythonu;
156+
SELECT raise_exception('hello', 'world');
157+
ERROR: plpy.Error: hello
158+
DETAIL: world
159+
CONTEXT: Traceback (most recent call last):
160+
PL/Python function "raise_exception", line 6, in <module>
161+
plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
162+
PL/Python function "raise_exception"
163+
SELECT raise_exception('message text', 'detail text', _sqlstate => 'YY333');
164+
ERROR: plpy.Error: message text
165+
DETAIL: detail text
166+
CONTEXT: Traceback (most recent call last):
167+
PL/Python function "raise_exception", line 6, in <module>
168+
plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
169+
PL/Python function "raise_exception"
170+
SELECT raise_exception(_message => 'message text',
171+
_detail => 'detail text',
172+
_hint => 'hint text',
173+
_sqlstate => 'XX555',
174+
_schema => 'schema text',
175+
_table => 'table text',
176+
_column => 'column text',
177+
_datatype => 'datatype text',
178+
_constraint => 'constraint text');
179+
ERROR: plpy.Error: message text
180+
DETAIL: detail text
181+
HINT: hint text
182+
CONTEXT: Traceback (most recent call last):
183+
PL/Python function "raise_exception", line 6, in <module>
184+
plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
185+
PL/Python function "raise_exception"
186+
SELECT raise_exception(_message => 'message text',
187+
_hint => 'hint text',
188+
_schema => 'schema text',
189+
_column => 'column text',
190+
_constraint => 'constraint text');
191+
ERROR: plpy.Error: message text
192+
HINT: hint text
193+
CONTEXT: Traceback (most recent call last):
194+
PL/Python function "raise_exception", line 6, in <module>
195+
plpy.error(**dict((k, v) for k, v in iter(kwargs.items()) if v))
196+
PL/Python function "raise_exception"
197+
DO $$
198+
DECLARE
199+
__message text;
200+
__detail text;
201+
__hint text;
202+
__sqlstate text;
203+
__schema_name text;
204+
__table_name text;
205+
__column_name text;
206+
__datatype text;
207+
__constraint text;
208+
BEGIN
209+
BEGIN
210+
PERFORM raise_exception(_message => 'message text',
211+
_detail => 'detail text',
212+
_hint => 'hint text',
213+
_sqlstate => 'XX555',
214+
_schema => 'schema text',
215+
_table => 'table text',
216+
_column => 'column text',
217+
_datatype => 'datatype text',
218+
_constraint => 'constraint text');
219+
EXCEPTION WHEN SQLSTATE 'XX555' THEN
220+
GET STACKED DIAGNOSTICS __message = MESSAGE_TEXT,
221+
__detail = PG_EXCEPTION_DETAIL,
222+
__hint = PG_EXCEPTION_HINT,
223+
__sqlstate = RETURNED_SQLSTATE,
224+
__schema_name = SCHEMA_NAME,
225+
__table_name = TABLE_NAME,
226+
__column_name = COLUMN_NAME,
227+
__datatype = PG_DATATYPE_NAME,
228+
__constraint = CONSTRAINT_NAME;
229+
RAISE NOTICE 'handled exception'
230+
USING DETAIL = format('message:(%s), detail:(%s), hint: (%s), sqlstate: (%s), '
231+
'schema:(%s), table:(%s), column:(%s), datatype:(%s), constraint:(%s)',
232+
__message, __detail, __hint, __sqlstate, __schema_name,
233+
__table_name, __column_name, __datatype, __constraint);
234+
END;
235+
END;
236+
$$;
237+
NOTICE: handled exception
238+
DETAIL: message:(plpy.Error: message text), detail:(detail text), hint: (hint text), sqlstate: (XX555), schema:(schema text), table:(table text), column:(column text), datatype:(datatype text), constraint:(constraint text)
239+
-- the displayed context is different between Python2 and Python3,
240+
-- but that's not important for this test
241+
\set SHOW_CONTEXT never
242+
do $$
243+
try:
244+
plpy.execute("select raise_exception(_message => 'my message', _sqlstate => 'XX987', _hint => 'some hint', _table=> 'users_tab', _datatype => 'user_type')")
245+
except Exception, e:
246+
plpy.info(e.spidata)
247+
raise e
248+
$$ LANGUAGE plpythonu;
249+
INFO: (119577128, None, 'some hint', None, 0, None, 'users_tab', None, 'user_type', None)
250+
ERROR: plpy.SPIError: plpy.Error: my message
251+
HINT: some hint
252+
do $$
253+
try:
254+
plpy.error(message = 'my message', sqlstate = 'XX987', hint = 'some hint', table = 'users_tab', datatype = 'user_type')
255+
except Exception, e:
256+
plpy.info('sqlstate: %s, hint: %s, tablename: %s, datatype: %s' % (e.sqlstate, e.hint, e.table_name, e.datatype_name))
257+
raise e
258+
$$ LANGUAGE plpythonu;
259+
INFO: sqlstate: XX987, hint: some hint, tablename: users_tab, datatype: user_type
260+
ERROR: plpy.Error: my message
261+
HINT: some hint

0 commit comments

Comments
 (0)