Commit 51db0d18 authored by Tom Lane's avatar Tom Lane

Fix plpgsql to enforce domain checks when returning a NULL domain value.

If a plpgsql function is declared to return a domain type, and the domain's
constraints forbid a null value, it was nonetheless possible to return
NULL, because we didn't bother to check the constraints for a null result.
I'd noticed this while fooling with domains-over-composite, but had not
gotten around to fixing it immediately.

Add a regression test script exercising this and various other domain
cases, largely borrowed from the plpython_types test.

Although this is clearly a bug fix, I'm not sure whether anyone would
thank us for changing the behavior in stable branches, so I'm inclined
not to back-patch.
parent 439c7bc1
...@@ -26,7 +26,7 @@ DATA = plpgsql.control plpgsql--1.0.sql plpgsql--unpackaged--1.0.sql ...@@ -26,7 +26,7 @@ DATA = plpgsql.control plpgsql--1.0.sql plpgsql--unpackaged--1.0.sql
REGRESS_OPTS = --dbname=$(PL_TESTDB) REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS = plpgsql_call plpgsql_control plpgsql_record \ REGRESS = plpgsql_call plpgsql_control plpgsql_domain plpgsql_record \
plpgsql_transaction plpgsql_varprops plpgsql_transaction plpgsql_varprops
all: all-lib all: all-lib
......
This diff is collapsed.
...@@ -557,6 +557,7 @@ do_compile(FunctionCallInfo fcinfo, ...@@ -557,6 +557,7 @@ do_compile(FunctionCallInfo fcinfo,
} }
function->fn_retistuple = type_is_rowtype(rettypeid); function->fn_retistuple = type_is_rowtype(rettypeid);
function->fn_retisdomain = (typeStruct->typtype == TYPTYPE_DOMAIN);
function->fn_retbyval = typeStruct->typbyval; function->fn_retbyval = typeStruct->typbyval;
function->fn_rettyplen = typeStruct->typlen; function->fn_rettyplen = typeStruct->typlen;
...@@ -584,6 +585,7 @@ do_compile(FunctionCallInfo fcinfo, ...@@ -584,6 +585,7 @@ do_compile(FunctionCallInfo fcinfo,
function->fn_rettype = InvalidOid; function->fn_rettype = InvalidOid;
function->fn_retbyval = false; function->fn_retbyval = false;
function->fn_retistuple = true; function->fn_retistuple = true;
function->fn_retisdomain = false;
function->fn_retset = false; function->fn_retset = false;
/* shouldn't be any declared arguments */ /* shouldn't be any declared arguments */
...@@ -707,6 +709,7 @@ do_compile(FunctionCallInfo fcinfo, ...@@ -707,6 +709,7 @@ do_compile(FunctionCallInfo fcinfo,
function->fn_rettype = VOIDOID; function->fn_rettype = VOIDOID;
function->fn_retbyval = false; function->fn_retbyval = false;
function->fn_retistuple = true; function->fn_retistuple = true;
function->fn_retisdomain = false;
function->fn_retset = false; function->fn_retset = false;
/* shouldn't be any declared arguments */ /* shouldn't be any declared arguments */
...@@ -886,6 +889,7 @@ plpgsql_compile_inline(char *proc_source) ...@@ -886,6 +889,7 @@ plpgsql_compile_inline(char *proc_source)
function->fn_rettype = VOIDOID; function->fn_rettype = VOIDOID;
function->fn_retset = false; function->fn_retset = false;
function->fn_retistuple = false; function->fn_retistuple = false;
function->fn_retisdomain = false;
/* a bit of hardwired knowledge about type VOID here */ /* a bit of hardwired knowledge about type VOID here */
function->fn_retbyval = true; function->fn_retbyval = true;
function->fn_rettyplen = sizeof(int32); function->fn_rettyplen = sizeof(int32);
......
...@@ -712,6 +712,22 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo, ...@@ -712,6 +712,22 @@ plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo,
func->fn_rettyplen); func->fn_rettyplen);
} }
} }
else
{
/*
* We're returning a NULL, which normally requires no conversion work
* regardless of datatypes. But, if we are casting it to a domain
* return type, we'd better check that the domain's constraints pass.
*/
if (func->fn_retisdomain)
estate.retval = exec_cast_value(&estate,
estate.retval,
&fcinfo->isnull,
estate.rettype,
-1,
func->fn_rettype,
-1);
}
estate.err_text = gettext_noop("during function exit"); estate.err_text = gettext_noop("during function exit");
......
...@@ -915,6 +915,7 @@ typedef struct PLpgSQL_function ...@@ -915,6 +915,7 @@ typedef struct PLpgSQL_function
int fn_rettyplen; int fn_rettyplen;
bool fn_retbyval; bool fn_retbyval;
bool fn_retistuple; bool fn_retistuple;
bool fn_retisdomain;
bool fn_retset; bool fn_retset;
bool fn_readonly; bool fn_readonly;
......
--
-- Tests for PL/pgSQL's behavior with domain types
--
CREATE DOMAIN booltrue AS bool CHECK (VALUE IS TRUE OR VALUE IS NULL);
CREATE FUNCTION test_argresult_booltrue(x booltrue, y bool) RETURNS booltrue AS $$
begin
return y;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_argresult_booltrue(true, true);
SELECT * FROM test_argresult_booltrue(false, true);
SELECT * FROM test_argresult_booltrue(true, false);
CREATE FUNCTION test_assign_booltrue(x bool, y bool) RETURNS booltrue AS $$
declare v booltrue := x;
begin
v := y;
return v;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_assign_booltrue(true, true);
SELECT * FROM test_assign_booltrue(false, true);
SELECT * FROM test_assign_booltrue(true, false);
CREATE DOMAIN uint2 AS int2 CHECK (VALUE >= 0);
CREATE FUNCTION test_argresult_uint2(x uint2, y int) RETURNS uint2 AS $$
begin
return y;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_argresult_uint2(100::uint2, 50);
SELECT * FROM test_argresult_uint2(100::uint2, -50);
SELECT * FROM test_argresult_uint2(null, 1);
CREATE FUNCTION test_assign_uint2(x int, y int) RETURNS uint2 AS $$
declare v uint2 := x;
begin
v := y;
return v;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_assign_uint2(100, 50);
SELECT * FROM test_assign_uint2(100, -50);
SELECT * FROM test_assign_uint2(-100, 50);
SELECT * FROM test_assign_uint2(null, 1);
CREATE DOMAIN nnint AS int NOT NULL;
CREATE FUNCTION test_argresult_nnint(x nnint, y int) RETURNS nnint AS $$
begin
return y;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_argresult_nnint(10, 20);
SELECT * FROM test_argresult_nnint(null, 20);
SELECT * FROM test_argresult_nnint(10, null);
CREATE FUNCTION test_assign_nnint(x int, y int) RETURNS nnint AS $$
declare v nnint := x;
begin
v := y;
return v;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_assign_nnint(10, 20);
SELECT * FROM test_assign_nnint(null, 20);
SELECT * FROM test_assign_nnint(10, null);
--
-- Domains over arrays
--
CREATE DOMAIN ordered_pair_domain AS integer[] CHECK (array_length(VALUE,1)=2 AND VALUE[1] < VALUE[2]);
CREATE FUNCTION test_argresult_array_domain(x ordered_pair_domain)
RETURNS ordered_pair_domain AS $$
begin
return x;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_argresult_array_domain(ARRAY[0, 100]::ordered_pair_domain);
SELECT * FROM test_argresult_array_domain(NULL::ordered_pair_domain);
CREATE FUNCTION test_argresult_array_domain_check_violation()
RETURNS ordered_pair_domain AS $$
begin
return array[2,1];
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_argresult_array_domain_check_violation();
CREATE FUNCTION test_assign_ordered_pair_domain(x int, y int, z int) RETURNS ordered_pair_domain AS $$
declare v ordered_pair_domain := array[x, y];
begin
v[2] := z;
return v;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_assign_ordered_pair_domain(1,2,3);
SELECT * FROM test_assign_ordered_pair_domain(1,2,0);
SELECT * FROM test_assign_ordered_pair_domain(2,1,3);
--
-- Arrays of domains
--
CREATE FUNCTION test_read_uint2_array(x uint2[]) RETURNS uint2 AS $$
begin
return x[1];
end
$$ LANGUAGE plpgsql;
select test_read_uint2_array(array[1::uint2]);
CREATE FUNCTION test_build_uint2_array(x int2) RETURNS uint2[] AS $$
begin
return array[x, x];
end
$$ LANGUAGE plpgsql;
select test_build_uint2_array(1::int2);
select test_build_uint2_array(-1::int2); -- fail
CREATE FUNCTION test_argresult_domain_array(x integer[])
RETURNS ordered_pair_domain[] AS $$
begin
return array[x::ordered_pair_domain, x::ordered_pair_domain];
end
$$ LANGUAGE plpgsql;
select test_argresult_domain_array(array[2,4]);
select test_argresult_domain_array(array[4,2]); -- fail
CREATE FUNCTION test_argresult_domain_array2(x ordered_pair_domain)
RETURNS integer AS $$
begin
return x[1];
end
$$ LANGUAGE plpgsql;
select test_argresult_domain_array2(array[2,4]);
select test_argresult_domain_array2(array[4,2]); -- fail
CREATE FUNCTION test_argresult_array_domain_array(x ordered_pair_domain[])
RETURNS ordered_pair_domain AS $$
begin
return x[1];
end
$$ LANGUAGE plpgsql;
select test_argresult_array_domain_array(array[array[2,4]::ordered_pair_domain]);
--
-- Domains within composite
--
CREATE TYPE nnint_container AS (f1 int, f2 nnint);
CREATE FUNCTION test_result_nnint_container(x int, y int)
RETURNS nnint_container AS $$
begin
return row(x, y)::nnint_container;
end
$$ LANGUAGE plpgsql;
SELECT test_result_nnint_container(null, 3);
SELECT test_result_nnint_container(3, null); -- fail
CREATE FUNCTION test_assign_nnint_container(x int, y int, z int)
RETURNS nnint_container AS $$
declare v nnint_container := row(x, y);
begin
v.f2 := z;
return v;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_assign_nnint_container(1,2,3);
SELECT * FROM test_assign_nnint_container(1,2,null);
SELECT * FROM test_assign_nnint_container(1,null,3);
-- Since core system allows this:
SELECT null::nnint_container;
-- so should PL/PgSQL
CREATE FUNCTION test_assign_nnint_container2(x int, y int, z int)
RETURNS nnint_container AS $$
declare v nnint_container;
begin
v.f2 := z;
return v;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_assign_nnint_container2(1,2,3);
SELECT * FROM test_assign_nnint_container2(1,2,null);
--
-- Domains of composite
--
CREATE TYPE named_pair AS (
i integer,
j integer
);
CREATE DOMAIN ordered_named_pair AS named_pair CHECK((VALUE).i <= (VALUE).j);
CREATE FUNCTION read_ordered_named_pair(p ordered_named_pair) RETURNS integer AS $$
begin
return p.i + p.j;
end
$$ LANGUAGE plpgsql;
SELECT read_ordered_named_pair(row(1, 2));
SELECT read_ordered_named_pair(row(2, 1)); -- fail
CREATE FUNCTION build_ordered_named_pair(i int, j int) RETURNS ordered_named_pair AS $$
begin
return row(i, j);
end
$$ LANGUAGE plpgsql;
SELECT build_ordered_named_pair(1,2);
SELECT build_ordered_named_pair(2,1); -- fail
CREATE FUNCTION test_assign_ordered_named_pair(x int, y int, z int)
RETURNS ordered_named_pair AS $$
declare v ordered_named_pair := row(x, y);
begin
v.j := z;
return v;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_assign_ordered_named_pair(1,2,3);
SELECT * FROM test_assign_ordered_named_pair(1,2,0);
SELECT * FROM test_assign_ordered_named_pair(2,1,3);
CREATE FUNCTION build_ordered_named_pairs(i int, j int) RETURNS ordered_named_pair[] AS $$
begin
return array[row(i, j), row(i, j+1)];
end
$$ LANGUAGE plpgsql;
SELECT build_ordered_named_pairs(1,2);
SELECT build_ordered_named_pairs(2,1); -- fail
CREATE FUNCTION test_assign_ordered_named_pairs(x int, y int, z int)
RETURNS ordered_named_pair[] AS $$
declare v ordered_named_pair[] := array[row(x, y)];
begin
-- ideally this would work, but it doesn't yet:
-- v[1].j := z;
return v;
end
$$ LANGUAGE plpgsql;
SELECT * FROM test_assign_ordered_named_pairs(1,2,3);
SELECT * FROM test_assign_ordered_named_pairs(2,1,3);
SELECT * FROM test_assign_ordered_named_pairs(1,2,0); -- should fail someday
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment