Hi

2016-03-13 20:24 GMT+01:00 Jim Nasby <jim.na...@bluetreble.com>:

> On 3/3/16 8:51 AM, Pavel Stehule wrote:
>
>> Hi
>>
>> I am testing behave, and some results looks strange
>>
>
> Thanks for the review!
>
> postgres=# \sf foo
>> CREATE OR REPLACE FUNCTION public.foo()
>>   RETURNS void
>>   LANGUAGE plpgsql
>> AS $function$
>> begin
>>    raise exception sqlstate 'ZZ666' using message='hello, world',
>> detail='hello, my world', hint = 'dont afraid';
>> end
>> $function$
>>
>> postgres=# select tcl_eval('spi_exec "select foo();"');
>> ERROR:  38000: hello, world
>> CONTEXT:  hello, world   <<<<==========???????
>>
>> the message was in context. Probably it is out of scope of this patch,
>> but it isn't consistent with other PL
>>
>>
>>      while executing
>> "spi_exec "select foo();""
>>      ("eval" body line 1)
>>      invoked from within
>> "eval $1"
>>      (procedure "__PLTcl_proc_16864" line 3)
>>      invoked from within
>> "__PLTcl_proc_16864 {spi_exec "select foo();"}"
>> in PL/Tcl function "tcl_eval"
>> LOCATION:  throw_tcl_error, pltcl.c:1217
>> Time: 1.178 ms
>>
>
> Both problems actually exists in HEAD. The issue is this line in
> throw_tcl_error:
>
>         econtext = utf_u2e(Tcl_GetVar(interp, "errorInfo",
> TCL_GLOBAL_ONLY));
>
> Offhand I don't see any great way to improve that behavior, and in any
> case it seems out of scope for this patch. As a workaround I'm just forcing
> psql error VERBOSITY to terse for now.
>

I understand - it is unpleasant, but it is another scope.


>
> postgres=# select tcl_eval('join $::errorCode "\n"');
>>                  tcl_eval
>> ═════════════════════════════════════════
>>   POSTGRES                               ↵
>>   message                                ↵
>>   hello, world                           ↵
>>   detail                                 ↵
>>   hello, my world                        ↵
>>   hint                                   ↵
>>   dont afraid                            ↵
>>   domain                                 ↵
>>   plpgsql-9.6                            ↵
>>   context_domain                         ↵
>>   postgres-9.6                           ↵
>>   context                                ↵
>>   PL/pgSQL function foo() line 3 at RAISE↵
>>   SQL statement "select foo();"          ↵
>>   cursor_position                        ↵
>>   0                                      ↵
>>   filename                               ↵
>>   pl_exec.c                              ↵
>>   lineno                                 ↵
>>   3165                                   ↵
>>   funcname                               ↵
>>   exec_stmt_raise
>> (1 row)
>>
>> I miss a SQLSTATE.
>>
>
> Great catch. Fixed.
>

I can verify it. The doc should be updated too.


>
> Why is used List object instead dictionary? TCL supports it
>> https://www.tcl.tk/man/tcl8.5/tutorial/Tcl23a.html
>>
>
> Because errorCode unfortunately is an array and not a dict. It doesn't
> really seem worth messing with it in the eval since this is just a sanity
> check...
>

I am sorry, my I expected so we introduced errorCode. My question was not
valid


> New patch attached. It also removes some other unstable output from the
> regression test.
>

I checked this patch:

* This patch is relative trivial without any controversy - allow a access
to ErrorData fields is good idea, and we do it in some other PL longer time.
* There are no problem with patching, compiling
* all tests was passed
* a comments in code are adequate to low complexity
* code respects PostgreSQL formatting
* attached documentation is good and correct
* regress tests are adequate

I fixed the documentation - there was not information about SQLSTATE field.
See, please, attachment.

I'll mark this patch as ready for commiters.

Regards

Pavel




