]> granicus.if.org Git - postgresql/commitdiff
Fast default trigger and expand_tuple fixes
authorAndrew Dunstan <andrew@dunslane.net>
Mon, 24 Sep 2018 20:11:24 +0000 (16:11 -0400)
committerAndrew Dunstan <andrew@dunslane.net>
Mon, 24 Sep 2018 20:11:24 +0000 (16:11 -0400)
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.

src/backend/access/common/heaptuple.c
src/backend/commands/trigger.c
src/test/regress/expected/fast_default.out
src/test/regress/sql/fast_default.sql

index d8b06bca7e7b81a3804e95c78260896ab418caac..7fc2fee74abbcbe6cb3ab1ccc29643508787351b 100644 (file)
@@ -823,44 +823,35 @@ expand_tuple(HeapTuple *targetHeapTuple,
                {
                        if (attrmiss[firstmissingnum].am_present)
                                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)
-               {
-                       hasNulls = true;
-               }
-               else
+               for (attnum = firstmissingnum;
+                        attnum < natts;
+                        attnum++)
                {
-
-                       /*
-                        * 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)
                        {
-                               if (attrmiss[attnum].am_present)
-                               {
-                                       Form_pg_attribute att = TupleDescAttr(tupleDesc, attnum);
+                               Form_pg_attribute att = TupleDescAttr(tupleDesc, attnum);
 
-                                       targetDataLen = att_align_datum(targetDataLen,
-                                                                                                       att->attalign,
-                                                                                                       att->attlen,
-                                                                                                       attrmiss[attnum].am_value);
+                               targetDataLen = att_align_datum(targetDataLen,
+                                                                                               att->attalign,
+                                                                                               att->attlen,
+                                                                                               attrmiss[attnum].am_value);
 
-                                       targetDataLen = att_addlength_pointer(targetDataLen,
-                                                                                                                 att->attlen,
-                                                                                                                 attrmiss[attnum].am_value);
-                               }
-                               else
-                               {
-                                       /* no missing value, so it must be null */
-                                       hasNulls = true;
-                               }
+                               targetDataLen = att_addlength_pointer(targetDataLen,
+                                                                                                         att->attlen,
+                                                                                                         attrmiss[attnum].am_value);
+                       }
+                       else
+                       {
+                               /* no missing value, so it must be null */
+                               hasNulls = true;
                        }
                }
        }                                                       /* end if have missing values */
index 2436692eb859dc5b84b9b178be375c5e1e62b47d..0665f110ba34c966e32e6665f2f32534141de270 100644 (file)
@@ -3396,7 +3396,10 @@ ltrmark:;
                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);
 
        return result;
index f3d783c28131f42be71e0fbd12d8da5a34260532..48bd360a799bc2ba1ecba8675e587629d7cfb4c5 100644 (file)
@@ -539,8 +539,197 @@ FROM t1;
  1 | 0
 (20 rows)
 
-DROP TABLE t1;
 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 comp();
 DROP TABLE m;
index 7b9cc47cef5e385ca9e7292f7b414814fcd150f2..06205cb39f0ca6238779e180bc3166e93a38118f 100644 (file)
@@ -360,8 +360,120 @@ SELECT a,
        AS z
 FROM t1;
 
-DROP TABLE t1;
 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 comp();
 DROP TABLE m;