From 319a81beafad444cf352040942123f0aed1dc392 Mon Sep 17 00:00:00 2001 From: Jamie Martin Date: Wed, 11 Apr 2012 18:12:42 -0700 Subject: [PATCH] Truncate trailing null attributes from heap rows to reduce the size of the row bitmap. --- src/backend/access/common/heaptuple.c | 64 ++++++--- src/backend/access/common/indextuple.c | 10 +- src/backend/access/heap/tuptoaster.c | 20 ++- src/include/access/htup.h | 2 +- src/test/regress/expected/trailing_nulls.out | 210 ++++++++++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/trailing_nulls.sql | 166 ++++++++++++++++++++ 7 files changed, 445 insertions(+), 29 deletions(-) create mode 100644 src/test/regress/expected/trailing_nulls.out create mode 100644 src/test/regress/sql/trailing_nulls.sql diff --git a/src/backend/access/common/heaptuple.c b/src/backend/access/common/heaptuple.c index 034dfe5..8975a12 100644 --- a/src/backend/access/common/heaptuple.c +++ b/src/backend/access/common/heaptuple.c @@ -133,22 +133,24 @@ void heap_fill_tuple(TupleDesc tupleDesc, Datum *values, bool *isnull, char *data, Size data_size, - uint16 *infomask, bits8 *bit) + uint16 *infomask, bits8 *bit, + int numberOfAttributes) { bits8 *bitP; int bitmask; int i; - int numberOfAttributes = tupleDesc->natts; Form_pg_attribute *att = tupleDesc->attrs; #ifdef USE_ASSERT_CHECKING char *start = data; #endif + *infomask &= ~(HEAP_HASNULL | HEAP_HASVARWIDTH | HEAP_HASEXTERNAL); if (bit != NULL) { bitP = &bit[-1]; bitmask = HIGHBIT; + *infomask |= HEAP_HASNULL; } else { @@ -157,7 +159,6 @@ heap_fill_tuple(TupleDesc tupleDesc, bitmask = 0; } - *infomask &= ~(HEAP_HASNULL | HEAP_HASVARWIDTH | HEAP_HASEXTERNAL); for (i = 0; i < numberOfAttributes; i++) { @@ -176,7 +177,6 @@ heap_fill_tuple(TupleDesc tupleDesc, if (isnull[i]) { - *infomask |= HEAP_HASNULL; continue; } @@ -636,14 +636,20 @@ heap_form_tuple(TupleDesc tupleDescriptor, int hoff; bool hasnull = false; Form_pg_attribute *att = tupleDescriptor->attrs; - int numberOfAttributes = tupleDescriptor->natts; + int maxNumberOfAttributes = tupleDescriptor->natts; int i; + int lastNonNullAttribute = -1; + int numberOfAttributes; /* The actual number of attributes for this particular row. + * trailing nulls aren't stored so this may be smaller than + * maxNumberOfAttributes + */ + - if (numberOfAttributes > MaxTupleAttributeNumber) + if (maxNumberOfAttributes > MaxTupleAttributeNumber) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_COLUMNS), errmsg("number of columns (%d) exceeds limit (%d)", - numberOfAttributes, MaxTupleAttributeNumber))); + maxNumberOfAttributes, MaxTupleAttributeNumber))); /* * Check for nulls and embedded tuples; expand any toasted attributes in @@ -656,20 +662,26 @@ heap_form_tuple(TupleDesc tupleDescriptor, * if an attribute is already toasted, it must have been sent to disk * already and so cannot contain toasted attributes. */ - for (i = 0; i < numberOfAttributes; i++) + for (i = 0; i < maxNumberOfAttributes; i++) { if (isnull[i]) hasnull = true; - else if (att[i]->attlen == -1 && + else + { + lastNonNullAttribute = i; + + if (att[i]->attlen == -1 && att[i]->attalign == 'd' && att[i]->attndims == 0 && !VARATT_IS_EXTENDED(DatumGetPointer(values[i]))) - { - values[i] = toast_flatten_tuple_attribute(values[i], + { + values[i] = toast_flatten_tuple_attribute(values[i], att[i]->atttypid, att[i]->atttypmod); + } } } + numberOfAttributes = lastNonNullAttribute + 1; /* * Determine total space needed @@ -719,7 +731,8 @@ heap_form_tuple(TupleDesc tupleDescriptor, (char *) td + hoff, data_len, &td->t_infomask, - (hasnull ? td->t_bits : NULL)); + (hasnull ? td->t_bits : NULL), + numberOfAttributes); return tuple; } @@ -1390,14 +1403,19 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor, int hoff; bool hasnull = false; Form_pg_attribute *att = tupleDescriptor->attrs; - int numberOfAttributes = tupleDescriptor->natts; + int maxNumberOfAttributes = tupleDescriptor->natts; int i; + int lastNonNullAttribute = -1; + int numberOfAttributes; /* The actual number of attributes for this particular row. + * trailing nulls aren't stored so this may be smaller than + * maxNumberOfAttributes + */ - if (numberOfAttributes > MaxTupleAttributeNumber) + if (maxNumberOfAttributes > MaxTupleAttributeNumber) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_COLUMNS), errmsg("number of columns (%d) exceeds limit (%d)", - numberOfAttributes, MaxTupleAttributeNumber))); + maxNumberOfAttributes, MaxTupleAttributeNumber))); /* * Check for nulls and embedded tuples; expand any toasted attributes in @@ -1410,20 +1428,25 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor, * if an attribute is already toasted, it must have been sent to disk * already and so cannot contain toasted attributes. */ - for (i = 0; i < numberOfAttributes; i++) + for (i = 0; i < maxNumberOfAttributes; i++) { if (isnull[i]) hasnull = true; - else if (att[i]->attlen == -1 && + else + { + lastNonNullAttribute = i; + if (att[i]->attlen == -1 && att[i]->attalign == 'd' && att[i]->attndims == 0 && !VARATT_IS_EXTENDED(values[i])) - { - values[i] = toast_flatten_tuple_attribute(values[i], + { + values[i] = toast_flatten_tuple_attribute(values[i], att[i]->atttypid, att[i]->atttypmod); + } } } + numberOfAttributes = lastNonNullAttribute + 1; /* * Determine total space needed @@ -1463,7 +1486,8 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor, (char *) tuple + hoff, data_len, &tuple->t_infomask, - (hasnull ? tuple->t_bits : NULL)); + (hasnull ? tuple->t_bits : NULL), + numberOfAttributes); return tuple; } diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c index 76c76e9..a924863 100644 --- a/src/backend/access/common/indextuple.c +++ b/src/backend/access/common/indextuple.c @@ -29,6 +29,13 @@ /* ---------------- * index_form_tuple * ---------------- + * + * Note that as opposed to heap rows we don't bother truncating off trailing + * null attributes. This is because the maximum number of attributes in an + * index (32) is well bounded and there isn't much to be gained by trying to + * reduce the bitmap that tracks null attributes since we don't expect index + * keys have a lot of trailing nulls. Also, the logic for tracking the null + * bitmap is hard-wired at 4 bytes (see IndexAttributeBitMapData). */ IndexTuple index_form_tuple(TupleDesc tupleDescriptor, @@ -139,7 +146,8 @@ index_form_tuple(TupleDesc tupleDescriptor, (char *) tp + hoff, data_size, &tupmask, - (hasnull ? (bits8 *) tp + sizeof(IndexTupleData) : NULL)); + (hasnull ? (bits8 *) tp + sizeof(IndexTupleData) : NULL), + numberOfAttributes); #ifdef TOAST_INDEX_HACK for (i = 0; i < numberOfAttributes; i++) diff --git a/src/backend/access/heap/tuptoaster.c b/src/backend/access/heap/tuptoaster.c index 28b5a20..6935a3c 100644 --- a/src/backend/access/heap/tuptoaster.c +++ b/src/backend/access/heap/tuptoaster.c @@ -425,6 +425,7 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup, bool need_free = false; bool need_delold = false; bool has_nulls = false; + int lastNonNullValOffset = -1; Size maxDataLen; Size hoff; @@ -534,6 +535,10 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup, has_nulls = true; continue; } + else + { + lastNonNullValOffset = i; + } /* * Now look at varlena attributes @@ -593,8 +598,8 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup, /* compute header overhead --- this should match heap_form_tuple() */ hoff = offsetof(HeapTupleHeaderData, t_bits); - if (has_nulls) - hoff += BITMAPLEN(numAttrs); + if (has_nulls && lastNonNullValOffset > -1) + hoff += BITMAPLEN(lastNonNullValOffset+1); if (newtup->t_data->t_infomask & HEAP_HASOID) hoff += sizeof(Oid); hoff = MAXALIGN(hoff); @@ -867,6 +872,7 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup, int32 new_header_len; int32 new_data_len; int32 new_tuple_len; + numAttrs = lastNonNullValOffset + 1; /* only store up to the last non-null attribute */ /* * Calculate the new size of the tuple. @@ -879,7 +885,7 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup, * whether there needs to be one at all. */ new_header_len = offsetof(HeapTupleHeaderData, t_bits); - if (has_nulls) + if (has_nulls && numAttrs > 0) new_header_len += BITMAPLEN(numAttrs); if (olddata->t_infomask & HEAP_HASOID) new_header_len += sizeof(Oid); @@ -914,7 +920,8 @@ toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup, (char *) new_data + new_header_len, new_data_len, &(new_data->t_infomask), - has_nulls ? new_data->t_bits : NULL); + has_nulls ? new_data->t_bits : NULL, + numAttrs); } else result_tuple = newtup; @@ -1060,12 +1067,12 @@ toast_flatten_tuple_attribute(Datum value, return value; /* not a composite type */ att = tupleDesc->attrs; - numAttrs = tupleDesc->natts; /* * Break down the tuple into fields. */ olddata = DatumGetHeapTupleHeader(value); + numAttrs = HeapTupleHeaderGetNatts(olddata); Assert(typeId == HeapTupleHeaderGetTypeId(olddata)); Assert(typeMod == HeapTupleHeaderGetTypMod(olddata)); /* Build a temporary HeapTuple control structure */ @@ -1147,7 +1154,8 @@ toast_flatten_tuple_attribute(Datum value, (char *) new_data + new_header_len, new_data_len, &(new_data->t_infomask), - has_nulls ? new_data->t_bits : NULL); + has_nulls ? new_data->t_bits : NULL, + numAttrs); /* * Free allocated temp values diff --git a/src/include/access/htup.h b/src/include/access/htup.h index 6a3778d..565b32b 100644 --- a/src/include/access/htup.h +++ b/src/include/access/htup.h @@ -891,7 +891,7 @@ extern Size heap_compute_data_size(TupleDesc tupleDesc, extern void heap_fill_tuple(TupleDesc tupleDesc, Datum *values, bool *isnull, char *data, Size data_size, - uint16 *infomask, bits8 *bit); + uint16 *infomask, bits8 *bit, int lastNonNullAttributeOffset); extern bool heap_attisnull(HeapTuple tup, int attnum); extern Datum nocachegetattr(HeapTuple tup, int attnum, TupleDesc att); diff --git a/src/test/regress/expected/trailing_nulls.out b/src/test/regress/expected/trailing_nulls.out new file mode 100644 index 0000000..197e729 --- /dev/null +++ b/src/test/regress/expected/trailing_nulls.out @@ -0,0 +1,210 @@ +-- +-- The row bitmap and row length don't track trailing nulls. Verify that this works correctly. +-- +-- simple case +CREATE TABLE trailing_null ( + c1 varchar(10), + c2 varchar(10), + c3 varchar(10), + c4 varchar(10), + c5 varchar(10) + ); + +INSERT INTO trailing_null VALUES (null, null, null, null, null); +INSERT INTO trailing_null VALUES (null, null, null, null, '5'); +INSERT INTO trailing_null VALUES (null, null, null, '4', null); +INSERT INTO trailing_null VALUES (null, null, '3', null, null); +INSERT INTO trailing_null VALUES (null, '2', null, null, null); +INSERT INTO trailing_null VALUES ('1', null, null, null, null); +SELECT * FROM trailing_null ORDER BY c1, c2, c3, c4, c5; + c1 | c2 | c3 | c4 | c5 +----+----+----+----+---- + 1 | | | | + | 2 | | | + | | 3 | | + | | | 4 | + | | | | 5 + | | | | +(6 rows) + +CREATE INDEX trailing_nulli ON trailing_null (c1, c2, c3, c4, c5); +INSERT INTO trailing_null VALUES ('1', null, null, null, null); +SELECT * FROM trailing_null ORDER BY c1, c2, c3, c4, c5; + c1 | c2 | c3 | c4 | c5 +----+----+----+----+---- + 1 | | | | + 1 | | | | + | 2 | | | + | | 3 | | + | | | 4 | + | | | | 5 + | | | | +(7 rows) + +DROP TABLE trailing_null; +-- corner case, no columns +CREATE TABLE trailing_null (); +DROP TABLE trailing_null; +-- toastable +CREATE TABLE trailing_null ( + c1 varchar(500), + c2 varchar(500), + c3 varchar(500), + c4 varchar(500), + c5 varchar(500), + c6 varchar(500), + c7 varchar(500), + c8 varchar(500), + c9 varchar(500), + c10 varchar(500), + c11 varchar(500), + c12 varchar(500), + c13 varchar(500), + c14 varchar(500), + c15 varchar(500), + c16 varchar(500), + c17 varchar(500) + ); +-- toast: easily compressed +INSERT INTO trailing_null VALUES ( + rpad('c1', 450, 'x'), + rpad('c2', 450, 'x'), + rpad('c3', 450, 'x'), + rpad('c4', 450, 'x'), + rpad('c5', 450, 'x'), + rpad('c6', 450, 'x'), + rpad('c7', 450, 'x'), + rpad('c8', 450, 'x'), + rpad('c9', 450, 'x'), + rpad('c10', 450, 'x'), + rpad('c11', 450, 'x'), + rpad('c12', 450, 'x'), + rpad('c13', 450, 'x'), + rpad('c14', 450, 'x'), + rpad('c15', 450, 'x'), + rpad('c16', 450, 'x'), + rpad('c17', 450, 'x') + ); +-- toast: off row storage +INSERT INTO trailing_null VALUES ( + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3) +); +-- verify that we have rows in our toast storage +VACUUM FULL trailing_null; +VACUUM ANALYZE trailing_null; +SELECT CAST(reltuples AS INTEGER) FROM pg_class + WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null'); + reltuples +----------- + 4 +(1 row) + + +-- non-toast, separate codepath +UPDATE trailing_null SET c17 = null; +SELECT length(c1), length(c16), c17 FROM trailing_null; + length | length | c17 +--------+--------+----- + 450 | 450 | + 336 | 336 | +(2 rows) + +VACUUM FULL trailing_null; +VACUUM ANALYZE trailing_null; +SELECT CAST(reltuples AS INTEGER) FROM pg_class + WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null'); + reltuples +----------- + 4 +(1 row) + +UPDATE trailing_null SET c16 = null; +SELECT length(c1), c16, c17 FROM trailing_null; + length | c16 | c17 +--------+-----+----- + 450 | | + 336 | | +(2 rows) + +VACUUM FULL trailing_null; +VACUUM ANALYZE trailing_null; +SELECT CAST(reltuples AS INTEGER) FROM pg_class + WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null'); + reltuples +----------- + 4 +(1 row) + +UPDATE trailing_null SET + c1 = null, + c2 = null, + c3 = null, + c4 = null, + c5 = null, + c6 = null, + c7 = null, + c8 = null, + c9 = null, + c10 = null, + c11 = null, + c12 = null, + c13 = null, + c14 = null, + c15 = null, + c16 = null, + c17 = null; +-- verify that we got rid of our toasted data +VACUUM FULL trailing_null; +VACUUM ANALYZE trailing_null; +SELECT CAST(reltuples AS INTEGER) FROM pg_class + WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null'); + reltuples +----------- + 4 +(1 row) + + +DROP TABLE trailing_null; +-- Let's hit the detoasting logic related to composite objects. Borrowed +-- from rowtypes.sql +-- The object here is to ensure that toasted references inside +-- composite values don't cause problems. The large f1 value will +-- be toasted inside pp, it must still work after being copied to people. +CREATE TYPE fullname as (first text, last text, extra text); +CREATE temp TABLE pp (f1 text, c1 varchar(10) null); +INSERT INTO pp VALUES (repeat('abcdefghijkl', 100000), null); +CREATE temp TABLE people (fn fullname, c1 varchar(10), c2 varchar(10)); +-- trailing null in our nested object +INSERT into people SELECT('Jim', f1, null)::fullname, c1, null from pp; +SELECT (fn).first, length((fn).last), c1, c2 from people; + first | length | c1 | c2 +-------+---------+----+---- + Jim | 1200000 | | +(1 row) + +ALTER TABLE people ADD COLUMN c3 varchar(10); +SELECT (fn).first, length((fn).last), c1, c2, c3 from people; + first | length | c1 | c2 | c3 +-------+---------+----+----+---- + Jim | 1200000 | | | +(1 row) + +DROP TABLE pp; +DROP TABLE people; +DROP TYPE fullname; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8852e0a..9ab329d 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -92,7 +92,7 @@ test: rules # ---------- # Another group of parallel tests # ---------- -test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock json +test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock json trailing_nulls # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/trailing_nulls.sql b/src/test/regress/sql/trailing_nulls.sql new file mode 100644 index 0000000..f617257 --- /dev/null +++ b/src/test/regress/sql/trailing_nulls.sql @@ -0,0 +1,166 @@ +-- +-- The row bitmap and row length don't track trailing nulls. Verify that this works correctly. +-- + +-- simple case +CREATE TABLE trailing_null ( + c1 varchar(10), + c2 varchar(10), + c3 varchar(10), + c4 varchar(10), + c5 varchar(10) + ); + +INSERT INTO trailing_null VALUES (null, null, null, null, null); +INSERT INTO trailing_null VALUES (null, null, null, null, '5'); +INSERT INTO trailing_null VALUES (null, null, null, '4', null); +INSERT INTO trailing_null VALUES (null, null, '3', null, null); +INSERT INTO trailing_null VALUES (null, '2', null, null, null); +INSERT INTO trailing_null VALUES ('1', null, null, null, null); + +SELECT * FROM trailing_null ORDER BY c1, c2, c3, c4, c5; + +CREATE INDEX trailing_nulli ON trailing_null (c1, c2, c3, c4, c5); + +INSERT INTO trailing_null VALUES ('1', null, null, null, null); + +SELECT * FROM trailing_null ORDER BY c1, c2, c3, c4, c5; + +DROP TABLE trailing_null; + +-- corner case, no columns +CREATE TABLE trailing_null (); +DROP TABLE trailing_null; + + +-- toastable +CREATE TABLE trailing_null ( + c1 varchar(500), + c2 varchar(500), + c3 varchar(500), + c4 varchar(500), + c5 varchar(500), + c6 varchar(500), + c7 varchar(500), + c8 varchar(500), + c9 varchar(500), + c10 varchar(500), + c11 varchar(500), + c12 varchar(500), + c13 varchar(500), + c14 varchar(500), + c15 varchar(500), + c16 varchar(500), + c17 varchar(500) + ); + +-- toast: easily compressed +INSERT INTO trailing_null VALUES ( + rpad('c1', 450, 'x'), + rpad('c2', 450, 'x'), + rpad('c3', 450, 'x'), + rpad('c4', 450, 'x'), + rpad('c5', 450, 'x'), + rpad('c6', 450, 'x'), + rpad('c7', 450, 'x'), + rpad('c8', 450, 'x'), + rpad('c9', 450, 'x'), + rpad('c10', 450, 'x'), + rpad('c11', 450, 'x'), + rpad('c12', 450, 'x'), + rpad('c13', 450, 'x'), + rpad('c14', 450, 'x'), + rpad('c15', 450, 'x'), + rpad('c16', 450, 'x'), + rpad('c17', 450, 'x') + ); + +-- toast: off row storage +INSERT INTO trailing_null VALUES ( + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3), + repeat('1234567890abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0987654321zywvutsrqponmlkjihgfedcba1b2c3e5f6g7h9i0', 3) +); + +-- verify that we have rows in our toast storage +VACUUM FULL trailing_null; +VACUUM ANALYZE trailing_null; +SELECT CAST(reltuples AS INTEGER) FROM pg_class + WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null'); + +-- non-toast, separate codepath +UPDATE trailing_null SET c17 = null; +SELECT length(c1), length(c16), c17 FROM trailing_null; +VACUUM FULL trailing_null; +VACUUM ANALYZE trailing_null; +SELECT CAST(reltuples AS INTEGER) FROM pg_class + WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null'); + +UPDATE trailing_null SET c16 = null; +SELECT length(c1), c16, c17 FROM trailing_null; +VACUUM FULL trailing_null; +VACUUM ANALYZE trailing_null; +SELECT CAST(reltuples AS INTEGER) FROM pg_class + WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null'); + +UPDATE trailing_null SET + c1 = null, + c2 = null, + c3 = null, + c4 = null, + c5 = null, + c6 = null, + c7 = null, + c8 = null, + c9 = null, + c10 = null, + c11 = null, + c12 = null, + c13 = null, + c14 = null, + c15 = null, + c16 = null, + c17 = null; + +-- verify that we got rid of our toasted data +VACUUM FULL trailing_null; +VACUUM ANALYZE trailing_null; +SELECT CAST(reltuples AS INTEGER) FROM pg_class + WHERE oid = (SELECT reltoastrelid FROM pg_class where relname = 'trailing_null'); + +DROP TABLE trailing_null; + +-- Let's hit the detoasting logic related to composite objects. Borrowed +-- from rowtypes.sql + +-- The object here is to ensure that toasted references inside +-- composite values don't cause problems. The large f1 value will +-- be toasted inside pp, it must still work after being copied to people. +CREATE TYPE fullname as (first text, last text, extra text); +CREATE temp TABLE pp (f1 text, c1 varchar(10) null); +INSERT INTO pp VALUES (repeat('abcdefghijkl', 100000), null); +CREATE temp TABLE people (fn fullname, c1 varchar(10), c2 varchar(10)); + +-- trailing null in our nested object +INSERT into people SELECT('Jim', f1, null)::fullname, c1, null from pp; +SELECT (fn).first, length((fn).last), c1, c2 from people; +ALTER TABLE people ADD COLUMN c3 varchar(10); +SELECT (fn).first, length((fn).last), c1, c2, c3 from people; + +DROP TABLE pp; +DROP TABLE people; +DROP TYPE fullname; -- 1.7.0.4