Here's a patch implementing custom Python exceptions for SPI errors
mentioned in
http://archives.postgresql.org/pgsql-hackers/2010-12/msg01991.php. It's
an incremental patch on top of the explicit-subxacts patch sent eariler.

Git branch for this patch:
https://github.com/wulczer/postgres/tree/custom-spi-exceptions.

What the patch does is provide a Python exception per each error defined
in utils/errcodes.h, and then raise the corresponding exception when a
SPI call fails. The parsing of errcodes.h in the Makefile is a little
grotty and would probably have to be ported to the Windows build system,
which I have no idea about.

With this patch you can do:

from plpy import spiexceptions

try:
    plpy.execute("insert into t values (4)")
catch spiexceptions.UniqueViolation:
    plpy.notice("unique constraint violation")
catch spiexceptions.NotNullViolation:
    plpy.notice("not null constraint violation")

All exceptions inherint from plpy.SPIError, so code thta just catches a
blanket SPIErorr will continue to work.

The patch lacks user-facing docs, if it gets accepted I'll write some.
Not sure if we should provide a table similar to
http://www.postgresql.org/docs/current/static/errcodes-appendix.html, or
just refer to that table and state that the rule is changing underscores
to camel case...

Also, I just realised that this patch does not really depend on the
explicit-subxacts change, but rather only on the spi-in-subxacts, so if
need be I can generate it as an incremental changeset ofer the latter
and not the former.

Cheers,
Jan
diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile
index 33dddc6..dd5b445 100644
*** a/src/pl/plpython/Makefile
--- b/src/pl/plpython/Makefile
*************** rpathdir = $(python_libdir)
*** 38,44 ****
  
  NAME = plpython$(python_majorversion)
  OBJS = plpython.o
! 
  
  # Python on win32 ships with import libraries only for Microsoft Visual C++,
  # which are not compatible with mingw gcc. Therefore we need to build a
--- 38,44 ----
  
  NAME = plpython$(python_majorversion)
  OBJS = plpython.o
! SPIEXCEPTIONS = spiexceptions.h
  
  # Python on win32 ships with import libraries only for Microsoft Visual C++,
  # which are not compatible with mingw gcc. Therefore we need to build a
*************** PSQLDIR = $(bindir)
*** 86,93 ****
  
  include $(top_srcdir)/src/Makefile.shlib
  
  
! all: all-lib
  
  install: all installdirs install-lib
  ifeq ($(python_majorversion),2)
--- 86,113 ----
  
  include $(top_srcdir)/src/Makefile.shlib
  
+ # A quite horrendous sed, but does the job. The steps are, in order:
+ # 1. Remove everything up to the line with Class 03. We only generate
+ #    exceptions for errors, not for warnings or notices
+ # 2. Remove lines that don't define an error code
+ # 3. Change ERRCODE_XXX into { "spiexceptions.ERRCODE_XY_Z, "XY_Z", ERRCODE_XY_Z },
+ # 4. Leave an uppercase letter after a dot or a quote, change the rest
+ #    into lowercase thus giving us
+ #    { "spiexceptions.Errcode_xy_z, "Xy_z", ERRCODE_XY_Z },
+ # 5. change lowercase letters after an underscore into uppercase, giving us
+ #    { "spiexceptions.ErrcodeXyZ, "XyZ", ERRCODE_XY_Z },
+ gen-spiexceptions:
+ 	echo "/* autogenerated from utils/errcodes.h, do not edit */" > $(SPIEXCEPTIONS)
+ 	sed -e '1,/Class 03/ d' \
+             -e '/^#define ERRCODE_.*MAKE_SQLSTATE/! d' \
+             -e 's|#define ERRCODE_\([^\t ]*\).*|{ "spiexceptions.\1", "\1", ERRCODE_\1 },|' \
+             -e 's|\(["\.]\)\([A-Z]\)\([^"]*\)|\1\2\L\3|g' \
+             -e 's|_\([a-z]\)|\u\1|g' \
+ 	$(top_srcdir)/src/include/utils/errcodes.h >> $(SPIEXCEPTIONS)
  