> --
> Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX
> Experts in Analytics, Data Architecture and PostgreSQL
> Data in Trouble? Get it in Treble! http://BlueTreble.com
>
diff --git a/doc/src/sgml/pltcl.sgml b/doc/src/sgml/pltcl.sgml
new file mode 100644
index d2175d5..1ac7804
*** a/doc/src/sgml/pltcl.sgml
--- b/doc/src/sgml/pltcl.sgml
*************** CREATE EVENT TRIGGER tcl_a_snitch ON ddl
*** 775,780 ****
--- 775,903 ----
      </para>
     </sect1>
  
+    <sect1 id="pltcl-error-handling">
+     <title>Error Handling in PL/Tcl</title>
+ 
+     <indexterm>
+      <primary>error handling</primary>
+      <secondary>in PL/Tcl</secondary>
+     </indexterm>
+ 
+     <para>
+      All Tcl errors that are allowed to propagate back to the top level of the
+      interpreter, that is, errors not caught within the stored procedure
+      using the Tcl <function>catch</function> command will raise a database
+      error.
+     </para>
+     <para>
+      Tcl code within or called from the stored procedure can choose to
+      raise a database error by invoking the <function>elog</function>
+      command provided by PL/Tcl or by generating an error using the Tcl
+      <function>error</function> command and not catching it with Tcl's
+      <function>catch</function> command.
+     </para>
+     <para>
+      Database errors that occur from the PL/Tcl stored procedure's
+      use of <function>spi_exec</function>, <function>spi_prepare</function>,
+      and <function>spi_execp</function> are also catchable by Tcl's
+      <function>catch</function> command.
+     </para>
+     <para>
+      Tcl provides an <varname>errorCode</varname> variable that can
+      represent additional information about the error in a form that
+      is easy for programs to interpret.  The contents are in Tcl list
+      format and the first word identifies the subsystem or
+      library responsible for the error and beyond that the contents are left
+      to the individual code or library.  For example if Tcl's
+      <function>open</function> command is asked to open a file that doesn't
+      exist, <varname>errorCode</varname>
+      might contain <literal>POSIX ENOENT {no such file or directory}</literal>
+      where the third element may vary by locale but the first and second
+      will not.
+     </para>
+     <para>
+      When <function>spi_exec</function>, <function>spi_prepare</function>
+      or <function>spi_execp</function> cause a database error to be raised,
+      that database eror propagates back to Tcl as a Tcl error.
+      In this case <varname>errorCode</varname> is set to a list
+      where the first element is <literal>POSTGRES</literal> followed by a
+      copious decoding of the Postgres error structure.  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 <varname>errorCode</varname>
+      list are key-value pairs where the first value is the name of the
+      field and the second is its value.
+     </para>
+     <para>
+      Fields that may be present include <varname>SQLSTATE</varname>,
+      <varname>message</varname>, <varname>detail</varname>,
+      <varname>detail_log</varname>, <varname>hint</varname>,
+      <varname>domain</varname>, <varname>context_domain</varname>,
+      <varname>context</varname>, <varname>schema</varname>,
+      <varname>table</varname>, <varname>column</varname>,
+      <varname>datatype</varname>, <varname>constraint</varname>,
+      <varname>cursor_position</varname>, <varname>internalquery</varname>,
+      <varname>internal_position</varname>, <varname>filename</varname>,
+      <varname>lineno</varname> and <varname>funcname</varname>.
+     </para>
+     <para>
+      You might find it useful to load the results into an array. Code
+      for doing that might look like
+ <programlisting>
+     if {[lindex $errorCode 0] == "POSTGRES"} {
+         array set errorRow [lrange $errorCode 1 end]
+     }
+ </programlisting>
+     </para>
+     <para>
+      In the example below we cause an error by attempting to
+      <command>SELECT</> from a table that doesn't exist.
+ <screen>
+ select tcl_eval('spi_exec "select * from foo;"');
+ </screen>
+ <screen>
+ <computeroutput>
+ ERROR:  relation "foo" does not exist
+ </computeroutput>
+ </screen>
+     </para>
+     <para>
+      Now we examine the error code.  (The double-colons explicitly
+      reference <varname>errorCode</varname> as a global variable.)
+ <screen>
+ select tcl_eval('join $::errorCode "\n"');
+ </screen>
+ <screen>
+ <computeroutput>
+            tcl_eval            
+ -------------------------------
+  POSTGRES                     +
+  SQLSTATE                     +
+  42P01                        +
+  message                      +
+  relation "foo" does not exist+
+  domain                       +
+  postgres-9.6                 +
+  context_domain               +
+  postgres-9.6                 +
+  cursorpos                    +
+  0                            +
+  internalquery                +
+  select * from foo;           +
+  internalpos                  +
+  15                           +
+  filename                     +
+  parse_relation.c             +
+  lineno                       +
+  1159                         +
+  funcname                     +
+  parserOpenTable
+ (1 row)
+ </computeroutput>
+ </screen>
+     </para>
+    </sect1>
+ 
     <sect1 id="pltcl-unknown">
         <title>Modules and the <function>unknown</> Command</title>
         <para>
