Fast default trigger and expand_tuple fixes

Ensure that triggers get properly filled in tuples for the OLD value.
Also fix the logic of detecting missing null values. The previous logic
failed to detect a missing null column before the first missing column
with a default. Fixing this has simplified the logic a bit.

Regression tests are added to test changes. This should ensure better
coverage of expand_tuple().

Original bug reports, and some code and test scripts from Tomas Vondra

Backpatch to release 11.
This commit is contained in:
Andrew Dunstan 2018-09-24 16:11:24 -04:00
parent 60e612b602
commit 7636e5c60f
4 changed files with 328 additions and 33 deletions

View File

@ -823,44 +823,35 @@ expand_tuple(HeapTuple *targetHeapTuple,
{ {
if (attrmiss[firstmissingnum].am_present) if (attrmiss[firstmissingnum].am_present)
break; break;
else
hasNulls = true;
} }
/* /*
* If there are no more missing values everything else must be NULL * Now walk the missing attributes. If there is a missing value
* make space for it. Otherwise, it's going to be NULL.
*/ */
if (firstmissingnum >= natts) for (attnum = firstmissingnum;
attnum < natts;
attnum++)
{ {
hasNulls = true; if (attrmiss[attnum].am_present)
}
else
{
/*
* Now walk the missing attributes. If there is a missing value
* make space for it. Otherwise, it's going to be NULL.
*/
for (attnum = firstmissingnum;
attnum < natts;
attnum++)
{ {
if (attrmiss[attnum].am_present) Form_pg_attribute att = TupleDescAttr(tupleDesc, attnum);
{
Form_pg_attribute att = TupleDescAttr(tupleDesc, attnum);
targetDataLen = att_align_datum(targetDataLen, targetDataLen = att_align_datum(targetDataLen,
att->attalign, att->attalign,
att->attlen, att->attlen,
attrmiss[attnum].am_value); attrmiss[attnum].am_value);
targetDataLen = att_addlength_pointer(targetDataLen, targetDataLen = att_addlength_pointer(targetDataLen,
att->attlen, att->attlen,
attrmiss[attnum].am_value); attrmiss[attnum].am_value);
} }
else else
{ {
/* no missing value, so it must be null */ /* no missing value, so it must be null */
hasNulls = true; hasNulls = true;
}
} }
} }
} /* end if have missing values */ } /* end if have missing values */

View File

@ -3396,7 +3396,10 @@ ltrmark:;
LockBuffer(buffer, BUFFER_LOCK_UNLOCK); LockBuffer(buffer, BUFFER_LOCK_UNLOCK);
} }
result = heap_copytuple(&tuple); if (HeapTupleHeaderGetNatts(tuple.t_data) < relation->rd_att->natts)
result = heap_expand_tuple(&tuple, relation->rd_att);
else
result = heap_copytuple(&tuple);
ReleaseBuffer(buffer); ReleaseBuffer(buffer);
return result; return result;

View File

@ -539,8 +539,197 @@ FROM t1;
1 | 0 1 | 0
(20 rows) (20 rows)
DROP TABLE t1;
DROP TABLE T; DROP TABLE T;
-- test that we account for missing columns without defaults correctly
-- in expand_tuple, and that rows are correctly expanded for triggers
CREATE FUNCTION test_trigger()
RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
raise notice 'old tuple: %', to_json(OLD)::text;
if TG_OP = 'DELETE'
then
return OLD;
else
return NEW;
end if;
end;
$$;
-- 2 new columns, both have defaults
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,3);
ALTER TABLE t ADD COLUMN x int NOT NULL DEFAULT 4;
ALTER TABLE t ADD COLUMN y int NOT NULL DEFAULT 5;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | 3 | 4 | 5
(1 row)
UPDATE t SET y = 2;
NOTICE: old tuple: {"id":1,"a":1,"b":2,"c":3,"x":4,"y":5}
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | 3 | 4 | 2
(1 row)
DROP TABLE t;
-- 2 new columns, first has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,3);
ALTER TABLE t ADD COLUMN x int NOT NULL DEFAULT 4;
ALTER TABLE t ADD COLUMN y int;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | 3 | 4 |
(1 row)
UPDATE t SET y = 2;
NOTICE: old tuple: {"id":1,"a":1,"b":2,"c":3,"x":4,"y":null}
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | 3 | 4 | 2
(1 row)
DROP TABLE t;
-- 2 new columns, second has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,3);
ALTER TABLE t ADD COLUMN x int;
ALTER TABLE t ADD COLUMN y int NOT NULL DEFAULT 5;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | 3 | | 5
(1 row)
UPDATE t SET y = 2;
NOTICE: old tuple: {"id":1,"a":1,"b":2,"c":3,"x":null,"y":5}
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | 3 | | 2
(1 row)
DROP TABLE t;
-- 2 new columns, neither has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,3);
ALTER TABLE t ADD COLUMN x int;
ALTER TABLE t ADD COLUMN y int;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | 3 | |
(1 row)
UPDATE t SET y = 2;
NOTICE: old tuple: {"id":1,"a":1,"b":2,"c":3,"x":null,"y":null}
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | 3 | | 2
(1 row)
DROP TABLE t;
-- same as last 4 tests but here the last original column has a NULL value
-- 2 new columns, both have defaults
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,NULL);
ALTER TABLE t ADD COLUMN x int NOT NULL DEFAULT 4;
ALTER TABLE t ADD COLUMN y int NOT NULL DEFAULT 5;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | | 4 | 5
(1 row)
UPDATE t SET y = 2;
NOTICE: old tuple: {"id":1,"a":1,"b":2,"c":null,"x":4,"y":5}
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | | 4 | 2
(1 row)
DROP TABLE t;
-- 2 new columns, first has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,NULL);
ALTER TABLE t ADD COLUMN x int NOT NULL DEFAULT 4;
ALTER TABLE t ADD COLUMN y int;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | | 4 |
(1 row)
UPDATE t SET y = 2;
NOTICE: old tuple: {"id":1,"a":1,"b":2,"c":null,"x":4,"y":null}
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | | 4 | 2
(1 row)
DROP TABLE t;
-- 2 new columns, second has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,NULL);
ALTER TABLE t ADD COLUMN x int;
ALTER TABLE t ADD COLUMN y int NOT NULL DEFAULT 5;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | | | 5
(1 row)
UPDATE t SET y = 2;
NOTICE: old tuple: {"id":1,"a":1,"b":2,"c":null,"x":null,"y":5}
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | | | 2
(1 row)
DROP TABLE t;
-- 2 new columns, neither has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,NULL);
ALTER TABLE t ADD COLUMN x int;
ALTER TABLE t ADD COLUMN y int;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | | |
(1 row)
UPDATE t SET y = 2;
NOTICE: old tuple: {"id":1,"a":1,"b":2,"c":null,"x":null,"y":null}
SELECT * FROM t;
id | a | b | c | x | y
----+---+---+---+---+---
1 | 1 | 2 | | | 2
(1 row)
DROP TABLE t;
-- cleanup
DROP FUNCTION test_trigger();
DROP TABLE t1;
DROP FUNCTION set(name); DROP FUNCTION set(name);
DROP FUNCTION comp(); DROP FUNCTION comp();
DROP TABLE m; DROP TABLE m;