! .PHONY: gen-spiexceptions
! 
! all: gen-spiexceptions all-lib
  
  install: all installdirs install-lib
  ifeq ($(python_majorversion),2)
*************** clean distclean maintainer-clean: clean-
*** 138,143 ****
--- 158,164 ----
  	rm -f $(OBJS)
  	rm -rf results
  	rm -f regression.diffs regression.out
+ 	rm -f $(SPIEXCEPTIONS)
  ifeq ($(PORTNAME), win32)
  	rm -f python${pytverstr}.def
  endif
diff --git a/src/pl/plpython/expected/plpython_error.out b/src/pl/plpython/expected/plpython_error.out
index 7fc8337..718ebce 100644
*** a/src/pl/plpython/expected/plpython_error.out
--- b/src/pl/plpython/expected/plpython_error.out
*************** CREATE FUNCTION sql_syntax_error() RETUR
*** 8,14 ****
  'plpy.execute("syntax error")'
          LANGUAGE plpythonu;
  SELECT sql_syntax_error();
! ERROR:  plpy.SPIError: syntax error at or near "syntax"
  CONTEXT:  PL/Python function "sql_syntax_error"
  /* check the handling of uncaught python exceptions
   */
--- 8,14 ----
  'plpy.execute("syntax error")'
          LANGUAGE plpythonu;
  SELECT sql_syntax_error();
! ERROR:  spiexceptions.SyntaxError: syntax error at or near "syntax"
  CONTEXT:  PL/Python function "sql_syntax_error"
  /* check the handling of uncaught python exceptions
   */
*************** CREATE FUNCTION exception_index_invalid_
*** 27,33 ****
  return rv[0]'
  	LANGUAGE plpythonu;
  SELECT exception_index_invalid_nested();
! ERROR:  plpy.SPIError: function test5(unknown) does not exist
  CONTEXT:  PL/Python function "exception_index_invalid_nested"
  /* a typo
   */
--- 27,33 ----
  return rv[0]'
  	LANGUAGE plpythonu;
  SELECT exception_index_invalid_nested();
! ERROR:  spiexceptions.UndefinedFunction: function test5(unknown) does not exist
  CONTEXT:  PL/Python function "exception_index_invalid_nested"
  /* a typo
   */
*************** return None
*** 43,49 ****
  '
  	LANGUAGE plpythonu;
  SELECT invalid_type_uncaught('rick');
! ERROR:  plpy.SPIError: type "test" does not exist
  CONTEXT:  PL/Python function "invalid_type_uncaught"
  /* for what it's worth catch the exception generated by
   * the typo, and return None
--- 43,49 ----
  '
  	LANGUAGE plpythonu;
  SELECT invalid_type_uncaught('rick');
! ERROR:  spiexceptions.UndefinedObject: type "test" does not exist
  CONTEXT:  PL/Python function "invalid_type_uncaught"
  /* for what it's worth catch the exception generated by
   * the typo, and return None
*************** SELECT valid_type('rick');
*** 109,111 ****
--- 109,149 ----
   
  (1 row)
  
+ /* Check catching specific types of exceptions
+  */
+ CREATE TABLE specific (
+     i integer PRIMARY KEY
+ );
+ NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "specific_pkey" for table "specific"
+ CREATE FUNCTION specific_exception(i integer) RETURNS void AS
+ $$
+ from plpy import spiexceptions
+ try:
+     plpy.execute("insert into specific values (%s)" % (i or "NULL"));
+ except spiexceptions.NotNullViolation, e:
+     plpy.notice("Violated the NOT NULL constraint, sqlstate %s" % e.sqlstate)
+ except spiexceptions.UniqueViolation, e:
+     plpy.notice("Violated the UNIQUE constraint, sqlstate %s" % e.sqlstate)
+ $$ LANGUAGE plpythonu;
+ SELECT specific_exception(2);
+  specific_exception 
+ --------------------
+  
+ (1 row)
+ 
+ SELECT specific_exception(NULL);
+ NOTICE:  Violated the NOT NULL constraint, sqlstate 23502
+ CONTEXT:  PL/Python function "specific_exception"
+  specific_exception 
+ --------------------
+  
+ (1 row)
+ 
+ SELECT specific_exception(2);
+ NOTICE:  Violated the UNIQUE constraint, sqlstate 23505
+ CONTEXT:  PL/Python function "specific_exception"
+  specific_exception 
+ --------------------
+  
+ (1 row)
+ 
diff --git a/src/pl/plpython/expected/plpython_subxact.out b/src/pl/plpython/expected/plpython_subxact.out
index 25a5a4b..fd7e9d9 100644
*** a/src/pl/plpython/expected/plpython_subxact.out
--- b/src/pl/plpython/expected/plpython_subxact.out
*************** SELECT * FROM subxact_tbl;
*** 43,49 ****
  
  TRUNCATE subxact_tbl;
  SELECT subxact_test('SPI');
