Truncate-trailing-null-columns-from-heap-rows.v2.patch
application/octet-stream
Filename: Truncate-trailing-null-columns-from-heap-rows.v2.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: context
Series: patch v2
| File | + | − |
|---|---|---|
| src/backend/access/common/heaptuple.c | 48 | 0 |
| src/backend/access/common/indextuple.c | 9 | 0 |
| src/backend/access/heap/tuptoaster.c | 21 | 0 |
| src/include/access/htup_details.h | 1 | 0 |
| src/test/regress/expected/trailing_nulls.out | 210 | 0 |
| src/test/regress/parallel_schedule | 1 | 0 |
| src/test/regress/sql/trailing_nulls.sql | 166 | 0 |
*** a/src/backend/access/common/heaptuple.c
--- b/src/backend/access/common/heaptuple.c
***************
*** 127,154 **** heap_compute_data_size(TupleDesc tupleDesc,
* We also fill the null bitmap (if any) and set the infomask bits
* that reflect the tuple's data contents.
*
* NOTE: it is now REQUIRED that the caller have pre-zeroed the data area.
*/
void
heap_fill_tuple(TupleDesc tupleDesc,
Datum *values, bool *isnull,
char *data, Size data_size,
! uint16 *infomask, bits8 *bit)
{
bits8 *bitP;
int bitmask;
int i;
- int numberOfAttributes = tupleDesc->natts;
Form_pg_attribute *att = tupleDesc->attrs;
#ifdef USE_ASSERT_CHECKING
char *start = data;
#endif
if (bit != NULL)
{
bitP = &bit[-1];
bitmask = HIGHBIT;
}
else
{
--- 127,160 ----
* We also fill the null bitmap (if any) and set the infomask bits
* that reflect the tuple's data contents.
*
+ * For truncating trailing nulls in the tuple pass the last non-null column
+ * value as numberOfAttributes. Also make sure that same number of columns
+ * are set in the tuple header formation.
+ *
* NOTE: it is now REQUIRED that the caller have pre-zeroed the data area.
*/
void
heap_fill_tuple(TupleDesc tupleDesc,
Datum *values, bool *isnull,
char *data, Size data_size,
! uint16 *infomask, bits8 *bit,
! int numberOfAttributes)
{
bits8 *bitP;
int bitmask;
int i;
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,163 **** heap_fill_tuple(TupleDesc tupleDesc,
bitmask = 0;
}
- *infomask &= ~(HEAP_HASNULL | HEAP_HASVARWIDTH | HEAP_HASEXTERNAL);
for (i = 0; i < numberOfAttributes; i++)
{
--- 163,168 ----
***************
*** 176,182 **** heap_fill_tuple(TupleDesc tupleDesc,
if (isnull[i])
{
- *infomask |= HEAP_HASNULL;
continue;
}
--- 181,186 ----
***************
*** 636,649 **** heap_form_tuple(TupleDesc tupleDescriptor,
int hoff;
bool hasnull = false;
Form_pg_attribute *att = tupleDescriptor->attrs;
! int numberOfAttributes = tupleDescriptor->natts;
int i;
! if (numberOfAttributes > MaxTupleAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("number of columns (%d) exceeds limit (%d)",
! numberOfAttributes, MaxTupleAttributeNumber)));
/*
* Check for nulls and embedded tuples; expand any toasted attributes in
--- 640,659 ----
int hoff;
bool hasnull = false;
Form_pg_attribute *att = tupleDescriptor->attrs;
! 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 (maxNumberOfAttributes > MaxTupleAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("number of columns (%d) exceeds limit (%d)",
! maxNumberOfAttributes, MaxTupleAttributeNumber)));
/*
* Check for nulls and embedded tuples; expand any toasted attributes in
***************
*** 656,675 **** 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++)
{
if (isnull[i])
hasnull = true;
! else 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],
att[i]->atttypid,
att[i]->atttypmod);
}
}
/*
* Determine total space needed
--- 666,691 ----
* if an attribute is already toasted, it must have been sent to disk
* already and so cannot contain toasted attributes.
*/
! for (i = 0; i < maxNumberOfAttributes; i++)
{
if (isnull[i])
hasnull = true;
! 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],
att[i]->atttypid,
att[i]->atttypmod);
+ }
}
}
+ numberOfAttributes = lastNonNullAttribute + 1;
/*
* Determine total space needed
***************
*** 719,725 **** heap_form_tuple(TupleDesc tupleDescriptor,
(char *) td + hoff,
data_len,
&td->t_infomask,
! (hasnull ? td->t_bits : NULL));
return tuple;
}
--- 735,742 ----
(char *) td + hoff,
data_len,
&td->t_infomask,
! (hasnull ? td->t_bits : NULL),
! numberOfAttributes);
return tuple;
}
***************
*** 1390,1403 **** heap_form_minimal_tuple(TupleDesc tupleDescriptor,
int hoff;
bool hasnull = false;
Form_pg_attribute *att = tupleDescriptor->attrs;
! int numberOfAttributes = tupleDescriptor->natts;
int i;
! if (numberOfAttributes > MaxTupleAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("number of columns (%d) exceeds limit (%d)",
! numberOfAttributes, MaxTupleAttributeNumber)));
/*
* Check for nulls and embedded tuples; expand any toasted attributes in
--- 1407,1425 ----
int hoff;
bool hasnull = false;
Form_pg_attribute *att = tupleDescriptor->attrs;
! 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 (maxNumberOfAttributes > MaxTupleAttributeNumber)
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_COLUMNS),
errmsg("number of columns (%d) exceeds limit (%d)",
! maxNumberOfAttributes, MaxTupleAttributeNumber)));
/*
* Check for nulls and embedded tuples; expand any toasted attributes in
***************
*** 1410,1429 **** 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++)
{
if (isnull[i])
hasnull = true;
! else 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],
att[i]->atttypid,
att[i]->atttypmod);
}
}
/*
* Determine total space needed
--- 1432,1456 ----
* if an attribute is already toasted, it must have been sent to disk
* already and so cannot contain toasted attributes.
*/
! for (i = 0; i < maxNumberOfAttributes; i++)
{
if (isnull[i])
hasnull = true;
! 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],
att[i]->atttypid,
att[i]->atttypmod);
+ }
}
}
+ numberOfAttributes = lastNonNullAttribute + 1;
/*
* Determine total space needed
***************
*** 1463,1469 **** heap_form_minimal_tuple(TupleDesc tupleDescriptor,
(char *) tuple + hoff,
data_len,
&tuple->t_infomask,
! (hasnull ? tuple->t_bits : NULL));
return tuple;
}
--- 1490,1497 ----
(char *) tuple + hoff,
data_len,
&tuple->t_infomask,
! (hasnull ? tuple->t_bits : NULL),
! numberOfAttributes);
return tuple;
}
*** a/src/backend/access/common/indextuple.c
--- b/src/backend/access/common/indextuple.c
***************
*** 29,34 ****
--- 29,41 ----
/* ----------------
* 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,145 **** index_form_tuple(TupleDesc tupleDescriptor,
(char *) tp + hoff,
data_size,
&tupmask,
! (hasnull ? (bits8 *) tp + sizeof(IndexTupleData) : NULL));
#ifdef TOAST_INDEX_HACK
for (i = 0; i < numberOfAttributes; i++)
--- 146,153 ----
(char *) tp + hoff,
data_size,
&tupmask,
! (hasnull ? (bits8 *) tp + sizeof(IndexTupleData) : NULL),
! numberOfAttributes);
#ifdef TOAST_INDEX_HACK
for (i = 0; i < numberOfAttributes; i++)
*** a/src/backend/access/heap/tuptoaster.c
--- b/src/backend/access/heap/tuptoaster.c
***************
*** 425,430 **** toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
--- 425,431 ----
bool need_free = false;
bool need_delold = false;
bool has_nulls = false;
+ int lastNonNullValOffset = -1;
Size maxDataLen;
Size hoff;
***************
*** 534,539 **** toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
--- 535,544 ----
has_nulls = true;
continue;
}
+ else
+ {
+ lastNonNullValOffset = i;
+ }
/*
* Now look at varlena attributes
***************
*** 593,600 **** 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 (newtup->t_data->t_infomask & HEAP_HASOID)
hoff += sizeof(Oid);
hoff = MAXALIGN(hoff);
--- 598,605 ----
/* compute header overhead --- this should match heap_form_tuple() */
hoff = offsetof(HeapTupleHeaderData, t_bits);
! if (has_nulls && lastNonNullValOffset > -1)
! hoff += BITMAPLEN(lastNonNullValOffset + 1);
if (newtup->t_data->t_infomask & HEAP_HASOID)
hoff += sizeof(Oid);
hoff = MAXALIGN(hoff);
***************
*** 869,874 **** toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
--- 874,886 ----
int32 new_tuple_len;
/*
+ * Only store up to the last non-null attribute in side tuple i.e
+ * truncate the trailing null columns. Once the tuple is formed reset
+ * numAttrs to previous value
+ */
+ numAttrs = lastNonNullValOffset + 1;
+
+ /*
* Calculate the new size of the tuple.
*
* Note: we used to assume here that the old tuple's t_hoff must equal
***************
*** 879,885 **** 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)
new_header_len += BITMAPLEN(numAttrs);
if (olddata->t_infomask & HEAP_HASOID)
new_header_len += sizeof(Oid);
--- 891,897 ----
* whether there needs to be one at all.
*/
new_header_len = offsetof(HeapTupleHeaderData, t_bits);
! if (has_nulls && numAttrs > 0)
new_header_len += BITMAPLEN(numAttrs);
if (olddata->t_infomask & HEAP_HASOID)
new_header_len += sizeof(Oid);
***************
*** 914,920 **** 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);
}
else
result_tuple = newtup;
--- 926,935 ----
(char *) new_data + new_header_len,
new_data_len,
&(new_data->t_infomask),
! has_nulls ? new_data->t_bits : NULL,
! numAttrs);
!
! numAttrs = tupleDesc->natts;
}
else
result_tuple = newtup;
***************
*** 1147,1153 **** 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);
/*
* Free allocated temp values
--- 1162,1169 ----
(char *) new_data + new_header_len,
new_data_len,
&(new_data->t_infomask),
! has_nulls ? new_data->t_bits : NULL,
! numAttrs);
/*
* Free allocated temp values
*** a/src/include/access/htup_details.h
--- b/src/include/access/htup_details.h
***************
*** 618,624 **** 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);
extern bool heap_attisnull(HeapTuple tup, int attnum);
extern Datum nocachegetattr(HeapTuple tup, int attnum,
TupleDesc att);
--- 618,624 ----
extern void heap_fill_tuple(TupleDesc tupleDesc,
Datum *values, bool *isnull,
char *data, Size data_size,
! uint16 *infomask, bits8 *bit, int lastNonNullAttributeOffset);
extern bool heap_attisnull(HeapTuple tup, int attnum);
extern Datum nocachegetattr(HeapTuple tup, int attnum,
TupleDesc att);
*** /dev/null
--- b/src/test/regress/expected/trailing_nulls.out
***************
*** 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;
*** a/src/test/regress/parallel_schedule
--- b/src/test/regress/parallel_schedule
***************
*** 98,104 **** test: event_trigger
# ----------
# 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
# ----------
# Another group of parallel tests
--- 98,104 ----
# ----------
# 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 trailing_nulls
# ----------
# Another group of parallel tests
*** /dev/null
--- b/src/test/regress/sql/trailing_nulls.sql
***************
*** 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;