Attached is a patch that makes the following changes:

(1) refactor execQual.c to expose a function for checking an instance of
a domain value against a list of domain constraints

(2) adjust pl/pgsql to fetch the constraints associated with the
function's return value. Because this is expensive, the constraints are
fetched once per session, when the function is compiled. I then modified
pl/pgsql to check any applicable constraints on the return value of a
function before returning it to the backend.

(3) add some regression tests for #2

The patch does NOT add checking of domain constraints for other PLs, or
for intermediate values in PL/PgSQL -- I plan to take a look at doing
one or both of those soon.

I also made a few semi-related cleanups. In pl_exec.c, it seems to me
that estate.eval_econtext MUST be non-NULL during the guts of both
plpgsql_exec_trigger() and plpgsql_exec_function(). Therefore checking
that estate.eval_econtext is non-NULL when cleaning up is unnecessary
(line 649 and 417 in current sources, respectively), so I've removed the
checks. Am I missing something?

Barring any objections I'll apply this patch tomorrow some time.

-Neil

============================================================
*** src/backend/executor/execQual.c	eb1daef85341439578a3f6bb73549c4420292bc3
--- src/backend/executor/execQual.c	20fa43d2335a50a1663c8e3e1d7d9e5d091dc79f
***************
*** 2673,2679 ****
  {
  	CoerceToDomain *ctest = (CoerceToDomain *) cstate->xprstate.expr;
  	Datum		result;
- 	ListCell   *l;
  
  	result = ExecEvalExpr(cstate->arg, econtext, isNull, isDone);
  
--- 2673,2678 ----
***************
*** 2680,2686 ****
  	if (isDone && *isDone == ExprEndResult)
  		return result;			/* nothing to check */
  
! 	foreach(l, cstate->constraints)
  	{
  		DomainConstraintState *con = (DomainConstraintState *) lfirst(l);
  
--- 2679,2707 ----
  	if (isDone && *isDone == ExprEndResult)
  		return result;			/* nothing to check */
  
! 	ExecCheckDomainConstraints(result, *isNull, cstate->constraints,
! 							   ctest->resulttype, econtext);
! 
! 	/* No constraints failed, so return the datum */
! 	return result;
! }
! 
! /*
!  * Check a list of domain constraints against an instance of the
!  * domain type, 'value'. 'isnull' indicates whether the instance is
!  * null. 'constraints' is a list of DomainConstraintState nodes
!  * describing the constraints to check. 'valtype' is the OID of the
!  * domain type. 'econtext' is the ExprContext in which the constraint
!  * expressions are evaluated.
!  */
! void
! ExecCheckDomainConstraints(Datum value, bool isnull,
! 						   List *constraints, Oid valtype,
! 						   ExprContext *econtext)
! {
! 	ListCell *l;
! 
! 	foreach(l, constraints)
  	{
  		DomainConstraintState *con = (DomainConstraintState *) lfirst(l);
  
***************
*** 2687,2697 ****
  		switch (con->constrainttype)
  		{
  			case DOM_CONSTRAINT_NOTNULL:
! 				if (*isNull)
  					ereport(ERROR,
  							(errcode(ERRCODE_NOT_NULL_VIOLATION),
  							 errmsg("domain %s does not allow null values",
! 									format_type_be(ctest->resulttype))));
  				break;
  			case DOM_CONSTRAINT_CHECK:
  				{
--- 2708,2718 ----
  		switch (con->constrainttype)
  		{
  			case DOM_CONSTRAINT_NOTNULL:
! 				if (isnull)
  					ereport(ERROR,
  							(errcode(ERRCODE_NOT_NULL_VIOLATION),
  							 errmsg("domain %s does not allow null values",
! 									format_type_be(valtype))));
  				break;
  			case DOM_CONSTRAINT_CHECK:
  				{
***************
*** 2709,2726 ****
  					save_datum = econtext->domainValue_datum;
  					save_isNull = econtext->domainValue_isNull;
  
! 					econtext->domainValue_datum = result;
! 					econtext->domainValue_isNull = *isNull;
  
  					conResult = ExecEvalExpr(con->check_expr,
  											 econtext, &conIsNull, NULL);
  
! 					if (!conIsNull &&
! 						!DatumGetBool(conResult))
  						ereport(ERROR,
  								(errcode(ERRCODE_CHECK_VIOLATION),
  								 errmsg("value for domain %s violates check constraint \"%s\"",
! 										format_type_be(ctest->resulttype),
  										con->name)));
  					econtext->domainValue_datum = save_datum;
  					econtext->domainValue_isNull = save_isNull;
--- 2730,2746 ----
  					save_datum = econtext->domainValue_datum;
  					save_isNull = econtext->domainValue_isNull;
  
! 					econtext->domainValue_datum = value;
! 					econtext->domainValue_isNull = isnull;
  
  					conResult = ExecEvalExpr(con->check_expr,
  											 econtext, &conIsNull, NULL);
  
! 					if (!conIsNull && !DatumGetBool(conResult))
  						ereport(ERROR,
  								(errcode(ERRCODE_CHECK_VIOLATION),
  								 errmsg("value for domain %s violates check constraint \"%s\"",
! 										format_type_be(valtype),
  										con->name)));
  					econtext->domainValue_datum = save_datum;
  					econtext->domainValue_isNull = save_isNull;
***************
*** 2733,2741 ****
  				break;
  		}
  	}
- 
- 	/* If all has gone well (constraints did not fail) return the datum */
- 	return result;
  }
  
  /*
--- 2753,2758 ----
============================================================
*** src/include/executor/executor.h	502cb98a7719bfb0236c04c8ea7aa999026dc584
--- src/include/executor/executor.h	b111913a7ef4735321f09c7686dd95a03b86910f
***************
*** 136,141 ****
--- 136,144 ----
  extern int	ExecCleanTargetListLength(List *targetlist);
  extern TupleTableSlot *ExecProject(ProjectionInfo *projInfo,
  			ExprDoneCond *isDone);
+ extern void ExecCheckDomainConstraints(Datum value, bool isnull,
+ 									   List *constraints, Oid valtype,
+ 									   ExprContext *econtext);
  
  /*
   * prototypes from functions in execScan.c
============================================================
*** src/pl/plpgsql/src/pl_comp.c	425488af1f2bb20c51747073b9837369c846fe38
--- src/pl/plpgsql/src/pl_comp.c	baf5c26e94285de9529694a1d891e8c7c8119748
***************
*** 48,53 ****
--- 48,54 ----
  #include "catalog/pg_class.h"
  #include "catalog/pg_proc.h"
  #include "catalog/pg_type.h"
+ #include "commands/typecmds.h"
  #include "funcapi.h"
  #include "nodes/makefuncs.h"
  #include "parser/gramparse.h"
***************
*** 344,350 ****
  	switch (functype)
  	{
  		case T_FUNCTION:
- 
  			/*
  			 * Fetch info about the procedure's parameters. Allocations aren't
  			 * needed permanently, so make them in tmp cxt.
--- 345,350 ----
***************
*** 534,539 ****
--- 534,547 ----
  												  true);
  				}
  			}
+ 
+ 			/*
+ 			 * If the function returns a domain value, lookup and
+ 			 * store any constraints associated with the domain.
+ 			 */
+ 			if (typeStruct->typtype == 'd')
+ 				function->domain_constr = GetDomainConstraints(rettypeid);
+ 
  			ReleaseSysCache(typeTup);
  			break;
  
============================================================
*** src/pl/plpgsql/src/pl_exec.c	36a133fa66455f08b44cda267840ad15017b2c6c
--- src/pl/plpgsql/src/pl_exec.c	fa72a8b7cea04dc747541f4a4fb4c6fde6c3b2e3
***************
*** 413,421 ****
  		}
  	}
  
  	/* Clean up any leftover temporary memory */
! 	if (estate.eval_econtext != NULL)
! 		FreeExprContext(estate.eval_econtext);
  	estate.eval_econtext = NULL;
  	exec_eval_cleanup(&estate);
  
--- 413,426 ----
  		}
  	}
  