! ERROR:  plpy.SPIError: invalid input syntax for integer: "oops"
  CONTEXT:  PL/Python function "subxact_test"
  SELECT * FROM subxact_tbl;
   i 
--- 43,49 ----
  
  TRUNCATE subxact_tbl;
  SELECT subxact_test('SPI');
! ERROR:  spiexceptions.InvalidTextRepresentation: invalid input syntax for integer: "oops"
  CONTEXT:  PL/Python function "subxact_test"
  SELECT * FROM subxact_tbl;
   i 
*************** SELECT * FROM subxact_tbl;
*** 87,93 ****
  
  TRUNCATE subxact_tbl;
  SELECT subxact_ctx_test('SPI');
! ERROR:  plpy.SPIError: invalid input syntax for integer: "oops"
  CONTEXT:  PL/Python function "subxact_ctx_test"
  SELECT * FROM subxact_tbl;
   i 
--- 87,93 ----
  
  TRUNCATE subxact_tbl;
  SELECT subxact_ctx_test('SPI');
! ERROR:  spiexceptions.InvalidTextRepresentation: invalid input syntax for integer: "oops"
  CONTEXT:  PL/Python function "subxact_ctx_test"
  SELECT * FROM subxact_tbl;
   i 
*************** with plpy.subxact():
*** 122,128 ****
  return "ok"
  $$ LANGUAGE plpythonu;
  SELECT subxact_nested_test();
! ERROR:  plpy.SPIError: syntax error at or near "error"
  CONTEXT:  PL/Python function "subxact_nested_test"
  SELECT * FROM subxact_tbl;
   i 
--- 122,128 ----
  return "ok"
  $$ LANGUAGE plpythonu;
  SELECT subxact_nested_test();
! ERROR:  spiexceptions.SyntaxError: syntax error at or near "error"
  CONTEXT:  PL/Python function "subxact_nested_test"
  SELECT * FROM subxact_tbl;
   i 
*************** SELECT * FROM subxact_tbl;
*** 131,137 ****
  
  TRUNCATE subxact_tbl;
  SELECT subxact_nested_test('t');
! NOTICE:  Swallowed SPIError('syntax error at or near "error"',)
  CONTEXT:  PL/Python function "subxact_nested_test"
   subxact_nested_test 
  ---------------------
--- 131,137 ----
  
  TRUNCATE subxact_tbl;
  SELECT subxact_nested_test('t');
! NOTICE:  Swallowed SyntaxError('syntax error at or near "error"',)
  CONTEXT:  PL/Python function "subxact_nested_test"
   subxact_nested_test 
  ---------------------
*************** with plpy.subxact():
*** 157,163 ****
  return "ok"
  $$ LANGUAGE plpythonu;
  SELECT subxact_deeply_nested_test();
! NOTICE:  Swallowed SPIError('syntax error at or near "error"',)
  CONTEXT:  PL/Python function "subxact_nested_test"
  SQL statement "select subxact_nested_test('t')"
  PL/Python function "subxact_nested_test"
--- 157,163 ----
  return "ok"
  $$ LANGUAGE plpythonu;
  SELECT subxact_deeply_nested_test();
! NOTICE:  Swallowed SyntaxError('syntax error at or near "error"',)
  CONTEXT:  PL/Python function "subxact_nested_test"
  SQL statement "select subxact_nested_test('t')"
  PL/Python function "subxact_nested_test"
