diff --git a/doc/src/sgml/pltcl.sgml b/doc/src/sgml/pltcl.sgml
index d2175d5..4cf32df 100644
--- a/doc/src/sgml/pltcl.sgml
+++ b/doc/src/sgml/pltcl.sgml
@@ -775,6 +775,169 @@ CREATE EVENT TRIGGER tcl_a_snitch ON ddl_command_start EXECUTE PROCEDURE tclsnit
+
+ Error Handling in PL/Tcl
+
+
+ error handling
+ in PL/Tcl
+
+
+
+ All Tcl errors that occur within a stored procedure and are not caught
+ using Tcl's catch or try
+ functions will raise a database error.
+
+
+ Tcl code can raise a database error by invoking the
+ elog command provided by PL/Tcl or by generating an
+ error using the Tcl error command and not catching it
+ with Tcl's catch command.
+
+
+ Database errors that occur from the PL/Tcl stored procedure's
+ use of spi_exec, spi_prepare,
+ and spi_execp are also catchable by Tcl's
+ catch command.
+
+
+ Tcl provides an errorCode variable that can represent
+ additional information about the error in a form that is easy for programs
+ to interpret. The contents are a Tcl list format. The first word
+ identifies the subsystem or library responsible for the error. The
+ remaining contents are up to the individual code or library. For example
+ if Tcl's open command is asked to open a file that
+ doesn't exist, errorCode might contain POSIX
+ ENOENT {no such file or directory} where the third element may
+ vary by locale but the first and second will not.
+
+
+ When spi_exec, spi_prepare
+ or spi_execp cause a database error to be raised,
+ that database eror propagates back to Tcl as a Tcl error. In this case
+ errorCode is set to a list where the first element is
+ POSTGRES followed by details of the Postgres error.
+ Since fields in the structure may or may not be present depending on the
+ nature of the error, how the function was invoked, etc, PL/Tcl has adopted
+ the convention that subsequent elements of the
+ errorCode list are key-value pairs where the first
+ value is the name of the field and the second is its value.
+
+
+ Fields that may be present include SQLSTATE,
+ message,
+ detail,
+ hint,
+ context,
+ schema,
+ table,
+ column,
+ datatype,
+ constraint,
+ cursor_position,
+ internalquery,
+ internal_position,
+ filename,
+ lineno and
+ funcname.
+
+
+ You might find it useful to load the results into an array. Code
+ for doing that might look like
+
+if {[lindex $errorCode 0] == "POSTGRES"} {
+ array set errorRow [lrange $errorCode 1 end]
+}
+
+
+
+ This example shows how to trap a specific SQL error.
+
+CREATE TABLE account(user_name varchar(1) NOT NULL PRIMARY KEY);
+CREATE OR REPLACE FUNCTION public.create_user(user_name text)
+ RETURNS void LANGUAGE pltcl AS $function$
+set prep [ spi_prepare "INSERT INTO account(user_name) VALUES(\$1)" [ list text ] ]
+if [ catch {
+ spi_execp $prep [ list $1 ]
+ } msg ] {
+ if {[lindex $::errorCode 0] == "POSTGRES"} {
+ array set errorData [lrange $::errorCode 1 end]
+ if { $errorData(SQLSTATE) == "23505" && $errorData(constraint) == "account_pkey" } {
+ return -code error "user '$1' already exists"
+ }
+ }
+ throw $::errorCode $msg
+}
+$function$;
+
+
+
+SELECT create_user('a');
+ create_user
+-------------
+
+(1 row)
+
+SELECT create_user('a');
+ERROR: user 'a' already exists
+CONTEXT: user 'a' already exists
+ while executing
+"__PLTcl_proc_16388 a"
+in PL/Tcl function "create_user"
+
+
+
+
+ This is an example of inspecting the raw errorCode data. (The
+ double-colons explicitly reference errorCode as a
+ global variable.)
+
+CREATE FUNCTION get_error_code() RETURNS text LANGUAGE pltcl AS $$
+join $::errorCode "\n"
+$$;
+
+
+
+SELECT create_user('aa');
+ERROR: value too long for type character varying(1)
+CONTEXT: value too long for type character varying(1)
+ while executing
+"throw $::errorCode $msg"
+ invoked from within
+"if [ catch {
+ spi_execp $prep [ list $1 ]
+ } msg ] {
+ if {[lindex $::errorCode 0] == "POSTGRES"} {
+ array set errorData [lrange $:..."
+ (procedure "__PLTcl_proc_16388" line 4)
+ invoked from within
+"__PLTcl_proc_16388 aa"
+in PL/Tcl function "create_user"
+
+SELECT get_error_code();
+ get_error_code
+-----------------------------------------------------------
+ POSTGRES +
+ SQLSTATE +
+ 22001 +
+ message +
+ value too long for type character varying(1) +
+ context +
+ SQL statement "INSERT INTO account(user_name) VALUES($1)"+
+ cursor_position +
+ 0 +
+ filename +
+ varchar.c +
+ lineno +
+ 624 +
+ funcname +
+ varchar
+(1 row)
+
+
+
+
+
Modules and the unknown> Command
diff --git a/src/pl/tcl/expected/pltcl_setup.out b/src/pl/tcl/expected/pltcl_setup.out
index e11718c..9a9d7d1 100644
--- a/src/pl/tcl/expected/pltcl_setup.out
+++ b/src/pl/tcl/expected/pltcl_setup.out
@@ -555,3 +555,47 @@ NOTICE: tclsnitch: ddl_command_start DROP TABLE
NOTICE: tclsnitch: ddl_command_end DROP TABLE
drop event trigger tcl_a_snitch;
drop event trigger tcl_b_snitch;
+-- test error handling
+/*
+ * The ugly hack of messsing with the verbosity is because the error context is
+ * set to the TCL variable errorInfo, which contains some unstable data (namely
+ * the full name of the TCL function created by the handler, which includes the
+ * Postgres backend PID).
+ */
+\set VERBOSITY terse
+CREATE OR REPLACE FUNCTION pg_temp.tcl_eval (varchar) RETURNS varchar AS $$
+eval $1
+$$ LANGUAGE pltcl;
+select pg_temp.tcl_eval('spi_exec "select * from foo;"');
+ERROR: relation "foo" does not exist
+select pg_temp.tcl_eval($$
+set list [lindex $::errorCode 0];
+foreach "key value" [lrange $::errorCode 1 end] {
+ if {$key == "domain" || $key == "context_domain" || $key == "lineno"} {
+ regsub -all {[0-9]} $value "" value
+ }
+ lappend list $key $value
+};
+return [join $list "\n"]
+$$);
+ tcl_eval
+-------------------------------
+ POSTGRES +
+ SQLSTATE +
+ 42P01 +
+ message +
+ relation "foo" does not exist+
+ cursor_position +
+ 0 +
+ internalquery +
+ select * from foo; +
+ internal_position +
+ 15 +
+ filename +
+ parse_relation.c +
+ lineno +
+ +
+ funcname +
+ parserOpenTable
+(1 row)
+
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index 5b27c73..1c7678f 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -1576,6 +1576,85 @@ compile_pltcl_function(Oid fn_oid, Oid tgreloid,
return prodesc;
}
+/**********************************************************************
+ * pltcl_construct_errorCode() - construct a Tcl errorCode
+ * list with detailed information from the PostgreSQL server
+ **********************************************************************/
+static void
+pltcl_construct_errorCode(Tcl_Interp *interp, ErrorData *edata)
+{
+ Tcl_Obj *obj = Tcl_NewObj();
+
+ UTF_BEGIN;
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("POSTGRES", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("SQLSTATE", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(unpack_sql_state(edata->sqlerrcode), -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("message", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->message), -1));
+
+ if (edata->detail)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("detail", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->detail), -1));
+ }
+ if (edata->hint)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("hint", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->hint), -1));
+ }
+ if (edata->context)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("context", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->context), -1));
+ }
+ if (edata->schema_name)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("schema", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->schema_name), -1));
+ }
+ if (edata->table_name)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("table", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->table_name), -1));
+ }
+ if (edata->column_name)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("column", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->column_name), -1));
+ }
+ if (edata->datatype_name)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("datatype", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->datatype_name), -1));
+ }
+ if (edata->constraint_name)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("constraint", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->constraint_name), -1));
+ }
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("cursor_position", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewIntObj(edata->cursorpos));
+ if (edata->internalquery)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("internalquery", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->internalquery), -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("internal_position", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewIntObj(edata->internalpos));
+ }
+ if (edata->filename)
+ {
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("filename", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->filename), -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("lineno", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewIntObj(edata->lineno));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("funcname", -1));
+ Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(UTF_E2U(edata->funcname), -1));
+ }
+ UTF_END;
+
+ Tcl_SetObjErrorCode(interp, obj);
+}
+
/**********************************************************************
* pltcl_elog() - elog() support for PLTcl
@@ -1652,6 +1731,7 @@ pltcl_elog(ClientData cdata, Tcl_Interp *interp,
UTF_BEGIN;
Tcl_SetObjResult(interp, Tcl_NewStringObj(UTF_E2U(edata->message), -1));
UTF_END;
+ pltcl_construct_errorCode(interp, edata);
FreeErrorData(edata);
return TCL_ERROR;
@@ -1884,6 +1964,7 @@ pltcl_subtrans_abort(Tcl_Interp *interp,
UTF_BEGIN;
Tcl_SetResult(interp, UTF_E2U(edata->message), TCL_VOLATILE);
UTF_END;
+ pltcl_construct_errorCode(interp, edata);
FreeErrorData(edata);
}
diff --git a/src/pl/tcl/sql/pltcl_setup.sql b/src/pl/tcl/sql/pltcl_setup.sql
index 53358ea..3ee8583 100644
--- a/src/pl/tcl/sql/pltcl_setup.sql
+++ b/src/pl/tcl/sql/pltcl_setup.sql
@@ -595,3 +595,29 @@ drop table foo;
drop event trigger tcl_a_snitch;
drop event trigger tcl_b_snitch;
+
+
+-- test error handling
+
+/*
+ * The ugly hack of messsing with the verbosity is because the error context is
+ * set to the TCL variable errorInfo, which contains some unstable data (namely
+ * the full name of the TCL function created by the handler, which includes the
+ * Postgres backend PID).
+ */
+\set VERBOSITY terse
+CREATE OR REPLACE FUNCTION pg_temp.tcl_eval (varchar) RETURNS varchar AS $$
+eval $1
+$$ LANGUAGE pltcl;
+
+select pg_temp.tcl_eval('spi_exec "select * from foo;"');
+select pg_temp.tcl_eval($$
+set list [lindex $::errorCode 0];
+foreach "key value" [lrange $::errorCode 1 end] {
+ if {$key == "domain" || $key == "context_domain" || $key == "lineno"} {
+ regsub -all {[0-9]} $value "" value
+ }
+ lappend list $key $value
+};
+return [join $list "\n"]
+$$);