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