+ 	/* Check any applicable domain constraints */
+ 	if (func->domain_constr != NIL)
+ 		ExecCheckDomainConstraints(estate.retval, estate.retisnull,
+ 								   func->domain_constr, func->fn_rettype,
+ 								   estate.eval_econtext);
+ 
  	/* Clean up any leftover temporary memory */
! 	FreeExprContext(estate.eval_econtext);
  	estate.eval_econtext = NULL;
  	exec_eval_cleanup(&estate);
  
***************
*** 646,653 ****
  	}
  
  	/* Clean up any leftover temporary memory */
! 	if (estate.eval_econtext != NULL)
! 		FreeExprContext(estate.eval_econtext);
  	estate.eval_econtext = NULL;
  	exec_eval_cleanup(&estate);
  
--- 651,657 ----
  	}
  
  	/* Clean up any leftover temporary memory */
! 	FreeExprContext(estate.eval_econtext);
  	estate.eval_econtext = NULL;
  	exec_eval_cleanup(&estate);
  
***************
*** 3195,3201 ****
  						   errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
  
  				/*
! 				 * Get the number of the records field to change and the
  				 * number of attributes in the tuple.
  				 */
  				fno = SPI_fnumber(rec->tupdesc, recfield->fieldname);
--- 3199,3205 ----
  						   errdetail("The tuple structure of a not-yet-assigned record is indeterminate.")));
  
  				/*
! 				 * Get the number of the record's field to change and the
  				 * number of attributes in the tuple.
  				 */
  				fno = SPI_fnumber(rec->tupdesc, recfield->fieldname);
