On 07/03/11 14:01, Jan Urbański wrote:
> On 07/03/11 13:53, Peter Eisentraut wrote:
>> On sön, 2011-03-06 at 13:14 +0100, Jan Urbański wrote:
>>> But fixing "raise plpy.Fatal()"
>>> to actually cause a FATAL is something that should be extracted from
>>> this patch and committed, even if the full patch does not make it.
>>
>> Um, what?  I didn't find any details about this in this thread, nor a
>> test case.

> So this in fact are three separate things, tracebacks, fix for
> plpy.Fatal and a one-line fix for reporting errors in Python iterators,
> that as I noticed has a side effect of changing the SQLCODE being raised
> :( I think I'll just respin the tracebacks patch as 3 separate ones,
> coming right up.

Respun as three separate patches. Sorry for the confusion. BTW: looks
like plpy.Fatal behaviour has been broken for quite some time now.

Jan
>From 06ac95d62de1aaf40dec020ac2892f20c3879db6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Urba=C5=84ski?= <wulc...@wulczer.org>
Date: Mon, 7 Mar 2011 14:16:25 +0100
Subject: [PATCH 3/3] Add Python tracebacks to error messages.

For errors originating from Python exceptions add the traceback to the
message context. While at it rework the Python to Postgres error
passing mechanism a bit. A future optimisation might be not splitting
the procedure source each time a traceback is generated, but for now
it's probably not the most important scenario to optimise for.
---
 src/pl/plpython/expected/plpython_do.out           |    5 +-
 src/pl/plpython/expected/plpython_error.out        |  193 +++++++++++++-
 src/pl/plpython/expected/plpython_error_0.out      |  193 +++++++++++++-
 .../plpython/expected/plpython_subtransaction.out  |   55 +++-
 .../expected/plpython_subtransaction_0.out         |   30 ++-
 .../expected/plpython_subtransaction_5.out         |   30 ++-
 src/pl/plpython/expected/plpython_test.out         |    5 +-
 src/pl/plpython/expected/plpython_types.out        |    5 +-
 src/pl/plpython/expected/plpython_types_3.out      |    5 +-
 src/pl/plpython/plpython.c                         |  287 ++++++++++++++++----
 src/pl/plpython/sql/plpython_error.sql             |  105 +++++++
 11 files changed, 821 insertions(+), 92 deletions(-)

diff --git a/src/pl/plpython/expected/plpython_do.out b/src/pl/plpython/expected/plpython_do.out
index a21b088..41b7a51 100644
--- a/src/pl/plpython/expected/plpython_do.out
+++ b/src/pl/plpython/expected/plpython_do.out
@@ -3,4 +3,7 @@ NOTICE:  This is plpythonu.
 CONTEXT:  PL/Python anonymous code block
 DO $$ nonsense $$ LANGUAGE plpythonu;
 ERROR:  NameError: global name 'nonsense' is not defined
-CONTEXT:  PL/Python anonymous code block
+CONTEXT:  Traceback (most recent call last):
+  PL/Python anonymous code block, line 1, in <module>
+    nonsense 
+PL/Python anonymous code block
diff --git a/src/pl/plpython/expected/plpython_error.out b/src/pl/plpython/expected/plpython_error.out
index e38ea60..0b7d87f 100644
--- a/src/pl/plpython/expected/plpython_error.out
+++ b/src/pl/plpython/expected/plpython_error.out
@@ -36,7 +36,10 @@ ERROR:  spiexceptions.SyntaxError: syntax error at or near "syntax"
 LINE 1: syntax error
         ^
 QUERY:  syntax error
-CONTEXT:  PL/Python function "sql_syntax_error"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "sql_syntax_error", line 1, in <module>
+    plpy.execute("syntax error")
+PL/Python function "sql_syntax_error"
 /* check the handling of uncaught python exceptions
  */
 CREATE FUNCTION exception_index_invalid(text) RETURNS text
@@ -45,7 +48,10 @@ CREATE FUNCTION exception_index_invalid(text) RETURNS text
 	LANGUAGE plpythonu;
 SELECT exception_index_invalid('test');
 ERROR:  IndexError: list index out of range
-CONTEXT:  PL/Python function "exception_index_invalid"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "exception_index_invalid", line 1, in <module>
+    return args[1]
+PL/Python function "exception_index_invalid"
 /* check handling of nested exceptions
  */
 CREATE FUNCTION exception_index_invalid_nested() RETURNS text
@@ -59,7 +65,10 @@ LINE 1: SELECT test5('foo')
                ^
 HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
 QUERY:  SELECT test5('foo')
-CONTEXT:  PL/Python function "exception_index_invalid_nested"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "exception_index_invalid_nested", line 1, in <module>
+    rv = plpy.execute("SELECT test5('foo')")
+PL/Python function "exception_index_invalid_nested"
 /* a typo
  */
 CREATE FUNCTION invalid_type_uncaught(a text) RETURNS text
@@ -75,7 +84,10 @@ return None
 	LANGUAGE plpythonu;
 SELECT invalid_type_uncaught('rick');
 ERROR:  spiexceptions.UndefinedObject: type "test" does not exist
-CONTEXT:  PL/Python function "invalid_type_uncaught"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "invalid_type_uncaught", line 3, in <module>
+    SD["plan"] = plpy.prepare(q, [ "test" ])
+PL/Python function "invalid_type_uncaught"
 /* for what it's worth catch the exception generated by
  * the typo, and return None
  */
@@ -121,7 +133,10 @@ return None
 	LANGUAGE plpythonu;
 SELECT invalid_type_reraised('rick');
 ERROR:  plpy.Error: type "test" does not exist
-CONTEXT:  PL/Python function "invalid_type_reraised"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "invalid_type_reraised", line 6, in <module>
+    plpy.error(str(ex))
+PL/Python function "invalid_type_reraised"
 /* no typo no messing about
  */
 CREATE FUNCTION valid_type(a text) RETURNS text
@@ -140,6 +155,164 @@ SELECT valid_type('rick');
  
 (1 row)
 
+/* error in nested functions to get a traceback
+*/
+CREATE FUNCTION nested_error() RETURNS text
+	AS
+'def fun1():
+	plpy.error("boom")
+
+def fun2():
+	fun1()
+
+def fun3():
+	fun2()
+
+fun3()
+return "not reached"
+'
+	LANGUAGE plpythonu;
+SELECT nested_error();
+ERROR:  plpy.Error: boom
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "nested_error", line 10, in <module>
+    fun3()
+  PL/Python function "nested_error", line 8, in fun3
+    fun2()
+  PL/Python function "nested_error", line 5, in fun2
+    fun1()
+  PL/Python function "nested_error", line 2, in fun1
+    plpy.error("boom")
+PL/Python function "nested_error"
+/* raising plpy.Error is just like calling plpy.error
+*/
+CREATE FUNCTION nested_error_raise() RETURNS text
+	AS
+'def fun1():
+	raise plpy.Error("boom")
+
+def fun2():
+	fun1()
+
+def fun3():
+	fun2()
+
+fun3()
+return "not reached"
+'
+	LANGUAGE plpythonu;
+SELECT nested_error_raise();
+ERROR:  plpy.Error: boom
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "nested_error_raise", line 10, in <module>
+    fun3()
+  PL/Python function "nested_error_raise", line 8, in fun3
+    fun2()
+  PL/Python function "nested_error_raise", line 5, in fun2
+    fun1()
+  PL/Python function "nested_error_raise", line 2, in fun1
+    raise plpy.Error("boom")
+PL/Python function "nested_error_raise"
+/* using plpy.warning should not produce a traceback
+*/
+CREATE FUNCTION nested_warning() RETURNS text
+	AS
+'def fun1():
+	plpy.warning("boom")
+
+def fun2():
+	fun1()
+
+def fun3():
+	fun2()
+
+fun3()
+return "you''ve been warned"
+'
+	LANGUAGE plpythonu;
+SELECT nested_warning();
+WARNING:  boom
+CONTEXT:  PL/Python function "nested_warning"
+   nested_warning   
+--------------------
+ you've been warned
+(1 row)
+
+/* AttirbuteError at toplevel used to give segfaults with the traceback
+*/
+CREATE FUNCTION toplevel_attribute_error() RETURNS void AS
+$$
+plpy.nonexistent
+$$ LANGUAGE plpythonu;
+SELECT toplevel_attribute_error();
+ERROR:  AttributeError: 'module' object has no attribute 'nonexistent'
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "toplevel_attribute_error", line 2, in <module>
+    plpy.nonexistent
+PL/Python function "toplevel_attribute_error"
+/* Calling PL/Python functions from SQL and vice versa should not lose context.
+ */
+CREATE OR REPLACE FUNCTION python_traceback() RETURNS void AS $$
+def first():
+  second()
+
+def second():
+  third()
+
+def third():
+  plpy.execute("select sql_error()")
+
+first()
+$$ LANGUAGE plpythonu;
+CREATE OR REPLACE FUNCTION sql_error() RETURNS void AS $$
+begin
+  select 1/0;
+end
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION python_from_sql_error() RETURNS void AS $$
+begin
+  select python_traceback();
+end
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION sql_from_python_error() RETURNS void AS $$
+plpy.execute("select sql_error()")
+$$ LANGUAGE plpythonu;
+SELECT python_traceback();
+ERROR:  spiexceptions.DivisionByZero: division by zero
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "python_traceback", line 11, in <module>
+    first()
+  PL/Python function "python_traceback", line 3, in first
+    second()
+  PL/Python function "python_traceback", line 6, in second
+    third()
+  PL/Python function "python_traceback", line 9, in third
+    plpy.execute("select sql_error()")
+PL/Python function "python_traceback"
+SELECT sql_error();
+ERROR:  division by zero
+CONTEXT:  SQL statement "select 1/0"
+PL/pgSQL function "sql_error" line 3 at SQL statement
+SELECT python_from_sql_error();
+ERROR:  spiexceptions.DivisionByZero: division by zero
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "python_traceback", line 11, in <module>
+    first()
+  PL/Python function "python_traceback", line 3, in first
+    second()
+  PL/Python function "python_traceback", line 6, in second
+    third()
+  PL/Python function "python_traceback", line 9, in third
+    plpy.execute("select sql_error()")
+PL/Python function "python_traceback"
+SQL statement "select python_traceback()"
+PL/pgSQL function "python_from_sql_error" line 3 at SQL statement
+SELECT sql_from_python_error();
+ERROR:  spiexceptions.DivisionByZero: division by zero
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "sql_from_python_error", line 2, in <module>
+    plpy.execute("select sql_error()")
+PL/Python function "sql_from_python_error"
 /* check catching specific types of exceptions
  */
 CREATE TABLE specific (
@@ -187,7 +360,10 @@ plpy.execute("rollback to save")
 $$ LANGUAGE plpythonu;
 SELECT manual_subxact();
 ERROR:  plpy.SPIError: SPI_execute failed: SPI_ERROR_TRANSACTION
-CONTEXT:  PL/Python function "manual_subxact"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "manual_subxact", line 2, in <module>
+    plpy.execute("savepoint save")
+PL/Python function "manual_subxact"
 /* same for prepared plans
  */
 CREATE FUNCTION manual_subxact_prepared() RETURNS void AS $$
@@ -199,4 +375,7 @@ plpy.execute(rollback)
 $$ LANGUAGE plpythonu;
 SELECT manual_subxact_prepared();
 ERROR:  plpy.SPIError: SPI_execute_plan failed: SPI_ERROR_TRANSACTION
-CONTEXT:  PL/Python function "manual_subxact_prepared"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "manual_subxact_prepared", line 4, in <module>
+    plpy.execute(save)
+PL/Python function "manual_subxact_prepared"
diff --git a/src/pl/plpython/expected/plpython_error_0.out b/src/pl/plpython/expected/plpython_error_0.out
index 1b65d35..ab1566f 100644
--- a/src/pl/plpython/expected/plpython_error_0.out
+++ b/src/pl/plpython/expected/plpython_error_0.out
@@ -36,7 +36,10 @@ ERROR:  spiexceptions.SyntaxError: syntax error at or near "syntax"
 LINE 1: syntax error
         ^
 QUERY:  syntax error
-CONTEXT:  PL/Python function "sql_syntax_error"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "sql_syntax_error", line 1, in <module>
+    plpy.execute("syntax error")
+PL/Python function "sql_syntax_error"
 /* check the handling of uncaught python exceptions
  */
 CREATE FUNCTION exception_index_invalid(text) RETURNS text
@@ -45,7 +48,10 @@ CREATE FUNCTION exception_index_invalid(text) RETURNS text
 	LANGUAGE plpythonu;
 SELECT exception_index_invalid('test');
 ERROR:  IndexError: list index out of range
-CONTEXT:  PL/Python function "exception_index_invalid"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "exception_index_invalid", line 1, in <module>
+    return args[1]
+PL/Python function "exception_index_invalid"
 /* check handling of nested exceptions
  */
 CREATE FUNCTION exception_index_invalid_nested() RETURNS text
@@ -59,7 +65,10 @@ LINE 1: SELECT test5('foo')
                ^
 HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
 QUERY:  SELECT test5('foo')
-CONTEXT:  PL/Python function "exception_index_invalid_nested"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "exception_index_invalid_nested", line 1, in <module>
+    rv = plpy.execute("SELECT test5('foo')")
+PL/Python function "exception_index_invalid_nested"
 /* a typo
  */
 CREATE FUNCTION invalid_type_uncaught(a text) RETURNS text
@@ -75,7 +84,10 @@ return None
 	LANGUAGE plpythonu;
 SELECT invalid_type_uncaught('rick');
 ERROR:  spiexceptions.UndefinedObject: type "test" does not exist
-CONTEXT:  PL/Python function "invalid_type_uncaught"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "invalid_type_uncaught", line 3, in <module>
+    SD["plan"] = plpy.prepare(q, [ "test" ])
+PL/Python function "invalid_type_uncaught"
 /* for what it's worth catch the exception generated by
  * the typo, and return None
  */
@@ -121,7 +133,10 @@ return None
 	LANGUAGE plpythonu;
 SELECT invalid_type_reraised('rick');
 ERROR:  plpy.Error: type "test" does not exist
-CONTEXT:  PL/Python function "invalid_type_reraised"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "invalid_type_reraised", line 6, in <module>
+    plpy.error(str(ex))
+PL/Python function "invalid_type_reraised"
 /* no typo no messing about
  */
 CREATE FUNCTION valid_type(a text) RETURNS text
@@ -140,6 +155,164 @@ SELECT valid_type('rick');
  
 (1 row)
 
+/* error in nested functions to get a traceback
+*/
+CREATE FUNCTION nested_error() RETURNS text
+	AS
+'def fun1():
+	plpy.error("boom")
+
+def fun2():
+	fun1()
+
+def fun3():
+	fun2()
+
+fun3()
+return "not reached"
+'
+	LANGUAGE plpythonu;
+SELECT nested_error();
+ERROR:  plpy.Error: boom
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "nested_error", line 10, in <module>
+    fun3()
+  PL/Python function "nested_error", line 8, in fun3
+    fun2()
+  PL/Python function "nested_error", line 5, in fun2
+    fun1()
+  PL/Python function "nested_error", line 2, in fun1
+    plpy.error("boom")
+PL/Python function "nested_error"
+/* raising plpy.Error is just like calling plpy.error
+*/
+CREATE FUNCTION nested_error_raise() RETURNS text
+	AS
+'def fun1():
+	raise plpy.Error("boom")
+
+def fun2():
+	fun1()
+
+def fun3():
+	fun2()
+
+fun3()
+return "not reached"
+'
+	LANGUAGE plpythonu;
+SELECT nested_error_raise();
+ERROR:  plpy.Error: boom
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "nested_error_raise", line 10, in <module>
+    fun3()
+  PL/Python function "nested_error_raise", line 8, in fun3
+    fun2()
+  PL/Python function "nested_error_raise", line 5, in fun2
+    fun1()
+  PL/Python function "nested_error_raise", line 2, in fun1
+    raise plpy.Error("boom")
+PL/Python function "nested_error_raise"
+/* using plpy.warning should not produce a traceback
+*/
+CREATE FUNCTION nested_warning() RETURNS text
+	AS
+'def fun1():
+	plpy.warning("boom")
+
+def fun2():
+	fun1()
+
+def fun3():
+	fun2()
+
+fun3()
+return "you''ve been warned"
+'
+	LANGUAGE plpythonu;
+SELECT nested_warning();
+WARNING:  boom
+CONTEXT:  PL/Python function "nested_warning"
+   nested_warning   
+--------------------
+ you've been warned
+(1 row)
+
+/* AttirbuteError at toplevel used to give segfaults with the traceback
+*/
+CREATE FUNCTION toplevel_attribute_error() RETURNS void AS
+$$
+plpy.nonexistent
+$$ LANGUAGE plpythonu;
+SELECT toplevel_attribute_error();
+ERROR:  AttributeError: 'module' object has no attribute 'nonexistent'
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "toplevel_attribute_error", line 2, in <module>
+    plpy.nonexistent
+PL/Python function "toplevel_attribute_error"
+/* Calling PL/Python functions from SQL and vice versa should not lose context.
+ */
+CREATE OR REPLACE FUNCTION python_traceback() RETURNS void AS $$
+def first():
+  second()
+
+def second():
+  third()
+
+def third():
+  plpy.execute("select sql_error()")
+
+first()
+$$ LANGUAGE plpythonu;
+CREATE OR REPLACE FUNCTION sql_error() RETURNS void AS $$
+begin
+  select 1/0;
+end
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION python_from_sql_error() RETURNS void AS $$
+begin
+  select python_traceback();
+end
+$$ LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION sql_from_python_error() RETURNS void AS $$
+plpy.execute("select sql_error()")
+$$ LANGUAGE plpythonu;
+SELECT python_traceback();
+ERROR:  spiexceptions.DivisionByZero: division by zero
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "python_traceback", line 11, in <module>
+    first()
+  PL/Python function "python_traceback", line 3, in first
+    second()
+  PL/Python function "python_traceback", line 6, in second
+    third()
+  PL/Python function "python_traceback", line 9, in third
+    plpy.execute("select sql_error()")
+PL/Python function "python_traceback"
+SELECT sql_error();
+ERROR:  division by zero
+CONTEXT:  SQL statement "select 1/0"
+PL/pgSQL function "sql_error" line 3 at SQL statement
+SELECT python_from_sql_error();
+ERROR:  spiexceptions.DivisionByZero: division by zero
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "python_traceback", line 11, in <module>
+    first()
+  PL/Python function "python_traceback", line 3, in first
+    second()
+  PL/Python function "python_traceback", line 6, in second
+    third()
+  PL/Python function "python_traceback", line 9, in third
+    plpy.execute("select sql_error()")
+PL/Python function "python_traceback"
+SQL statement "select python_traceback()"
+PL/pgSQL function "python_from_sql_error" line 3 at SQL statement
+SELECT sql_from_python_error();
+ERROR:  spiexceptions.DivisionByZero: division by zero
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "sql_from_python_error", line 2, in <module>
+    plpy.execute("select sql_error()")
+PL/Python function "sql_from_python_error"
 /* check catching specific types of exceptions
  */
 CREATE TABLE specific (
@@ -187,7 +360,10 @@ plpy.execute("rollback to save")
 $$ LANGUAGE plpythonu;
 SELECT manual_subxact();
 ERROR:  plpy.SPIError: SPI_execute failed: SPI_ERROR_TRANSACTION
-CONTEXT:  PL/Python function "manual_subxact"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "manual_subxact", line 2, in <module>
+    plpy.execute("savepoint save")
+PL/Python function "manual_subxact"
 /* same for prepared plans
  */
 CREATE FUNCTION manual_subxact_prepared() RETURNS void AS $$
@@ -199,4 +375,7 @@ plpy.execute(rollback)
 $$ LANGUAGE plpythonu;
 SELECT manual_subxact_prepared();
 ERROR:  plpy.SPIError: SPI_execute_plan failed: SPI_ERROR_TRANSACTION
-CONTEXT:  PL/Python function "manual_subxact_prepared"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "manual_subxact_prepared", line 4, in <module>
+    plpy.execute(save)
+PL/Python function "manual_subxact_prepared"
diff --git a/src/pl/plpython/expected/plpython_subtransaction.out b/src/pl/plpython/expected/plpython_subtransaction.out
index 50d97fa..515b0bb 100644
--- a/src/pl/plpython/expected/plpython_subtransaction.out
+++ b/src/pl/plpython/expected/plpython_subtransaction.out
@@ -47,7 +47,10 @@ ERROR:  spiexceptions.InvalidTextRepresentation: invalid input syntax for intege
 LINE 1: INSERT INTO subtransaction_tbl VALUES ('oops')
                                                ^
 QUERY:  INSERT INTO subtransaction_tbl VALUES ('oops')
-CONTEXT:  PL/Python function "subtransaction_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_test", line 11, in <module>
+    plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')")
+PL/Python function "subtransaction_test"
 SELECT * FROM subtransaction_tbl;
  i 
 ---
@@ -56,7 +59,10 @@ SELECT * FROM subtransaction_tbl;
 TRUNCATE subtransaction_tbl;
 SELECT subtransaction_test('Python');
 ERROR:  AttributeError: 'module' object has no attribute 'attribute_error'
-CONTEXT:  PL/Python function "subtransaction_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_test", line 13, in <module>
+    plpy.attribute_error
+PL/Python function "subtransaction_test"
 SELECT * FROM subtransaction_tbl;
  i 
 ---
@@ -93,7 +99,10 @@ ERROR:  spiexceptions.InvalidTextRepresentation: invalid input syntax for intege
 LINE 1: INSERT INTO subtransaction_tbl VALUES ('oops')
                                                ^
 QUERY:  INSERT INTO subtransaction_tbl VALUES ('oops')
-CONTEXT:  PL/Python function "subtransaction_ctx_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_ctx_test", line 6, in <module>
+    plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')")
+PL/Python function "subtransaction_ctx_test"
 SELECT * FROM subtransaction_tbl;
  i 
 ---
@@ -102,7 +111,10 @@ SELECT * FROM subtransaction_tbl;
 TRUNCATE subtransaction_tbl;
 SELECT subtransaction_ctx_test('Python');
 ERROR:  AttributeError: 'module' object has no attribute 'attribute_error'
-CONTEXT:  PL/Python function "subtransaction_ctx_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_ctx_test", line 8, in <module>
+    plpy.attribute_error
+PL/Python function "subtransaction_ctx_test"
 SELECT * FROM subtransaction_tbl;
  i 
 ---
@@ -130,7 +142,10 @@ ERROR:  spiexceptions.SyntaxError: syntax error at or near "error"
 LINE 1: error
         ^
 QUERY:  error
-CONTEXT:  PL/Python function "subtransaction_nested_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_nested_test", line 8, in <module>
+    plpy.execute("error")
+PL/Python function "subtransaction_nested_test"
 SELECT * FROM subtransaction_tbl;
  i 
 ---
@@ -230,7 +245,10 @@ with plpy.subtransaction() as s:
 $$ LANGUAGE plpythonu;
 SELECT subtransaction_exit_without_enter();
 ERROR:  ValueError: this subtransaction has not been entered
-CONTEXT:  PL/Python function "subtransaction_exit_without_enter"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_without_enter", line 2, in <module>
+    plpy.subtransaction().__exit__(None, None, None)
+PL/Python function "subtransaction_exit_without_enter"
 SELECT subtransaction_enter_without_exit();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_enter_without_exit"
@@ -243,7 +261,10 @@ SELECT subtransaction_exit_twice();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_exit_twice"
 ERROR:  ValueError: this subtransaction has not been entered
-CONTEXT:  PL/Python function "subtransaction_exit_twice"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_twice", line 3, in <module>
+    plpy.subtransaction().__exit__(None, None, None)
+PL/Python function "subtransaction_exit_twice"
 SELECT subtransaction_enter_twice();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_enter_twice"
@@ -256,18 +277,30 @@ CONTEXT:  PL/Python function "subtransaction_enter_twice"
 
 SELECT subtransaction_exit_same_subtransaction_twice();
 ERROR:  ValueError: this subtransaction has already been exited
-CONTEXT:  PL/Python function "subtransaction_exit_same_subtransaction_twice"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_same_subtransaction_twice", line 5, in <module>
+    s.__exit__(None, None, None)
+PL/Python function "subtransaction_exit_same_subtransaction_twice"
 SELECT subtransaction_enter_same_subtransaction_twice();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_enter_same_subtransaction_twice"
 ERROR:  ValueError: this subtransaction has already been entered
-CONTEXT:  PL/Python function "subtransaction_enter_same_subtransaction_twice"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_enter_same_subtransaction_twice", line 4, in <module>
+    s.__enter__()
+PL/Python function "subtransaction_enter_same_subtransaction_twice"
 SELECT subtransaction_enter_subtransaction_in_with();
 ERROR:  ValueError: this subtransaction has already been entered
-CONTEXT:  PL/Python function "subtransaction_enter_subtransaction_in_with"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_enter_subtransaction_in_with", line 3, in <module>
+    s.__enter__()
+PL/Python function "subtransaction_enter_subtransaction_in_with"
 SELECT subtransaction_exit_subtransaction_in_with();
 ERROR:  ValueError: this subtransaction has already been exited
-CONTEXT:  PL/Python function "subtransaction_exit_subtransaction_in_with"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_subtransaction_in_with", line 3, in <module>
+    s.__exit__(None, None, None)
+PL/Python function "subtransaction_exit_subtransaction_in_with"
 -- Make sure we don't get a "current transaction is aborted" error
 SELECT 1 as test;
  test 
diff --git a/src/pl/plpython/expected/plpython_subtransaction_0.out b/src/pl/plpython/expected/plpython_subtransaction_0.out
index 164e987..4017c41 100644
--- a/src/pl/plpython/expected/plpython_subtransaction_0.out
+++ b/src/pl/plpython/expected/plpython_subtransaction_0.out
@@ -47,7 +47,10 @@ ERROR:  spiexceptions.InvalidTextRepresentation: invalid input syntax for intege
 LINE 1: INSERT INTO subtransaction_tbl VALUES ('oops')
                                                ^
 QUERY:  INSERT INTO subtransaction_tbl VALUES ('oops')
-CONTEXT:  PL/Python function "subtransaction_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_test", line 11, in <module>
+    plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')")
+PL/Python function "subtransaction_test"
 SELECT * FROM subtransaction_tbl;
  i 
 ---
@@ -56,7 +59,10 @@ SELECT * FROM subtransaction_tbl;
 TRUNCATE subtransaction_tbl;
 SELECT subtransaction_test('Python');
 ERROR:  AttributeError: 'module' object has no attribute 'attribute_error'
-CONTEXT:  PL/Python function "subtransaction_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_test", line 13, in <module>
+    plpy.attribute_error
+PL/Python function "subtransaction_test"
 SELECT * FROM subtransaction_tbl;
  i 
 ---
@@ -223,7 +229,10 @@ ERROR:  could not compile PL/Python function "subtransaction_exit_subtransaction
 DETAIL:  SyntaxError: invalid syntax (line 3)
 SELECT subtransaction_exit_without_enter();
 ERROR:  ValueError: this subtransaction has not been entered
-CONTEXT:  PL/Python function "subtransaction_exit_without_enter"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_without_enter", line 2, in <module>
+    plpy.subtransaction().__exit__(None, None, None)
+PL/Python function "subtransaction_exit_without_enter"
 SELECT subtransaction_enter_without_exit();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_enter_without_exit"
@@ -236,7 +245,10 @@ SELECT subtransaction_exit_twice();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_exit_twice"
 ERROR:  ValueError: this subtransaction has not been entered
-CONTEXT:  PL/Python function "subtransaction_exit_twice"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_twice", line 3, in <module>
+    plpy.subtransaction().__exit__(None, None, None)
+PL/Python function "subtransaction_exit_twice"
 SELECT subtransaction_enter_twice();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_enter_twice"
@@ -249,12 +261,18 @@ CONTEXT:  PL/Python function "subtransaction_enter_twice"
 
 SELECT subtransaction_exit_same_subtransaction_twice();
 ERROR:  ValueError: this subtransaction has already been exited
-CONTEXT:  PL/Python function "subtransaction_exit_same_subtransaction_twice"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_same_subtransaction_twice", line 5, in <module>
+    s.__exit__(None, None, None)
+PL/Python function "subtransaction_exit_same_subtransaction_twice"
 SELECT subtransaction_enter_same_subtransaction_twice();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_enter_same_subtransaction_twice"
 ERROR:  ValueError: this subtransaction has already been entered
-CONTEXT:  PL/Python function "subtransaction_enter_same_subtransaction_twice"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_enter_same_subtransaction_twice", line 4, in <module>
+    s.__enter__()
+PL/Python function "subtransaction_enter_same_subtransaction_twice"
 SELECT subtransaction_enter_subtransaction_in_with();
 ERROR:  function subtransaction_enter_subtransaction_in_with() does not exist
 LINE 1: SELECT subtransaction_enter_subtransaction_in_with();
diff --git a/src/pl/plpython/expected/plpython_subtransaction_5.out b/src/pl/plpython/expected/plpython_subtransaction_5.out
index 4e6c067..9216151 100644
--- a/src/pl/plpython/expected/plpython_subtransaction_5.out
+++ b/src/pl/plpython/expected/plpython_subtransaction_5.out
@@ -47,7 +47,10 @@ ERROR:  spiexceptions.InvalidTextRepresentation: invalid input syntax for intege
 LINE 1: INSERT INTO subtransaction_tbl VALUES ('oops')
                                                ^
 QUERY:  INSERT INTO subtransaction_tbl VALUES ('oops')
-CONTEXT:  PL/Python function "subtransaction_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_test", line 11, in <module>
+    plpy.execute("INSERT INTO subtransaction_tbl VALUES ('oops')")
+PL/Python function "subtransaction_test"
 SELECT * FROM subtransaction_tbl;
  i 
 ---
@@ -56,7 +59,10 @@ SELECT * FROM subtransaction_tbl;
 TRUNCATE subtransaction_tbl;
 SELECT subtransaction_test('Python');
 ERROR:  AttributeError: 'module' object has no attribute 'attribute_error'
-CONTEXT:  PL/Python function "subtransaction_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_test", line 13, in <module>
+    plpy.attribute_error
+PL/Python function "subtransaction_test"
 SELECT * FROM subtransaction_tbl;
  i 
 ---
@@ -223,7 +229,10 @@ ERROR:  could not compile PL/Python function "subtransaction_exit_subtransaction
 DETAIL:  SyntaxError: invalid syntax (<string>, line 3)
 SELECT subtransaction_exit_without_enter();
 ERROR:  ValueError: this subtransaction has not been entered
-CONTEXT:  PL/Python function "subtransaction_exit_without_enter"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_without_enter", line 2, in <module>
+    plpy.subtransaction().__exit__(None, None, None)
+PL/Python function "subtransaction_exit_without_enter"
 SELECT subtransaction_enter_without_exit();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_enter_without_exit"
@@ -236,7 +245,10 @@ SELECT subtransaction_exit_twice();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_exit_twice"
 ERROR:  ValueError: this subtransaction has not been entered
-CONTEXT:  PL/Python function "subtransaction_exit_twice"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_twice", line 3, in <module>
+    plpy.subtransaction().__exit__(None, None, None)
+PL/Python function "subtransaction_exit_twice"
 SELECT subtransaction_enter_twice();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_enter_twice"
@@ -249,12 +261,18 @@ CONTEXT:  PL/Python function "subtransaction_enter_twice"
 
 SELECT subtransaction_exit_same_subtransaction_twice();
 ERROR:  ValueError: this subtransaction has already been exited
-CONTEXT:  PL/Python function "subtransaction_exit_same_subtransaction_twice"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_exit_same_subtransaction_twice", line 5, in <module>
+    s.__exit__(None, None, None)
+PL/Python function "subtransaction_exit_same_subtransaction_twice"
 SELECT subtransaction_enter_same_subtransaction_twice();
 WARNING:  forcibly aborting a subtransaction that has not been exited
 CONTEXT:  PL/Python function "subtransaction_enter_same_subtransaction_twice"
 ERROR:  ValueError: this subtransaction has already been entered
-CONTEXT:  PL/Python function "subtransaction_enter_same_subtransaction_twice"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "subtransaction_enter_same_subtransaction_twice", line 4, in <module>
+    s.__enter__()
+PL/Python function "subtransaction_enter_same_subtransaction_twice"
 SELECT subtransaction_enter_subtransaction_in_with();
 ERROR:  function subtransaction_enter_subtransaction_in_with() does not exist
 LINE 1: SELECT subtransaction_enter_subtransaction_in_with();
diff --git a/src/pl/plpython/expected/plpython_test.out b/src/pl/plpython/expected/plpython_test.out
index c2358b4..f2dda66 100644
--- a/src/pl/plpython/expected/plpython_test.out
+++ b/src/pl/plpython/expected/plpython_test.out
@@ -74,4 +74,7 @@ CONTEXT:  PL/Python function "elog_test"
 WARNING:  warning
 CONTEXT:  PL/Python function "elog_test"
 ERROR:  plpy.Error: error
-CONTEXT:  PL/Python function "elog_test"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "elog_test", line 10, in <module>
+    plpy.error('error')
+PL/Python function "elog_test"
diff --git a/src/pl/plpython/expected/plpython_types.out b/src/pl/plpython/expected/plpython_types.out
index d5f2c70..8881613 100644
--- a/src/pl/plpython/expected/plpython_types.out
+++ b/src/pl/plpython/expected/plpython_types.out
@@ -625,7 +625,10 @@ SELECT name, test_composite_table_input(employee.*) FROM employee;
 ALTER TABLE employee DROP bonus;
 SELECT name, test_composite_table_input(employee.*) FROM employee;
 ERROR:  KeyError: 'bonus'
-CONTEXT:  PL/Python function "test_composite_table_input"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "test_composite_table_input", line 2, in <module>
+    return e['basesalary'] + e['bonus']
+PL/Python function "test_composite_table_input"
 ALTER TABLE employee ADD bonus integer;
 UPDATE employee SET bonus = 10;
 SELECT name, test_composite_table_input(employee.*) FROM employee;
diff --git a/src/pl/plpython/expected/plpython_types_3.out b/src/pl/plpython/expected/plpython_types_3.out
index ca81b08..d1ae863 100644
--- a/src/pl/plpython/expected/plpython_types_3.out
+++ b/src/pl/plpython/expected/plpython_types_3.out
@@ -625,7 +625,10 @@ SELECT name, test_composite_table_input(employee.*) FROM employee;
 ALTER TABLE employee DROP bonus;
 SELECT name, test_composite_table_input(employee.*) FROM employee;
 ERROR:  KeyError: 'bonus'
-CONTEXT:  PL/Python function "test_composite_table_input"
+CONTEXT:  Traceback (most recent call last):
+  PL/Python function "test_composite_table_input", line 2, in <module>
+    return e['basesalary'] + e['bonus']
+PL/Python function "test_composite_table_input"
 ALTER TABLE employee ADD bonus integer;
 UPDATE employee SET bonus = 10;
 SELECT name, test_composite_table_input(employee.*) FROM employee;
diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c
index 1f8c27f..f643bc3 100644
--- a/src/pl/plpython/plpython.c
+++ b/src/pl/plpython/plpython.c
@@ -71,6 +71,7 @@ typedef int Py_ssize_t;
  */
 #if PY_MAJOR_VERSION >= 3
 #define PyInt_FromLong(x) PyLong_FromLong(x)
+#define PyInt_AsLong(x) PyLong_AsLong(x)
 #endif
 
 /*
@@ -217,6 +218,7 @@ typedef struct PLyProcedure
 								 * type */
 	bool		is_setof;		/* true, if procedure returns result set */
 	PyObject   *setof;			/* contents of result set. */
+	char		*src;			/* textual procedure code, after mangling */
 	char	  **argnames;		/* Argument names */
 	PLyTypeInfo args[FUNC_MAX_ARGS];
 	int			nargs;
@@ -342,7 +344,7 @@ static void
 PLy_elog(int, const char *,...)
 __attribute__((format(printf, 2, 3)));
 static void PLy_get_spi_error_data(PyObject *exc, char **detail, char **hint, char **query, int *position);
-static char *PLy_traceback(int *);
+static void PLy_traceback(char **, char **, int *);
 
 static void *PLy_malloc(size_t);
 static void *PLy_malloc0(size_t);
@@ -1603,6 +1605,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 	proc->is_setof = procStruct->proretset;
 	proc->setof = NULL;
 	proc->argnames = NULL;
+	proc->src = NULL;
 
 	PG_TRY();
 	{
@@ -1788,6 +1791,8 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 	 * insert the function code into the interpreter
 	 */
 	msrc = PLy_procedure_munge_source(proc->pyname, src);
+	/* Save the mangled source for later inclusion in tracebacks */
+	proc->src = PLy_strdup(msrc);
 	crv = PyRun_String(msrc, Py_file_input, proc->globals, NULL);
 	pfree(msrc);
 
@@ -1885,6 +1890,8 @@ PLy_procedure_delete(PLyProcedure *proc)
 		if (proc->argnames && proc->argnames[i])
 			PLy_free(proc->argnames[i]);
 	}
+	if (proc->src)
+		PLy_free(proc->src);
 	if (proc->argnames)
 		PLy_free(proc->argnames);
 }
@@ -3450,8 +3457,8 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
 			PLy_elog(ERROR, "could not execute plan");
 		sv = PyString_AsString(so);
 		PLy_exception_set_plural(PyExc_TypeError,
-							  "Expected sequence of %d argument, got %d: %s",
-							 "Expected sequence of %d arguments, got %d: %s",
+								 "Expected sequence of %d argument, got %d: %s",
+								 "Expected sequence of %d arguments, got %d: %s",
 								 plan->nargs,
 								 plan->nargs, nargs, sv);
 		Py_DECREF(so);
@@ -4366,19 +4373,21 @@ failure:
  * the current Python error, previously set by PLy_exception_set().
  * This should be used to propagate Python errors into PG.	If fmt is
  * NULL, the Python error becomes the primary error message, otherwise
- * it becomes the detail.
+ * it becomes the detail. If there is a Python traceback, it put in the
+ * context.
  */
 static void
 PLy_elog(int elevel, const char *fmt,...)
 {
-	char	   *xmsg;
-	int			xlevel;
-	StringInfoData emsg;
 	PyObject	*exc, *val, *tb;
 	char		*detail = NULL;
 	char		*hint = NULL;
 	char		*query = NULL;
 	int			position = 0;
+	char		*fmsg = NULL;
+	char		*emsg = NULL;
+	char		*xmsg = NULL;
+	int			tb_depth = 0;
 
 	PyErr_Fetch(&exc, &val, &tb);
 	if (exc != NULL)
@@ -4390,54 +4399,99 @@ PLy_elog(int elevel, const char *fmt,...)
 	}
 	PyErr_Restore(exc, val, tb);
 
-	xmsg = PLy_traceback(&xlevel);
+	/* this is a no-op if there is no current Python exception */
+	PLy_traceback(&emsg, &xmsg, &tb_depth);
 
 	if (fmt)
 	{
-		initStringInfo(&emsg);
+		StringInfoData	si;
+		initStringInfo(&si);
 		for (;;)
 		{
 			va_list		ap;
 			bool		success;
 
 			va_start(ap, fmt);
-			success = appendStringInfoVA(&emsg, dgettext(TEXTDOMAIN, fmt), ap);
+			success = appendStringInfoVA(&si, dgettext(TEXTDOMAIN, fmt), ap);
 			va_end(ap);
 			if (success)
 				break;
-			enlargeStringInfo(&emsg, emsg.maxlen);
+			enlargeStringInfo(&si, si.maxlen);
 		}
+		fmsg = si.data;
 	}
 
 	PG_TRY();
 	{
+		/* If we have a format string, it should be the main error message and
+		 * the emsg + traceback the detailed message.
+		 *
+		 * If we don't have fmsg, we should use emsg as the main error message
+		 * (and failing that just say "no exception data") and put the
+		 * traceback in the detail.
+		 *
+		 * The traceback is present if tb_depth > 0.
+		 */
 		if (fmt)
-			ereport(elevel,
-					(errmsg("%s", emsg.data),
-					 (xmsg) ? errdetail("%s", xmsg) : 0,
-					 (hint) ? errhint("%s", hint) : 0,
-					 (query) ? internalerrquery(query) : 0,
-					 (position) ? internalerrposition(position) : 0));
+		{
+			if (tb_depth > 0)
+			{
+				ereport(elevel,
+						(errmsg("%s", fmsg),
+						 (detail) ? errdetail("%s", detail) : 0,
+						 (emsg) ? errdetail("%s", emsg) : 0,
+						 (xmsg) ? errcontext("%s", xmsg) : 0,
+						 (hint) ? errhint("%s", hint) : 0,
+						 (query) ? internalerrquery(query) : 0,
+						 (position) ? internalerrposition(position) : 0));
+			}
+			else
+			{
+				ereport(elevel,
+						(errmsg("%s", fmsg),
+						 (detail) ? errdetail("%s", detail) : 0,
+						 (emsg) ? errdetail("%s", emsg) : 0,
+						 (hint) ? errhint("%s", hint) : 0,
+						 (query) ? internalerrquery(query) : 0,
+						 (position) ? internalerrposition(position) : 0));
+			}
+		}
 		else
-			ereport(elevel,
-					(errmsg("%s", xmsg),
-					 (detail) ? errdetail("%s", detail) : 0,
-					 (hint) ? errhint("%s", hint) : 0,
-					 (query) ? internalerrquery(query) : 0,
-					 (position) ? internalerrposition(position) : 0));
+		{
+			if (tb_depth > 0)
+			{
+				ereport(elevel,
+						(errmsg("%s", emsg ? emsg : "no exception data"),
+						 (detail) ? errdetail("%s", detail) : 0,
+						 (xmsg) ? errcontext("%s", xmsg) : 0,
+						 (hint) ? errhint("%s", hint) : 0,
+						 (query) ? internalerrquery(query) : 0,
+						 (position) ? internalerrposition(position) : 0));
+			}
+			else
+			{
+				ereport(elevel,
+						(errmsg("%s", emsg ? emsg : "no exception data"),
+						 (detail) ? errdetail("%s", detail) : 0,
+						 (hint) ? errhint("%s", hint) : 0,
+						 (query) ? internalerrquery(query) : 0,
+						 (position) ? internalerrposition(position) : 0));
+			}
+		}
 	}
 	PG_CATCH();
 	{
-		if (fmt)
-			pfree(emsg.data);
+		if (fmsg)
+			pfree(fmsg);
+		if (emsg)
+			pfree(emsg);
 		if (xmsg)
 			pfree(xmsg);
 		PG_RE_THROW();
 	}
 	PG_END_TRY();
 
-	if (fmt)
-		pfree(emsg.data);
+	pfree(emsg);
 	if (xmsg)
 		pfree(xmsg);
 }
@@ -4464,8 +4518,44 @@ cleanup:
 }
 
 