View File

@ -360,8 +360,120 @@ SELECT a,
AS z AS z
FROM t1; FROM t1;
DROP TABLE t1;
DROP TABLE T; DROP TABLE T;
-- test that we account for missing columns without defaults correctly
-- in expand_tuple, and that rows are correctly expanded for triggers
CREATE FUNCTION test_trigger()
RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
raise notice 'old tuple: %', to_json(OLD)::text;
if TG_OP = 'DELETE'
then
return OLD;
else
return NEW;
end if;
end;
$$;
-- 2 new columns, both have defaults
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,3);
ALTER TABLE t ADD COLUMN x int NOT NULL DEFAULT 4;
ALTER TABLE t ADD COLUMN y int NOT NULL DEFAULT 5;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
UPDATE t SET y = 2;
SELECT * FROM t;
DROP TABLE t;
-- 2 new columns, first has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,3);
ALTER TABLE t ADD COLUMN x int NOT NULL DEFAULT 4;
ALTER TABLE t ADD COLUMN y int;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
UPDATE t SET y = 2;
SELECT * FROM t;
DROP TABLE t;
-- 2 new columns, second has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,3);
ALTER TABLE t ADD COLUMN x int;
ALTER TABLE t ADD COLUMN y int NOT NULL DEFAULT 5;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
UPDATE t SET y = 2;
SELECT * FROM t;
DROP TABLE t;
-- 2 new columns, neither has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,3);
ALTER TABLE t ADD COLUMN x int;
ALTER TABLE t ADD COLUMN y int;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
UPDATE t SET y = 2;
SELECT * FROM t;
DROP TABLE t;
-- same as last 4 tests but here the last original column has a NULL value
-- 2 new columns, both have defaults
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,NULL);
ALTER TABLE t ADD COLUMN x int NOT NULL DEFAULT 4;
ALTER TABLE t ADD COLUMN y int NOT NULL DEFAULT 5;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
UPDATE t SET y = 2;
SELECT * FROM t;
DROP TABLE t;
-- 2 new columns, first has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,NULL);
ALTER TABLE t ADD COLUMN x int NOT NULL DEFAULT 4;
ALTER TABLE t ADD COLUMN y int;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
UPDATE t SET y = 2;
SELECT * FROM t;
DROP TABLE t;
-- 2 new columns, second has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,NULL);
ALTER TABLE t ADD COLUMN x int;
ALTER TABLE t ADD COLUMN y int NOT NULL DEFAULT 5;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
UPDATE t SET y = 2;
SELECT * FROM t;
DROP TABLE t;
-- 2 new columns, neither has default
CREATE TABLE t (id serial PRIMARY KEY, a int, b int, c int);
INSERT INTO t (a,b,c) VALUES (1,2,NULL);
ALTER TABLE t ADD COLUMN x int;
ALTER TABLE t ADD COLUMN y int;
CREATE TRIGGER a BEFORE UPDATE ON t FOR EACH ROW EXECUTE PROCEDURE test_trigger();
SELECT * FROM t;
UPDATE t SET y = 2;
SELECT * FROM t;
DROP TABLE t;
-- cleanup
DROP FUNCTION test_trigger();
DROP TABLE t1;
DROP FUNCTION set(name); DROP FUNCTION set(name);
DROP FUNCTION comp(); DROP FUNCTION comp();
DROP TABLE m; DROP TABLE m;