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;