***************
*** 4100,4107 ****
  	if (!isnull)
  	{
  		/*
! 		 * If the type of the queries return value isn't that of the variable,
! 		 * convert it.
  		 */
  		if (valtype != reqtype || reqtypmod != -1)
  		{
--- 4104,4111 ----
  	if (!isnull)
  	{
  		/*
! 		 * If the type of the query's return value isn't that of the
! 		 * variable, convert it.
  		 */
  		if (valtype != reqtype || reqtypmod != -1)
  		{
============================================================
*** src/pl/plpgsql/src/plpgsql.h	40743f1288e49eb6a6aedf3ddb0846b598d7319b
--- src/pl/plpgsql/src/plpgsql.h	064138e4024ab55c179f1999327b0dd7324bdc79
***************
*** 568,573 ****
--- 568,574 ----
  	int			fn_functype;
  	PLpgSQL_func_hashkey *fn_hashkey;	/* back-link to hashtable key */
  	MemoryContext fn_cxt;
+ 	List		*domain_constr;	/* Domain constraints on the return value */
  
  	Oid			fn_rettype;
  	int			fn_rettyplen;
============================================================
*** src/test/regress/expected/plpgsql.out	3e546643943140cd0a07045bccf1c59f51d63fa2
--- src/test/regress/expected/plpgsql.out	9897920b146f632ba4b77ee425f75709bd1f1710
***************
*** 2721,2723 ****
--- 2721,2750 ----
  $$ language plpgsql;
  ERROR:  end label "outer_label" specified for unlabelled block
  CONTEXT:  compile of PL/pgSQL function "end_label4" near line 5
+ -- check that domain constraints on the function's return value are
+ -- enforced
+ CREATE DOMAIN foo_domain AS INT4 NOT NULL CHECK (VALUE < 10);
+ CREATE FUNCTION domain_check() RETURNS foo_domain AS $$
+ DECLARE
+     x int;
+     y int;
+ BEGIN
+     x := 35;
+     y := 15;
+     RETURN x - y;
+ END;
+ $$ LANGUAGE plpgsql;
+ SELECT domain_check(); -- should fail
+ ERROR:  value for domain foo_domain violates check constraint "foo_domain_check"
+ CONTEXT:  PL/pgSQL function "domain_check" while casting return value to function's return type
+ CREATE FUNCTION domain_check_null() RETURNS foo_domain AS $$
+ BEGIN
+     RETURN NULL;
+ END;
+ $$ LANGUAGE plpgsql;
+ SELECT domain_check_null(); -- should fail
+ ERROR:  domain foo_domain does not allow null values
+ CONTEXT:  PL/pgSQL function "domain_check_null" while casting return value to function's return type
+ DROP DOMAIN foo_domain CASCADE;
+ NOTICE:  drop cascades to function domain_check_null()
+ NOTICE:  drop cascades to function domain_check()
============================================================
*** src/test/regress/sql/plpgsql.sql	e8f2eb786ca15e60b817fc839cc412500ee5a693
--- src/test/regress/sql/plpgsql.sql	3d3ce2e48d253638b307d4bc14f549348491a299
***************
*** 2280,2282 ****
--- 2280,2309 ----
    end loop outer_label;
  end;
  $$ language plpgsql;
+ 
+ -- check that domain constraints on the function's return value are
+ -- enforced
+ CREATE DOMAIN foo_domain AS INT4 NOT NULL CHECK (VALUE < 10);
+ 
+ CREATE FUNCTION domain_check() RETURNS foo_domain AS $$
+ DECLARE
+     x int;
+     y int;
+ BEGIN
+     x := 35;
+     y := 15;
+     RETURN x - y;
+ END;
+ $$ LANGUAGE plpgsql;
+ 
+ SELECT domain_check(); -- should fail
+ 
+ CREATE FUNCTION domain_check_null() RETURNS foo_domain AS $$
+ BEGIN
+     RETURN NULL;
+ END;
+ $$ LANGUAGE plpgsql;
+ 
+ SELECT domain_check_null(); -- should fail
+ 
+ DROP DOMAIN foo_domain CASCADE;
---------------------------(end of broadcast)---------------------------
TIP 2: Don't 'kill -9' the postmaster

Reply via email to