diff --git a/src/pl/plpython/expected/plpython_test.out b/src/pl/plpython/expected/plpython_test.out
index 674c739..ef2b284 100644
*** a/src/pl/plpython/expected/plpython_test.out
--- b/src/pl/plpython/expected/plpython_test.out
*************** contents.sort()
*** 43,51 ****
  return ", ".join(contents)
  $$ LANGUAGE plpythonu;
  select module_contents();
!                                           module_contents                                           
! ----------------------------------------------------------------------------------------------------
!  Error, Fatal, SPIError, debug, error, execute, fatal, info, log, notice, prepare, subxact, warning
  (1 row)
  
  CREATE FUNCTION elog_test() RETURNS void
--- 43,51 ----
  return ", ".join(contents)
  $$ LANGUAGE plpythonu;
  select module_contents();
!                                                   module_contents                                                  
! -------------------------------------------------------------------------------------------------------------------
!  Error, Fatal, SPIError, debug, error, execute, fatal, info, log, notice, prepare, spiexceptions, subxact, warning
  (1 row)
  
  CREATE FUNCTION elog_test() RETURNS void
diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c
index bd5dc65..69d8d2c 100644
*** a/src/pl/plpython/plpython.c
--- b/src/pl/plpython/plpython.c
*************** typedef struct PLySubxactObject
*** 258,263 ****
--- 258,285 ----
  	bool	exited;
  } PLySubxactObject;
  
+ /* A list of all known exceptions, generated from utils/errcodes.h */
+ typedef struct ExceptionMap
+ {
+ 	char		*name;
+ 	char		*classname;
+ 	int			sqlstate;
+ } ExceptionMap;
+ 
+ static const ExceptionMap exception_map[] = {
+ #include "spiexceptions.h"
+ 	{NULL, NULL, 0}
+ };
+ 
+ /* A hashtable mapping sqlstates to exceptions, for speedy lookup */
+ static HTAB *PLy_spi_exceptions;
+ 
+ typedef struct PLyExceptionEntry
+ {
+ 	int			sqlstate;	/* hash key, must be first */
+ 	PyObject	*exc;		/* corresponding exception */
+ } PLyExceptionEntry;
+ 
  /* function declarations */
  
  #if PY_MAJOR_VERSION >= 3
*************** static PyMethodDef PLy_methods[] = {
*** 2765,2770 ****
--- 2787,2796 ----
  	{NULL, NULL, 0, NULL}
  };
  
+ static PyMethodDef PLy_exc_methods[] = {
+ 	{NULL, NULL, 0, NULL}
+ };
+ 
  #if PY_MAJOR_VERSION >= 3
  static PyModuleDef PLy_module = {
  	PyModuleDef_HEAD_INIT,		/* m_base */
*************** static PyModuleDef PLy_module = {
*** 2777,2782 ****
--- 2803,2820 ----
  	NULL,                       /* m_clear */
  	NULL                        /* m_free */
  };
+ 
+ static PyModuleDef PLy_exc_module = {
+ 	PyModuleDef_HEAD_INIT,		/* m_base */
+ 	"spiexceptions",			/* m_name */
+ 	NULL,						/* m_doc */
+ 	-1,							/* m_size */
+ 	PLy_exc_methods,			/* m_methods */
+ 	NULL,						/* m_reload */
+ 	NULL,						/* m_traverse */
+ 	NULL,						/* m_clear */
+ 	NULL						/* m_free */
+ };
  #endif
  
  /* plan object methods */
*************** PLy_spi_prepare(PyObject *self, PyObject
*** 3080,3086 ****
  	}
  	PG_CATCH();
  	{
! 		ErrorData	*edata;
  
  		Py_DECREF(plan);
  		Py_XDECREF(optr);
--- 3118,3126 ----
  	}
  	PG_CATCH();
  	{
! 		ErrorData				*edata;
! 		PLyExceptionEntry		*entry;
! 		PyObject				*exc;
  
  		Py_DECREF(plan);
  		Py_XDECREF(optr);
*************** PLy_spi_prepare(PyObject *self, PyObject
*** 3105,3112 ****
  			SPI_restore_connection();
  		}
  
  		/* Make Python raise the exception */
! 		PLy_exception_set(PLy_exc_spi_error, edata->message);
  		return NULL;
  	}
  	PG_END_TRY();
--- 3145,3158 ----
  			SPI_restore_connection();
  		}
  