diff --git a/src/pl/tcl/expected/pltcl_setup.out b/src/pl/tcl/expected/pltcl_setup.out
new file mode 100644
index e11718c..0592c48
*** a/src/pl/tcl/expected/pltcl_setup.out
--- b/src/pl/tcl/expected/pltcl_setup.out
*************** NOTICE:  tclsnitch: ddl_command_start DR
*** 555,557 ****
--- 555,605 ----
  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+
+  domain                       +
+  postgres-.                   +
+  context_domain               +
+  postgres-.                   +
+  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
new file mode 100644
index 5b27c73..d8a8a89
*** a/src/pl/tcl/pltcl.c
--- b/src/pl/tcl/pltcl.c
*************** compile_pltcl_function(Oid fn_oid, Oid t
*** 1576,1581 ****
--- 1576,1673 ----
  	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();
+ 
+ 	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(edata->message, -1));
+ 
+ 	if (edata->detail)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("detail", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->detail, -1));
+ 	}
+ 	if (edata->detail_log)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("detail_log", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->detail_log, -1));
+ 	}
+ 	if (edata->hint)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("hint", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->hint, -1));
+ 	}
+ 	if (edata->domain)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("domain", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->domain, -1));
+ 	}
+ 	if (edata->context_domain)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("context_domain", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->context_domain, -1));
+ 	}
+ 	if (edata->context)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("context", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->context, -1));
+ 	}
+ 	if (edata->schema_name)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("schema", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->schema_name, -1));
+ 	}
+ 	if (edata->table_name)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("table", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->table_name, -1));
+ 	}
+ 	if (edata->column_name)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("column", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->column_name, -1));
+ 	}
+ 	if (edata->datatype_name)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("datatype", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(edata->datatype_name, -1));
+ 	}
+ 	if (edata->constraint_name)
+ 	{
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj("constraint", -1));
+ 		Tcl_ListObjAppendElement(interp, obj, Tcl_NewStringObj(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(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(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(edata->funcname, -1));
+ 	}
+ 
+ 	Tcl_SetObjErrorCode(interp, obj);
+ }
+ 
  
  /**********************************************************************
   * pltcl_elog()		- elog() support for PLTcl
*************** pltcl_elog(ClientData cdata, Tcl_Interp
*** 1610,1615 ****
--- 1702,1709 ----
  
  	level = loglevels[priIndex];
  
+ 	level = loglevels[priIndex];
+ 
  	if (level == ERROR)
  	{
  		/*
*************** pltcl_elog(ClientData cdata, Tcl_Interp
*** 1652,1657 ****
--- 1746,1752 ----
  		UTF_BEGIN;
  		Tcl_SetObjResult(interp, Tcl_NewStringObj(UTF_E2U(edata->message), -1));
  		UTF_END;
+ 		pltcl_construct_errorCode(interp, edata);
  		FreeErrorData(edata);
  
  		return TCL_ERROR;
*************** pltcl_subtrans_abort(Tcl_Interp *interp,
*** 1884,1889 ****
--- 1979,1985 ----
  	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
new file mode 100644
index 53358ea..3ee8583
*** a/src/pl/tcl/sql/pltcl_setup.sql
--- b/src/pl/tcl/sql/pltcl_setup.sql
*************** drop table foo;
*** 595,597 ****
--- 595,623 ----
  
  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"]
+ $$);
-- 
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