+/* Get the given source line as a palloc'd string */
 static char *
-PLy_traceback(int *xlevel)
+get_source_line(char *src, int lineno)
+{
+	char	*s;
+	char	*next;
+	int		current = 0;
+
+	next = src;
+	while (current != lineno)
+	{
+		s = next;
+		next = strchr(s + 1, '\n');
+		current++;
+		if (next == NULL)
+			break;
+	}
+
+	if (current != lineno)
+		return NULL;
+
+	while (s && isspace(*s))
+		s++;
+
+	if (next == NULL)
+		return pstrdup(s);
+
+	return pnstrdup(s, next - s);
+}
+
+/*
+ * Extract a Python traceback from the current exception.
+ *
+ * The exception error message is returned in emsg, the traceback in xmsg (both
+ * as palloc's strings) and the traceback depth in tb_depth.
+ */
+static void
+PLy_traceback(char **emsg, char **xmsg, int *tb_depth)
 {
 	PyObject   *e,
 			   *v,
@@ -4476,6 +4566,7 @@ PLy_traceback(int *xlevel)
 	char	   *e_module_s = NULL;
 	PyObject   *vob = NULL;
 	char	   *vstr;
+	StringInfoData estr;
 	StringInfoData xstr;
 
 	/*
@@ -4487,13 +4578,11 @@ PLy_traceback(int *xlevel)
 	 * oops, no exception, return
 	 */
 	if (e == NULL)
-	{
-		*xlevel = WARNING;
-		return NULL;
-	}
+		return;
 
 	PyErr_NormalizeException(&e, &v, &tb);
-	Py_XDECREF(tb);
+
+	/* format the exception and its value and put it in emsg */
 
 	e_type_o = PyObject_GetAttrString(e, "__name__");
 	e_module_o = PyObject_GetAttrString(e, "__module__");
@@ -4507,42 +4596,138 @@ PLy_traceback(int *xlevel)
 	else
 		vstr = "unknown";
 
-	initStringInfo(&xstr);
+	initStringInfo(&estr);
 	if (!e_type_s || !e_module_s)
 	{
 		if (PyString_Check(e))
 			/* deprecated string exceptions */
-			appendStringInfoString(&xstr, PyString_AsString(e));
+			appendStringInfoString(&estr, PyString_AsString(e));
 		else
 			/* shouldn't happen */
-			appendStringInfoString(&xstr, "unrecognized exception");
+			appendStringInfoString(&estr, "unrecognized exception");
 	}
 	/* mimics behavior of traceback.format_exception_only */
 	else if (strcmp(e_module_s, "builtins") == 0
 			 || strcmp(e_module_s, "__main__") == 0
 			 || strcmp(e_module_s, "exceptions") == 0)
-		appendStringInfo(&xstr, "%s", e_type_s);
+		appendStringInfo(&estr, "%s", e_type_s);
 	else
-		appendStringInfo(&xstr, "%s.%s", e_module_s, e_type_s);
-	appendStringInfo(&xstr, ": %s", vstr);
+		appendStringInfo(&estr, "%s.%s", e_module_s, e_type_s);
+	appendStringInfo(&estr, ": %s", vstr);
+
+	*emsg = estr.data;
+
+	/* now format the traceback and put it in xmsg */
+	*tb_depth = 0;
+	initStringInfo(&xstr);
+	/* Mimick Python traceback reporting as close as possible */
+	appendStringInfoString(&xstr, "Traceback (most recent call last):");
+	while (tb != NULL && tb != Py_None)
+	{
+		PyObject	*volatile tb_prev = NULL;
+		PyObject	*volatile frame = NULL;
+		PyObject	*volatile code = NULL;
+		PyObject	*volatile name = NULL;
+		PyObject	*volatile lineno = NULL;
+
+		PG_TRY();
+		{
+			lineno = PyObject_GetAttrString(tb, "tb_lineno");
+			if (lineno == NULL)
+				elog(ERROR, "could not get line number from Python traceback");
+
+			frame = PyObject_GetAttrString(tb, "tb_frame");
+			if (frame == NULL)
+				elog(ERROR, "could not get frame from Python traceback");
+
+			code = PyObject_GetAttrString(frame, "f_code");
+			if (code == NULL)
+				elog(ERROR, "could not get code object from Python frame");
+
+			name = PyObject_GetAttrString(code, "co_name");
+			if (name == NULL)
+				elog(ERROR, "could not get function name from Python code object");
+		}
+		PG_CATCH();
+		{
+			Py_XDECREF(frame);
+			Py_XDECREF(code);
+			Py_XDECREF(name);
+			Py_XDECREF(lineno);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		/* The first frame always points at <module>, skip it */
+		if (*tb_depth > 0)
+		{
+			char	*proname;
+			char	*fname;
+			char	*line;
+			long	plain_lineno;
+
+			/*
+			 * The second frame points at the internal function, but to mimick
+			 * Python error reporting we want to say <module>
+			 */
+			if (*tb_depth == 1)
+				fname = "<module>";
+			else
+				fname = PyString_AsString(name);
+
+			proname = PLy_procedure_name(PLy_curr_procedure);
+			plain_lineno = PyInt_AsLong(lineno);
+
+			if (proname == NULL)
+				appendStringInfo(
+					&xstr, "\n  PL/Python anonymous code block, line %ld, in %s",
+					plain_lineno - 1, fname);
+			else
+				appendStringInfo(
+					&xstr, "\n  PL/Python function \"%s\", line %ld, in %s",
+					proname, plain_lineno - 1, fname);
+
+			if (PLy_curr_procedure)
+			{
+				/*
+				 * If we know the current procedure, append the exact line from
+				 * the source, again mimicking Python's traceback.py module
+				 * behaviour. We could store the already line-splitted source
+				 * to avoid splitting it every time, but producing a traceback
+				 * is not the most important scenario to optimise for.
+				 */
+				line = get_source_line(PLy_curr_procedure->src, plain_lineno);
+				if (line != NULL)
+				{
+					appendStringInfo(&xstr, "\n    %s", line);
+					pfree(line);
+				}
+			}
+		}
+
+		Py_DECREF(frame);
+		Py_DECREF(code);
+		Py_DECREF(name);
+		Py_DECREF(lineno);
+
+		/* Release the current frame and go to the next one */
+		tb_prev = tb;
+		tb = PyObject_GetAttrString(tb, "tb_next");
+		Assert(tb_prev != Py_None);
+		Py_DECREF(tb_prev);
+		if (tb == NULL)
+			elog(ERROR, "could not traverse Python traceback");
+		(*tb_depth)++;
+	}
+
+	/* Return the traceback */
+	*xmsg = xstr.data;
 
 	Py_XDECREF(e_type_o);
 	Py_XDECREF(e_module_o);
 	Py_XDECREF(vob);
 	Py_XDECREF(v);
-
-	/*
-	 * intuit an appropriate error level based on the exception type
-	 */
-	if (PLy_exc_error && PyErr_GivenExceptionMatches(e, PLy_exc_error))
-		*xlevel = ERROR;
-	else if (PLy_exc_fatal && PyErr_GivenExceptionMatches(e, PLy_exc_fatal))
-		*xlevel = FATAL;
-	else
-		*xlevel = ERROR;
-
 	Py_DECREF(e);
-	return xstr.data;
 }
 
 /* python module code */
diff --git a/src/pl/plpython/sql/plpython_error.sql b/src/pl/plpython/sql/plpython_error.sql
index 0f456f4..98d950b 100644
--- a/src/pl/plpython/sql/plpython_error.sql
+++ b/src/pl/plpython/sql/plpython_error.sql
@@ -131,6 +131,111 @@ return None
 
 SELECT valid_type('rick');
 
+/* error in nested functions to get a traceback
+*/
+CREATE FUNCTION nested_error() RETURNS text
+	AS
+'def fun1():
+	plpy.error("boom")
+
+def fun2():
+	fun1()
+
+def fun3():
+	fun2()
+
+fun3()
+return "not reached"
+'
+	LANGUAGE plpythonu;
+
+SELECT nested_error();
+
+/* raising plpy.Error is just like calling plpy.error
+*/
+CREATE FUNCTION nested_error_raise() RETURNS text
+	AS
+'def fun1():
+	raise plpy.Error("boom")
+
+def fun2():
+	fun1()
+
+def fun3():
+	fun2()
+
+fun3()
+return "not reached"
+'
+	LANGUAGE plpythonu;
+
+SELECT nested_error_raise();
+
+/* using plpy.warning should not produce a traceback
+*/
+CREATE FUNCTION nested_warning() RETURNS text
+	AS
+'def fun1():
+	plpy.warning("boom")
+
+def fun2():
+	fun1()
+
+def fun3():
+	fun2()
+
+fun3()
+return "you''ve been warned"
+'
+	LANGUAGE plpythonu;
+
+SELECT nested_warning();
+
+/* AttirbuteError at toplevel used to give segfaults with the traceback
+*/
+CREATE FUNCTION toplevel_attribute_error() RETURNS void AS
+$$
+plpy.nonexistent
+$$ LANGUAGE plpythonu;
+
+SELECT toplevel_attribute_error();
+
+/* Calling PL/Python functions from SQL and vice versa should not lose context.
+ */
+CREATE OR REPLACE FUNCTION python_traceback() RETURNS void AS $$
+def first():
+  second()
+
+def second():
+  third()
+
+def third():
+  plpy.execute("select sql_error()")
+
+first()
+$$ LANGUAGE plpythonu;
+
+CREATE OR REPLACE FUNCTION sql_error() RETURNS void AS $$
+begin
+  select 1/0;
+end
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION python_from_sql_error() RETURNS void AS $$
+begin
+  select python_traceback();
+end
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION sql_from_python_error() RETURNS void AS $$
+plpy.execute("select sql_error()")
+$$ LANGUAGE plpythonu;
+
+SELECT python_traceback();
+SELECT sql_error();
+SELECT python_from_sql_error();
+SELECT sql_from_python_error();
+
 /* check catching specific types of exceptions
  */
 CREATE TABLE specific (
-- 
1.7.2.3

>From f7f6fde7a79fffa4c479dc5cfc1e529f9298e935 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Urba=C5=84ski?= <wulc...@wulczer.org>
Date: Mon, 7 Mar 2011 14:14:38 +0100
Subject: [PATCH 2/3] Fix behaviour when raising plpy.Fatal()

It should cause a elog(FATAL) error, and it fact it was simply causing
a elog(ERROR).
---
 src/pl/plpython/plpython.c |    9 +++++++--
 1 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c
index 43d332d..1f8c27f 100644
--- a/src/pl/plpython/plpython.c
+++ b/src/pl/plpython/plpython.c
@@ -4381,8 +4381,13 @@ PLy_elog(int elevel, const char *fmt,...)
 	int			position = 0;
 
 	PyErr_Fetch(&exc, &val, &tb);
-	if (exc != NULL && PyErr_GivenExceptionMatches(val, PLy_exc_spi_error))
-		PLy_get_spi_error_data(val, &detail, &hint, &query, &position);
+	if (exc != NULL)
+	{
+		if (PyErr_GivenExceptionMatches(val, PLy_exc_spi_error))
+			PLy_get_spi_error_data(val, &detail, &hint, &query, &position);
+		else if (PyErr_GivenExceptionMatches(val, PLy_exc_fatal))
+			elevel = FATAL;
+	}
 	PyErr_Restore(exc, val, tb);
 
 	xmsg = PLy_traceback(&xlevel);
-- 
1.7.2.3

>From 823b8c4267b48389a043fd30b26ddc3d9488d784 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Urba=C5=84ski?= <wulc...@wulczer.org>
Date: Mon, 7 Mar 2011 14:09:30 +0100
Subject: [PATCH 1/3] Report Python errors from iterators with PLy_elog.

This improves reporting, as the error string now includes the actual
Python exception. As a side effect, this no longer sets the errcode to
ERRCODE_DATA_EXCEPTION, which might be considered a feature, as it's
not documented and not clear why iterator errors should be treated
differently.
---
 src/pl/plpython/plpython.c |    4 +---
 1 files changed, 1 insertions(+), 3 deletions(-)

diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c
index 4a9e2a4..43d332d 100644
--- a/src/pl/plpython/plpython.c
+++ b/src/pl/plpython/plpython.c
@@ -1157,9 +1157,7 @@ PLy_function_handler(FunctionCallInfo fcinfo, PLyProcedure *proc)
 				PLy_function_delete_args(proc);
 
 				if (has_error)
-					ereport(ERROR,
-							(errcode(ERRCODE_DATA_EXCEPTION),
-						  errmsg("error fetching next item from iterator")));
+					PLy_elog(ERROR, "error fetching next item from iterator");
 
 				/* Disconnect from the SPI manager before returning */
 				if (SPI_finish() != SPI_OK_FINISH)
-- 
1.7.2.3

-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to