+ 		/* Look up the correct exception */
+ 		entry = hash_search(PLy_spi_exceptions, &(edata->sqlerrcode),
+ 							HASH_FIND, NULL);
+ 		/* We really should find it, but just in case have a fallback */
+ 		Assert(entry != NULL);
+ 		exc = entry ? entry->exc : PLy_exc_spi_error;
  		/* Make Python raise the exception */
! 		PLy_exception_set(exc, edata->message);
  		return NULL;
  	}
  	PG_END_TRY();
*************** PLy_spi_execute_plan(PyObject *ob, PyObj
*** 3257,3263 ****
  	}
  	PG_CATCH();
  	{
! 		ErrorData	*edata;
  
  		/* Save error info */
  		MemoryContextSwitchTo(oldcontext);
--- 3303,3311 ----
  	}
  	PG_CATCH();
  	{
! 		ErrorData				*edata;
! 		PLyExceptionEntry		*entry;
! 		PyObject				*exc;
  
  		/* Save error info */
  		MemoryContextSwitchTo(oldcontext);
*************** PLy_spi_execute_plan(PyObject *ob, PyObj
*** 3292,3299 ****
  			SPI_restore_connection();
  		}
  
  		/* Make Python raise the exception */
! 		PLy_exception_set(PLy_exc_spi_error, edata->message);
  		return NULL;
  	}
  	PG_END_TRY();
--- 3340,3353 ----
  			SPI_restore_connection();
  		}
  
+ 		/* Look up the correct exception */
+ 		entry = hash_search(PLy_spi_exceptions, &(edata->sqlerrcode),
+ 							HASH_FIND, NULL);
+ 		/* We really should find it, but just in case have a fallback */
+ 		Assert(entry != NULL);
+ 		exc = entry ? entry->exc : PLy_exc_spi_error;
  		/* Make Python raise the exception */
! 		PLy_exception_set(exc, edata->message);
  		return NULL;
  	}
  	PG_END_TRY();
*************** PLy_spi_execute_query(char *query, long
*** 3352,3358 ****
  	}
  	PG_CATCH();
  	{
! 		ErrorData	*edata;
  
  		/* Save error info */
  		MemoryContextSwitchTo(oldcontext);
--- 3406,3414 ----
  	}
  	PG_CATCH();
  	{
! 		ErrorData				*edata;
! 		PLyExceptionEntry		*entry;
! 		PyObject				*exc;
  
  		/* Save error info */
  		MemoryContextSwitchTo(oldcontext);
*************** PLy_spi_execute_query(char *query, long
*** 3374,3381 ****
  			SPI_restore_connection();
  		}
  
  		/* Make Python raise the exception */
! 		PLy_exception_set(PLy_exc_spi_error, edata->message);
  		return NULL;
  	}
  	PG_END_TRY();
--- 3430,3443 ----
  			SPI_restore_connection();
  		}
  
+ 		/* Look up the correct exception */
+ 		entry = hash_search(PLy_spi_exceptions, &edata->sqlerrcode,
+ 							HASH_FIND, NULL);
+ 		/* We really should find it, but just in case have a fallback */
+ 		Assert(entry != NULL);
+ 		exc = entry ? entry->exc : PLy_exc_spi_error;
  		/* Make Python raise the exception */
! 		PLy_exception_set(exc, edata->message);
  		return NULL;
  	}
  	PG_END_TRY();
*************** static void PLy_add_exception(PyObject *
*** 3606,3625 ****
--- 3668,3724 ----
  #endif
  }
  
+ /* Add all the autogenerated exceptions as subclasses of SPIError */
+ static void
+ PLy_generate_spi_exceptions(PyObject *mod, PyObject *base)
+ {
+ 	int	i;
+ 
+ 	for (i = 0; exception_map[i].name != NULL; i++)
+ 	{
+ 		bool					 found;
+ 		PyObject				*exc;
+ 		PLyExceptionEntry		*entry;
+ 		PyObject				*sqlstate;
+ 		PyObject				*dict = PyDict_New();
+ 
+ 		sqlstate = PyString_FromString(unpack_sql_state(
+ 										   exception_map[i].sqlstate));
+ 		PyDict_SetItemString(dict, "sqlstate", sqlstate);
+ 		Py_DECREF(sqlstate);
+ 		exc = PyErr_NewException(exception_map[i].name, base, dict);
+ 		PLy_add_exception(mod, exception_map[i].classname, exc);
+ 		entry = hash_search(PLy_spi_exceptions, &exception_map[i].sqlstate,
+ 							HASH_ENTER, &found);
+ 		entry->exc = exc;
+ 		Assert(!found);
+ 	}
+ }
+ 
  /* Add exceptions to the plpy module */
  static void
  PLy_initialize_exceptions(PyObject *plpy)
  {
  	PyObject	*mod;
+ 	PyObject	*exc;
+ 	PyObject	*excmod;
+ 	HASHCTL		 hash_ctl;
  
  #if PY_MAJOR_VERSION < 3
  	/* For Python <3 we add the exceptions to the module dictionary */
  	mod = PyModule_GetDict(plpy);
+ 	excmod = Py_InitModule("spiexceptions", PLy_exc_methods);
+ 	exc = PyModule_GetDict(excmod);
  #else
  	/* In Python 3 you add them directly into the module */
  	mod = plpy;
+ 	excmod = PyModule_Create(&PLy_exc_module);
+ 	exc = excmod;
  #endif
  
+ 	if (PyModule_AddObject(plpy, "spiexceptions", excmod) < 0)
+ 		PLy_elog(ERROR, "Failed to add the spiexceptions module");
+ 
  	PLy_exc_error = PyErr_NewException("plpy.Error", NULL, NULL);
  	PLy_exc_fatal = PyErr_NewException("plpy.Fatal", NULL, NULL);
  	PLy_exc_spi_error = PyErr_NewException("plpy.SPIError", NULL, NULL);
*************** PLy_initialize_exceptions(PyObject *plpy
*** 3627,3632 ****
--- 3726,3740 ----
  	PLy_add_exception(mod, "Error", PLy_exc_error);
  	PLy_add_exception(mod, "Fatal", PLy_exc_fatal);
  	PLy_add_exception(mod, "SPIError", PLy_exc_spi_error);
+ 
+ 	memset(&hash_ctl, 0, sizeof(hash_ctl));
+ 	hash_ctl.keysize = sizeof(int);
+ 	hash_ctl.entrysize = sizeof(PLyExceptionEntry);
+ 	hash_ctl.hash = tag_hash;
+ 	PLy_spi_exceptions = hash_create("SPI exceptions", 256,
+ 									 &hash_ctl, HASH_ELEM | HASH_FUNCTION);
+ 
+ 	PLy_generate_spi_exceptions(exc, PLy_exc_spi_error);
  }
  
  /*
diff --git a/src/pl/plpython/sql/plpython_error.sql b/src/pl/plpython/sql/plpython_error.sql
index 5ca6849..2306c1d 100644
*** a/src/pl/plpython/sql/plpython_error.sql
--- b/src/pl/plpython/sql/plpython_error.sql
*************** return None
*** 107,109 ****
--- 107,130 ----
  	LANGUAGE plpythonu;
  
  SELECT valid_type('rick');
+ 
+ /* Check catching specific types of exceptions
+  */
+ CREATE TABLE specific (
+     i integer PRIMARY KEY
+ );
+ 
+ CREATE FUNCTION specific_exception(i integer) RETURNS void AS
+ $$
+ from plpy import spiexceptions
+ try:
+     plpy.execute("insert into specific values (%s)" % (i or "NULL"));
+ except spiexceptions.NotNullViolation, e:
+     plpy.notice("Violated the NOT NULL constraint, sqlstate %s" % e.sqlstate)
+ except spiexceptions.UniqueViolation, e:
+     plpy.notice("Violated the UNIQUE constraint, sqlstate %s" % e.sqlstate)
+ $$ LANGUAGE plpythonu;
+ 
+ SELECT specific_exception(2);
+ SELECT specific_exception(NULL);
+ SELECT specific_exception(2);
-- 
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