ZStandard (with dictionaries) compression support for TOAST compression

Started by Nikhil Kumar Veldanda10 months ago46 messages
#1Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
1 attachment(s)

Hi all,

The ZStandard compression algorithm [1]https://facebook.github.io/zstd/[2]https://github.com/facebook/zstd, though not currently used for
TOAST compression in PostgreSQL, offers significantly improved compression
ratios compared to lz4/pglz in both dictionary-based and non-dictionary
modes. Attached find for review my patch to add ZStandard compression to
Postgres. In tests this patch used with a pre-trained dictionary achieved
up to four times the compression ratio of LZ4, while ZStandard without a
dictionary outperformed LZ4/pglz by about two times during compression of
data.

Notably, this is the first compression algorithm for Postgres that can make
use of a dictionary to provide higher levels of compression, but
dictionaries have to be generated and maintained, and so I’ve had to break
new ground in that regard. To use the dictionary support requires training
and storing a dictionary for a given variable-length column type. On a
variable-length column, a SQL function will be called. It will sample the
column’s data and feed it into the ZStandard training API which will return
a dictionary. In the example, the column is of JSONB type. The SQL function
takes the table name and the attribute number as inputs. If the training is
successful, it will return true; otherwise, it will return false.

‘’‘
test=# select build_zstd_dict_for_attribute('"public"."zstd"', 1);
build_zstd_dict_for_attribute
-------------------------------
t
(1 row)
‘’‘

The sampling logic and data to feed to the ZStandard training API can vary
by data type. The patch includes an method to write other type-specific
training functions and includes a default for JSONB, TEXT and BYTEA. There
is a new option called ‘build_zstd_dict’ that takes a function name as
input in ‘CREATE TYPE’. In this way anyone can write their own
type-specific training function by handling sampling logic and returning
the necessary information for the ZStandard training API in
“ZstdTrainingData” format.

```
typedef struct ZstdTrainingData
{
char *sample_buffer; /* Pointer to the raw sample buffer */
size_t *sample_sizes; /* Array of sample sizes */
int nitems; /* Number of sample sizes */
} ZstdTrainingData;
```
This information is feed into the ZStandard train API, which generates a
dictionary and inserts it into the dictionary catalog table. Additionally,
we update the ‘pg_attribute’ attribute options to include the unique
dictionary ID for that specific attribute. During compression, based on the
available dictionary ID, we retrieve the dictionary and use it to compress
the documents. I’ve created standard training function
(`zstd_dictionary_builder`) for JSONB, TEXT, and BYTEA.

We store dictionary and dictid in the new catalog table
‘pg_zstd_dictionaries’

```
test=# \d pg_zstd_dictionaries
Table "pg_catalog.pg_zstd_dictionaries"
Column | Type | Collation | Nullable | Default
--------+-------+-----------+----------+---------
dictid | oid | | not null |
dict | bytea | | not null |
Indexes:
"pg_zstd_dictionaries_dictid_index" PRIMARY KEY, btree (dictid)
```

This is the entire ZStandard dictionary infrastructure. A column can have
multiple dictionaries. The latest dictionary will be identified by the
pg_attribute attoptions. We never delete dictionaries once they are
generated. If a dictionary is not provided and attcompression is set to
zstd, we compress with ZStandard without dictionary. For decompression, the
zstd-compressed frame contains a dictionary identifier (dictid) that
indicates the dictionary used for compression. By retrieving this dictid
from the zstd frame, we then fetch the corresponding dictionary and perform
decompression.

#############################################################################

Enter toast compression framework changes,

We identify a compressed datum compression algorithm using the top two bits
of va_tcinfo (varattrib_4b.va_compressed).
It is possible to have four compression methods. However, based on previous
community email discussions regarding toast compression changes[3]/messages/by-id/YoMiNmkztrslDbNS@paquier.xyz, the
idea of using it for a new compression algorithm has been rejected, and a
suggestion has been made to extend it which I’ve implemented in this patch.
This change necessitates an update to ‘varattrib_4b’ and ‘varatt_external’
on disk structures. I’ve made sure that this changes are backward
compatible.

```
typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;
struct /* Compressed-in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method; see va_extinfo */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
} va_compressed;
struct
{
uint32 va_header;
uint32 va_tcinfo;
uint32 va_cmp_alg;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;

typedef struct varatt_external
{
int32 va_rawsize; /* Original data size (includes header) */
uint32 va_extinfo; /* External saved size (without header) and
* compression method */
Oid va_valueid; /* Unique ID of value within TOAST table */
Oid va_toastrelid; /* RelID of TOAST table containing it */
uint32 va_cmp_alg; /* The additional compression algorithms
* information. */
} varatt_external;
```

As I need to update this structs, I’ve made changes to the existing macros.
Additionally added compression and decompression routines related to
ZStandard as needed. These are major design changes in the patch to
incorporate ZStandard with dictionary compression.

Please let me know what you think about all this. Are there any concerns
with my approach? In particular, I would appreciate your thoughts on the
on-disk changes that result from this.

kind regards,

Nikhil Veldanda
Amazon Web Services: https://aws.amazon.com

[1]: https://facebook.github.io/zstd/
[2]: https://github.com/facebook/zstd
[3]: /messages/by-id/YoMiNmkztrslDbNS@paquier.xyz
/messages/by-id/YoMiNmkztrslDbNS@paquier.xyz

Attachments:

v1-0001-Add-ZStandard-with-dictionaries-compression-suppo.patchapplication/octet-stream; name=v1-0001-Add-ZStandard-with-dictionaries-compression-suppo.patchDownload
From 94cbf115d2d12d3908eac5b784c608e16df579e6 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Tue, 4 Mar 2025 08:14:32 +0000
Subject: [PATCH v1] Add ZStandard (with dictionaries) compression support for
 TOAST

---
 contrib/amcheck/verify_heapam.c               |   1 +
 doc/src/sgml/catalogs.sgml                    |  55 ++
 doc/src/sgml/ref/create_type.sgml             |  21 +-
 src/backend/access/brin/brin_tuple.c          |  18 +-
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/indextuple.c        |  18 +-
 src/backend/access/common/reloptions.c        |  36 +-
 src/backend/access/common/toast_compression.c | 269 +++++++-
 src/backend/access/common/toast_internals.c   |   8 +-
 src/backend/access/table/toast_helper.c       |  17 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/heap.c                    |   8 +-
 src/backend/catalog/meson.build               |   1 +
 src/backend/catalog/pg_type.c                 |  11 +-
 src/backend/catalog/pg_zstd_dictionaries.c    | 601 ++++++++++++++++++
 src/backend/commands/analyze.c                |   7 +-
 src/backend/commands/typecmds.c               |  99 ++-
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/pg_amcheck/t/004_verify_heapam.pl     |  14 +-
 src/bin/pg_dump/pg_dump.c                     |  25 +-
 src/bin/psql/describe.c                       |   5 +-
 src/include/access/toast_compression.h        |  13 +-
 src/include/access/toast_helper.h             |   2 +
 src/include/access/toast_internals.h          |  35 +-
 src/include/catalog/Makefile                  |   3 +-
 src/include/catalog/catversion.h              |   2 +-
 src/include/catalog/meson.build               |   1 +
 src/include/catalog/pg_proc.dat               |  10 +
 src/include/catalog/pg_type.dat               |   6 +-
 src/include/catalog/pg_type.h                 |   8 +-
 src/include/catalog/pg_zstd_dictionaries.h    |  53 ++
 src/include/parser/analyze.h                  |   5 +
 src/include/utils/attoptcache.h               |   6 +
 src/include/varatt.h                          |  80 ++-
 src/test/regress/expected/compression.out     |   5 +-
 src/test/regress/expected/compression_1.out   |   3 +
 .../regress/expected/compression_zstd.out     | 123 ++++
 .../regress/expected/compression_zstd_1.out   | 181 ++++++
 src/test/regress/expected/oidjoins.out        |   1 +
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression.sql          |   1 +
 src/test/regress/sql/compression_zstd.sql     |  97 +++
 src/tools/pgindent/typedefs.list              |   5 +
 45 files changed, 1791 insertions(+), 88 deletions(-)
 create mode 100644 src/backend/catalog/pg_zstd_dictionaries.c
 create mode 100644 src/include/catalog/pg_zstd_dictionaries.h
 create mode 100644 src/test/regress/expected/compression_zstd.out
 create mode 100644 src/test/regress/expected/compression_zstd_1.out
 create mode 100644 src/test/regress/sql/compression_zstd.sql

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 827312306f..f01cc940e3 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1700,6 +1700,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb05063555..ed4c51a678 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -369,6 +369,12 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-zstd-dictionaries"><structname>pg_zstd_dictionaries</structname></link></entry>
+      <entry>Zstandard dictionaries</entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
@@ -9779,4 +9785,53 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </table>
  </sect1>
 
+
+<sect1 id="catalog-pg-zstd-dictionaries">
+  <title><structname>pg_zstd_dictionaries</structname></title>
+
+  <indexterm zone="catalog-pg-zstd-dictionaries">
+    <primary>pg_zstd_dictionaries</primary>
+  </indexterm>
+
+  <para>
+    The catalog <structname>pg_zstd_dictionaries</structname> maintains the dictionaries essential for Zstandard compression and decompression.
+  </para>
+
+  <table>
+    <title><structname>pg_zstd_dictionaries</structname> Columns</title>
+    <tgroup cols="1">
+      <thead>
+        <row>
+          <entry role="catalog_table_entry">
+            <para role="column_definition">Column Type</para>
+            <para>Description</para>
+          </entry>
+        </row>
+      </thead>
+      <tbody>
+        <row>
+          <entry role="catalog_table_entry">
+            <para role="column_definition">
+              <structfield>dictid</structfield> <type>oid</type>
+            </para>
+            <para>
+              Dictionary identifier; a non-null OID that uniquely identifies a dictionary.
+            </para>
+          </entry>
+        </row>
+        <row>
+          <entry role="catalog_table_entry">
+            <para role="column_definition">
+              <structfield>dict</structfield> <type>bytea</type>
+            </para>
+            <para>
+              Variable-length field containing the zstd dictionary data. This field must not be null.
+            </para>
+          </entry>
+        </row>
+      </tbody>
+    </tgroup>
+  </table>
+</sect1>
+
 </chapter>
diff --git a/doc/src/sgml/ref/create_type.sgml b/doc/src/sgml/ref/create_type.sgml
index 994dfc6526..ad4cf2f8b3 100644
--- a/doc/src/sgml/ref/create_type.sgml
+++ b/doc/src/sgml/ref/create_type.sgml
@@ -56,6 +56,7 @@ CREATE TYPE <replaceable class="parameter">name</replaceable> (
     [ , ELEMENT = <replaceable class="parameter">element</replaceable> ]
     [ , DELIMITER = <replaceable class="parameter">delimiter</replaceable> ]
     [ , COLLATABLE = <replaceable class="parameter">collatable</replaceable> ]
+    [ , BUILD_ZSTD_DICT = <replaceable class="parameter">zstd_training_function</replaceable> ]
 )
 
 CREATE TYPE <replaceable class="parameter">name</replaceable>
@@ -211,7 +212,8 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
    <replaceable class="parameter">type_modifier_input_function</replaceable>,
    <replaceable class="parameter">type_modifier_output_function</replaceable>,
    <replaceable class="parameter">analyze_function</replaceable>, and
-   <replaceable class="parameter">subscript_function</replaceable>
+   <replaceable class="parameter">subscript_function</replaceable>, and
+   <replaceable class="parameter">zstd_training_function</replaceable>
    are optional.  Generally these functions have to be coded in C
    or another low-level language.
   </para>
@@ -491,6 +493,15 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
    make use of the collation information; this does not happen
    automatically merely by marking the type collatable.
   </para>
+
+  <para>
+    The optional <replaceable class="parameter">zstd_training_function</replaceable>
+    performs type-specific sample collection for a column of the corresponding data type.
+    By default, for <type>jsonb</type>, <type>text</type>, and <type>bytea</type> data types, the function <literal>zstd_dictionary_builder</literal> is defined. It attempts to gather samples for a column 
+    and returns a sample buffer for zstd dictionary training. The training function must be declared to accept two arguments of type <type>internal</type> and return an <type>internal</type> result. 
+    The detailed information for zstd training function is provided in <filename>src/backend/catalog/pg_zstd_dictionaries.c</filename>.
+  </para>
+
   </refsect2>
 
   <refsect2 id="sql-createtype-array" xreflabel="Array Types">
@@ -846,6 +857,14 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
      </para>
     </listitem>
    </varlistentry>
+   <varlistentry>
+    <term><replaceable class="parameter">build_zstd_dict</replaceable></term>
+    <listitem>
+        <para>
+        Specifies the name of a function that performs sampling and provides the logic necessary to generate a sample buffer for zstd training.
+        </para>
+    </listitem>
+   </varlistentry>
   </variablelist>
  </refsect1>
 
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6d..67175942e9 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -40,7 +40,7 @@
 #include "access/tupmacs.h"
 #include "utils/datum.h"
 #include "utils/memutils.h"
-
+#include "utils/attoptcache.h"
 
 /*
  * This enables de-toasting of index entries.  Needed until VACUUM is
@@ -223,6 +223,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			{
 				Datum		cvalue;
 				char		compression;
+				int			zstd_cmp_level = DEFAULT_ZSTD_CMP_LEVEL;
+				Oid			zstd_dictid = InvalidDictId;
 				Form_pg_attribute att = TupleDescAttr(brdesc->bd_tupdesc,
 													  keyno);
 
@@ -237,7 +239,19 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 				else
 					compression = InvalidCompressionMethod;
 
-				cvalue = toast_compress_datum(value, compression);
+				if (compression == TOAST_ZSTD_COMPRESSION)
+				{
+					AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+					if (aopt != NULL)
+					{
+						zstd_cmp_level = aopt->zstd_cmp_level;
+						zstd_dictid = (Oid) aopt->zstd_dictid;
+					}
+				}
+				cvalue = toast_compress_datum(value, compression,
+											  zstd_dictid,
+											  zstd_cmp_level);
 
 				if (DatumGetPointer(cvalue) != NULL)
 				{
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 6265178774..b57a9f024c 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -246,10 +246,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
 				TOAST_PGLZ_COMPRESSION_ID)
@@ -485,6 +485,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +530,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 1986b943a2..625f91629c 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -21,6 +21,7 @@
 #include "access/htup_details.h"
 #include "access/itup.h"
 #include "access/toast_internals.h"
+#include "utils/attoptcache.h"
 
 /*
  * This enables de-toasting of index entries.  Needed until VACUUM is
@@ -124,8 +125,23 @@ index_form_tuple_context(TupleDesc tupleDescriptor,
 		{
 			Datum		cvalue;
 
+			int			zstd_cmp_level = DEFAULT_ZSTD_CMP_LEVEL;
+			Oid			zstd_dictid = InvalidDictId;
+
+			if (att->attcompression == TOAST_ZSTD_COMPRESSION)
+			{
+				AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+				if (aopt != NULL)
+				{
+					zstd_cmp_level = aopt->zstd_cmp_level;
+					zstd_dictid = (Oid) aopt->zstd_dictid;
+				}
+			}
 			cvalue = toast_compress_datum(untoasted_values[i],
-										  att->attcompression);
+										  att->attcompression,
+										  zstd_dictid,
+										  zstd_cmp_level);
 
 			if (DatumGetPointer(cvalue) != NULL)
 			{
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 59fb53e770..b99ba93ec3 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -34,6 +34,7 @@
 #include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "access/toast_compression.h"
 
 /*
  * Contents of pg_class.reloptions
@@ -389,7 +390,26 @@ static relopt_int intRelOpts[] =
 		},
 		-1, 0, 1024
 	},
-
+	{
+		{
+			"zstd_dict_size",
+			"Max dict size for zstd",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_DICT_SIZE, 0, 112640	/* Max dict size(110 KB), 0
+											 * indicates Don't use dictionary
+											 * for compression */
+	},
+	{
+		{
+			"zstd_cmp_level",
+			"Set column's ZSTD compression level",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_CMP_LEVEL, 1, 22
+	},
 	/* list terminator */
 	{{NULL}}
 };
@@ -478,6 +498,15 @@ static relopt_real realRelOpts[] =
 		},
 		0, -1.0, DBL_MAX
 	},
+	{
+		{
+			"zstd_dictid",
+			"Current Zstd dictid for column",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		InvalidDictId, InvalidDictId, UINT32_MAX
+	},
 	{
 		{
 			"vacuum_cleanup_index_scale_factor",
@@ -2093,7 +2122,10 @@ attribute_reloptions(Datum reloptions, bool validate)
 {
 	static const relopt_parse_elt tab[] = {
 		{"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)},
-		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}
+		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)},
+		{"zstd_dictid", RELOPT_TYPE_REAL, offsetof(AttributeOpts, zstd_dictid)},
+		{"zstd_dict_size", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_dict_size)},
+		{"zstd_cmp_level", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_cmp_level)},
 	};
 
 	return (bytea *) build_reloptions(reloptions, validate,
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97..64151eb778 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,19 +17,25 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#include <zdict.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
+#include "catalog/pg_zstd_dictionaries.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
 
-#define NO_LZ4_SUPPORT() \
+#define NO_METHOD_SUPPORT(method) \
 	ereport(ERROR, \
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
-			 errmsg("compression method lz4 not supported"), \
-			 errdetail("This functionality requires the server to be built with lz4 support.")))
+			 errmsg("compression method %s not supported", method), \
+			 errdetail("This functionality requires the server to be built with %s support.", method)))
 
 /*
  * Compress a varlena using PGLZ.
@@ -139,7 +145,7 @@ struct varlena *
 lz4_compress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	NO_METHOD_SUPPORT("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		valsize;
@@ -182,7 +188,7 @@ struct varlena *
 lz4_decompress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	NO_METHOD_SUPPORT("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -215,7 +221,7 @@ struct varlena *
 lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	NO_METHOD_SUPPORT("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -289,10 +295,17 @@ CompressionNameToMethod(const char *compression)
 	else if (strcmp(compression, "lz4") == 0)
 	{
 #ifndef USE_LZ4
-		NO_LZ4_SUPPORT();
+		NO_METHOD_SUPPORT("lz4");
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd") == 0)
+	{
+#ifndef USE_ZSTD
+		NO_METHOD_SUPPORT("zstd");
+#endif
+		return TOAST_ZSTD_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -309,8 +322,250 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_COMPRESSION:
+			return "zstd";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
 	}
 }
+
+/* Compress datum using ZSTD with optional dictionary (using cdict) */
+struct varlena *
+zstd_compress_datum(const struct varlena *value, Oid dictid, int zstd_level)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	void	   *dest;
+	size_t		cmp_size,
+				ret;
+	ZSTD_CCtx  *cctx = ZSTD_createCCtx();
+	ZSTD_CDict *cdict = NULL;
+
+	if (!cctx)
+		ereport(ERROR, (errmsg("Failed to create ZSTD compression context")));
+
+	ret = ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, zstd_level);
+	if (ZSTD_isError(ret))
+	{
+		ZSTD_freeCCtx(cctx);
+		ereport(ERROR, (errmsg("Failed to reference ZSTD compression level: %s", ZSTD_getErrorName(ret))));
+	}
+
+	if (dictid != InvalidDictId)
+	{
+		bytea	   *dict_bytea = get_zstd_dict(dictid);
+		const void *dict_buffer = VARDATA_ANY(dict_bytea);
+		uint32		dict_size = VARSIZE_ANY(dict_bytea) - VARHDRSZ;
+
+		cdict = ZSTD_createCDict(dict_buffer, dict_size, zstd_level);
+		pfree(dict_bytea);
+
+		if (!cdict)
+		{
+			ZSTD_freeCCtx(cctx);
+			ereport(ERROR, (errmsg("Failed to create ZSTD compression dictionary")));
+		}
+
+		ret = ZSTD_CCtx_refCDict(cctx, cdict);
+		if (ZSTD_isError(ret))
+		{
+			ZSTD_freeCDict(cdict);
+			ZSTD_freeCCtx(cctx);
+			ereport(ERROR, (errmsg("Failed to reference ZSTD dictionary: %s", ZSTD_getErrorName(ret))));
+		}
+	}
+
+	/* Allocate space for the compressed varlena (header + data) */
+	compressed = (struct varlena *) palloc(max_size + VARHDRSZ_COMPRESSED_EXT);
+	dest = (char *) compressed + VARHDRSZ_COMPRESSED_EXT;
+
+	/* Compress the data */
+	cmp_size = ZSTD_compress2(cctx, dest, max_size, VARDATA_ANY(value), valsize);
+
+	/* Cleanup */
+	ZSTD_freeCDict(cdict);
+	ZSTD_freeCCtx(cctx);
+
+	if (ZSTD_isError(cmp_size))
+	{
+		pfree(compressed);
+		ereport(ERROR, (errmsg("ZSTD compression failed: %s", ZSTD_getErrorName(cmp_size))));
+	}
+
+	/*
+	 * If compression did not reduce size, return NULL so that the
+	 * uncompressed data is stored
+	 */
+	if (cmp_size > valsize)
+	{
+		pfree(compressed);
+		return NULL;
+	}
+
+	/* Set the compressed size in the varlena header */
+	SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARHDRSZ_COMPRESSED_EXT);
+	return compressed;
+
+#else
+	NO_METHOD_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		cmp_size_exhdr = VARSIZE_4B(value) - VARHDRSZ_COMPRESSED_EXT;
+	Oid			dictid;
+	struct varlena *result;
+	size_t		uncmp_size,
+				ret;
+	ZSTD_DCtx  *dctx = ZSTD_createDCtx();
+	ZSTD_DDict *ddict = NULL;
+
+	if (!dctx)
+		ereport(ERROR, (errmsg("Failed to create ZSTD decompression context")));
+
+	/*
+	 * Extract the dictionary ID from the compressed frame. This function
+	 * reads the dictionary ID from the frame header.
+	 */
+	dictid = (Oid) ZSTD_getDictID_fromFrame(VARDATA_4B_C(value), cmp_size_exhdr);
+
+	if (dictid != InvalidDictId)
+	{
+		bytea	   *dict_bytea = get_zstd_dict(dictid);
+		const void *dict_buffer = VARDATA_ANY(dict_bytea);
+		uint32		dict_size = VARSIZE_ANY(dict_bytea) - VARHDRSZ;
+
+		ddict = ZSTD_createDDict(dict_buffer, dict_size);
+		pfree(dict_bytea);
+
+		if (!ddict)
+		{
+			ZSTD_freeDCtx(dctx);
+			ereport(ERROR, (errmsg("Failed to create ZSTD compression dictionary")));
+		}
+
+		ret = ZSTD_DCtx_refDDict(dctx, ddict);
+		if (ZSTD_isError(ret))
+		{
+			ZSTD_freeDDict(ddict);
+			ZSTD_freeDCtx(dctx);
+			ereport(ERROR, (errmsg("Failed to reference ZSTD dictionary: %s", ZSTD_getErrorName(ret))));
+		}
+	}
+
+	/* Allocate space for the uncompressed data */
+	result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+	uncmp_size = ZSTD_decompressDCtx(dctx,
+									 VARDATA(result),
+									 actual_size_exhdr,
+									 VARDATA_4B_C(value),
+									 cmp_size_exhdr);
+
+	/* Cleanup */
+	ZSTD_freeDDict(ddict);
+	ZSTD_freeDCtx(dctx);
+
+	if (ZSTD_isError(uncmp_size))
+	{
+		pfree(result);
+		ereport(ERROR, (errmsg("ZSTD decompression failed: %s", ZSTD_getErrorName(uncmp_size))));
+	}
+
+	/* Set final size in the varlena header */
+	SET_VARSIZE(result, uncmp_size + VARHDRSZ);
+	return result;
+
+#else
+	NO_METHOD_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum using the streaming API and optional dictionary */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	ZSTD_DCtx  *dctx = ZSTD_createDCtx();
+	ZSTD_DDict *ddict = NULL;
+	Oid			dictid;
+	uint32		cmp_size_exhdr = VARSIZE_4B(value) - VARHDRSZ_COMPRESSED_EXT;
+	size_t		ret;
+
+	if (dctx == NULL)
+		elog(ERROR, "could not create zstd decompression context");
+
+	/* Extract the dictionary ID from the compressed frame */
+	dictid = (Oid) ZSTD_getDictID_fromFrame(VARDATA_4B_C(value), cmp_size_exhdr);
+
+	if (dictid != InvalidDictId)
+	{
+		bytea	   *dict_bytea = get_zstd_dict(dictid);
+		const void *dict_buffer = VARDATA_ANY(dict_bytea);
+		uint32		dict_size = VARSIZE_ANY(dict_bytea) - VARHDRSZ;
+
+		/* Create and bind the dictionary to the decompression context */
+		ddict = ZSTD_createDDict(dict_buffer, dict_size);
+		pfree(dict_bytea);
+
+		if (!ddict)
+		{
+			ZSTD_freeDCtx(dctx);
+			ereport(ERROR, (errmsg("Failed to create ZSTD compression dictionary")));
+		}
+
+		ret = ZSTD_DCtx_refDDict(dctx, ddict);
+		if (ZSTD_isError(ret))
+		{
+			ZSTD_freeDDict(ddict);
+			ZSTD_freeDCtx(dctx);
+			ereport(ERROR, (errmsg("Failed to reference ZSTD dictionary: %s", ZSTD_getErrorName(ret))));
+		}
+	}
+
+	inBuf.src = (char *) value + VARHDRSZ_COMPRESSED_EXT;
+	inBuf.size = VARSIZE(value) - VARHDRSZ_COMPRESSED_EXT;
+	inBuf.pos = 0;
+
+	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+	outBuf.dst = (char *) result + VARHDRSZ;
+	outBuf.size = slicelength;
+	outBuf.pos = 0;
+
+	/* Common decompression loop */
+	while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+	{
+		ret = ZSTD_decompressStream(dctx, &outBuf, &inBuf);
+		if (ZSTD_isError(ret))
+		{
+			pfree(result);
+			ZSTD_freeDDict(ddict);
+			ZSTD_freeDCtx(dctx);
+			elog(ERROR, "zstd decompression failed: %s", ZSTD_getErrorName(ret));
+		}
+	}
+
+	/* Cleanup */
+	ZSTD_freeDDict(ddict);
+	ZSTD_freeDCtx(dctx);
+
+	Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+	SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+	return result;
+#else
+	NO_METHOD_SUPPORT("zstd");
+	return NULL;
+#endif
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346c..c8a03a6aab 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -43,7 +43,7 @@ static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
  * ----------
  */
 Datum
-toast_compress_datum(Datum value, char cmethod)
+toast_compress_datum(Datum value, char cmethod, Oid zstd_dictid, int zstd_level)
 {
 	struct varlena *tmp = NULL;
 	int32		valsize;
@@ -71,6 +71,10 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			tmp = zstd_compress_datum((const struct varlena *) value, zstd_dictid, zstd_level);
+			cmid = TOAST_ZSTD_COMPRESSION_ID;
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", cmethod);
 	}
@@ -176,6 +180,7 @@ toast_save_datum(Relation rel, Datum value,
 		data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
 		toast_pointer.va_rawsize = data_todo + VARHDRSZ;	/* as if not short */
 		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.va_cmp_alg = 0;
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
@@ -196,6 +201,7 @@ toast_save_datum(Relation rel, Datum value,
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		toast_pointer.va_rawsize = VARSIZE(dval);
 		toast_pointer.va_extinfo = data_todo;
+		toast_pointer.va_cmp_alg = 0;
 	}
 
 	/*
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d..bba90097d3 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -19,7 +19,8 @@
 #include "access/toast_internals.h"
 #include "catalog/pg_type_d.h"
 #include "varatt.h"
-
+#include "utils/attoptcache.h"
+#include "access/toast_compression.h"
 
 /*
  * Prepare to TOAST a tuple.
@@ -55,6 +56,18 @@ toast_tuple_init(ToastTupleContext *ttc)
 		ttc->ttc_attr[i].tai_colflags = 0;
 		ttc->ttc_attr[i].tai_oldexternal = NULL;
 		ttc->ttc_attr[i].tai_compression = att->attcompression;
+		ttc->ttc_attr[i].zstd_dictid = InvalidDictId;
+		ttc->ttc_attr[i].zstd_level = DEFAULT_ZSTD_CMP_LEVEL;
+		if (att->attcompression == TOAST_ZSTD_COMPRESSION)
+		{
+			AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+			if (aopt)
+			{
+				ttc->ttc_attr[i].zstd_dictid = (Oid) aopt->zstd_dictid;
+				ttc->ttc_attr[i].zstd_level = aopt->zstd_cmp_level;
+			}
+		}
 
 		if (ttc->ttc_oldvalues != NULL)
 		{
@@ -230,7 +243,7 @@ toast_tuple_try_compression(ToastTupleContext *ttc, int attribute)
 	Datum		new_value;
 	ToastAttrInfo *attr = &ttc->ttc_attr[attribute];
 
-	new_value = toast_compress_datum(*value, attr->tai_compression);
+	new_value = toast_compress_datum(*value, attr->tai_compression, attr->zstd_dictid, attr->zstd_level);
 
 	if (DatumGetPointer(new_value) != NULL)
 	{
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c090094ed0..282afbcef5 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -46,7 +46,8 @@ OBJS = \
 	pg_subscription.o \
 	pg_type.o \
 	storage.o \
-	toasting.o
+	toasting.o \
+	pg_zstd_dictionaries.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bf..493963b1b8 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1071,7 +1071,9 @@ AddNewRelationType(const char *typeName,
 				   -1,			/* typmod */
 				   0,			/* array dimensions for typBaseType */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* rowtypes never have a collation */
+				   InvalidOid,	/* rowtypes never have a collation */
+				   InvalidOid	/* generate dictionary procedure - default */
+		);
 }
 
 /* --------------------------------
@@ -1394,7 +1396,9 @@ heap_create_with_catalog(const char *relname,
 				   -1,			/* typmod */
 				   0,			/* array dimensions for typBaseType */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* rowtypes never have a collation */
+				   InvalidOid,	/* rowtypes never have a collation */
+				   InvalidOid	/* generate dictionary procedure - default */
+			);
 
 		pfree(relarrayname);
 	}
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 1958ea9238..8f0413189c 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -34,6 +34,7 @@ backend_sources += files(
   'pg_type.c',
   'storage.c',
   'toasting.c',
+  'pg_zstd_dictionaries.c',
 )
 
 
diff --git a/src/backend/catalog/pg_type.c b/src/backend/catalog/pg_type.c
index b36f81afb9..bbed8f64ad 100644
--- a/src/backend/catalog/pg_type.c
+++ b/src/backend/catalog/pg_type.c
@@ -120,6 +120,7 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId)
 	values[Anum_pg_type_typtypmod - 1] = Int32GetDatum(-1);
 	values[Anum_pg_type_typndims - 1] = Int32GetDatum(0);
 	values[Anum_pg_type_typcollation - 1] = ObjectIdGetDatum(InvalidOid);
+	values[Anum_pg_type_typebuildzstddictionary - 1] = ObjectIdGetDatum(InvalidOid);
 	nulls[Anum_pg_type_typdefaultbin - 1] = true;
 	nulls[Anum_pg_type_typdefault - 1] = true;
 	nulls[Anum_pg_type_typacl - 1] = true;
@@ -223,7 +224,8 @@ TypeCreate(Oid newTypeOid,
 		   int32 typeMod,
 		   int32 typNDims,		/* Array dimensions for baseType */
 		   bool typeNotNull,
-		   Oid typeCollation)
+		   Oid typeCollation,
+		   Oid generateDictionaryProcedure)
 {
 	Relation	pg_type_desc;
 	Oid			typeObjectId;
@@ -378,6 +380,7 @@ TypeCreate(Oid newTypeOid,
 	values[Anum_pg_type_typtypmod - 1] = Int32GetDatum(typeMod);
 	values[Anum_pg_type_typndims - 1] = Int32GetDatum(typNDims);
 	values[Anum_pg_type_typcollation - 1] = ObjectIdGetDatum(typeCollation);
+	values[Anum_pg_type_typebuildzstddictionary - 1] = ObjectIdGetDatum(generateDictionaryProcedure);
 
 	/*
 	 * initialize the default binary value for this type.  Check for nulls of
@@ -679,6 +682,12 @@ GenerateTypeDependencies(HeapTuple typeTuple,
 		add_exact_object_address(&referenced, addrs_normal);
 	}
 
+	if (OidIsValid(typeForm->typebuildzstddictionary))
+	{
+		ObjectAddressSet(referenced, ProcedureRelationId, typeForm->typebuildzstddictionary);
+		add_exact_object_address(&referenced, addrs_normal);
+	}
+
 	if (OidIsValid(typeForm->typsubscript))
 	{
 		ObjectAddressSet(referenced, ProcedureRelationId, typeForm->typsubscript);
diff --git a/src/backend/catalog/pg_zstd_dictionaries.c b/src/backend/catalog/pg_zstd_dictionaries.c
new file mode 100644
index 0000000000..f52f9decb9
--- /dev/null
+++ b/src/backend/catalog/pg_zstd_dictionaries.c
@@ -0,0 +1,601 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "access/heapam.h"
+#include "access/table.h"
+#include "access/relation.h"
+#include "access/tableam.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class_d.h"
+#include "catalog/pg_zstd_dictionaries.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
+#include "catalog/pg_type.h"
+#include "catalog/namespace.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/hsearch.h"
+#include "access/toast_compression.h"
+#include "utils/attoptcache.h"
+#include "parser/analyze.h"
+#include "common/hashfn.h"
+#include "nodes/makefuncs.h"
+#include "access/reloptions.h"
+#include "miscadmin.h"
+#include "access/genam.h"
+#include "executor/tuptable.h"
+#include "access/htup_details.h"
+#include "access/sdir.h"
+#include "utils/lsyscache.h"
+#include "utils/relcache.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
+#include "nodes/pg_list.h"
+
+#ifdef USE_ZSTD
+#include <zstd.h>
+#include <zdict.h>
+#endif
+
+#define TARG_ROWS 1000
+
+typedef struct SampleEntry SampleEntry;
+typedef struct SampleCollector SampleCollector;
+
+/* Structure to store a sample entry */
+struct SampleEntry
+{
+	void	   *data;			/* Pointer to sample data */
+	size_t		size;			/* Size of the sample */
+};
+
+/* Structure to collect samples along with a hash table for deduplication */
+struct SampleCollector
+{
+	SampleEntry *samples;		/* Dynamic array of pointers to SampleEntry */
+	int			sample_count;	/* Number of collected samples */
+};
+
+static bool build_zstd_dictionary_internal(Oid relid, AttrNumber attno);
+static Oid	GetNewDictId(Relation relation, Oid indexId, AttrNumber dictIdColumn);
+
+/* ----------------------------------------------------------------
+ * Zstandard dictionary training related methods
+ * ----------------------------------------------------------------
+ */
+
+/*
+ * build_zstd_dictionary_internal
+ *   1) Validate that the given (relid, attno) can have a Zstd compression enabled on heap relation
+ *   2) Call the type-specific dictionary builder
+ *   3) Train a dictionary via ZDICT_trainFromBuffer()
+ *   4) Insert dictionary into pg_zstd_dictionaries
+ *   5) Update pg_attribute.attoptions with dictid
+ */
+pg_attribute_unused()
+static bool
+build_zstd_dictionary_internal(Oid relid, AttrNumber attno)
+{
+#ifdef USE_ZSTD
+	Relation	catalogRel;
+	TupleDesc	catTupDesc;
+	Oid			dictid;
+	Relation	rel;
+	TupleDesc	tupleDesc;
+	Form_pg_attribute att;
+	AttributeOpts *attopt;
+	HeapTuple	typeTup;
+	Form_pg_type typeForm;
+	Oid			baseTypeOid;
+	Oid			train_func;
+	Datum		dictDatum;
+	ZstdTrainingData *dict;
+	char	   *samples_buffer;
+	size_t	   *sample_sizes;
+	int			nitems;
+	uint32		dictionary_size;
+	void	   *dict_data;
+	size_t		dict_size;
+
+	/* ----
+     * 1) Open user relation just to verify it's a normal table and has Zstd compression
+     * ----
+     */
+	rel = table_open(relid, AccessShareLock);
+	if (rel->rd_rel->relkind != RELKIND_RELATION)
+	{
+		table_close(rel, AccessShareLock);
+		return false;			/* not a regular table */
+	}
+
+	/* If the column doesn't use Zstd, nothing to do */
+	tupleDesc = RelationGetDescr(rel);
+	att = TupleDescAttr(tupleDesc, attno - 1);
+	if (att->attcompression != TOAST_ZSTD_COMPRESSION)
+	{
+		table_close(rel, AccessShareLock);
+		return false;
+	}
+
+	/* Check attoptions for user-requested dictionary size, etc. */
+	attopt = get_attribute_options(relid, attno);
+	if (attopt && attopt->zstd_dict_size == 0)
+	{
+		/* user explicitly says "no dictionary needed" */
+		table_close(rel, AccessShareLock);
+		return false;
+	}
+
+	/*
+	 * 2) Look up the type's custom dictionary builder function We'll call it
+	 * to get sample data. Then we can close 'rel' because we don't need it
+	 * open to do the actual Zdict training.
+	 */
+	typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(att->atttypid));
+	if (!HeapTupleIsValid(typeTup))
+	{
+		table_close(rel, AccessShareLock);
+		elog(ERROR, "cache lookup failed for type %u", att->atttypid);
+	}
+	typeForm = (Form_pg_type) GETSTRUCT(typeTup);
+
+	if (typeForm->typlen != -1)
+	{
+		ReleaseSysCache(typeTup);
+		table_close(rel, AccessShareLock);
+		return false;
+	}
+
+	/* Get the base type */
+	baseTypeOid = get_element_type(typeForm->oid);
+	train_func = InvalidOid;
+
+	if (OidIsValid(baseTypeOid))
+	{
+		HeapTuple	baseTypeTup;
+		Form_pg_type baseTypeForm;
+
+		/* It's an array type: get the base type's training function */
+		baseTypeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(baseTypeOid));
+		if (!HeapTupleIsValid(baseTypeTup))
+			ereport(ERROR,
+					(errmsg("Cache lookup failed for base type %u", baseTypeOid)));
+
+		baseTypeForm = (Form_pg_type) GETSTRUCT(baseTypeTup);
+		train_func = baseTypeForm->typebuildzstddictionary;
+		ReleaseSysCache(baseTypeTup);
+	}
+	else
+		train_func = typeForm->typebuildzstddictionary;
+
+	/* If the type does not supply a builder, skip */
+	if (!OidIsValid(train_func))
+	{
+		ReleaseSysCache(typeTup);
+		table_close(rel, AccessShareLock);
+		return false;
+	}
+
+	/* Call the type-specific builder. It should return ZstdTrainingData */
+	dictDatum = OidFunctionCall2(train_func,
+								 PointerGetDatum(rel),	/* pass relation ref */
+								 PointerGetDatum(att));
+	ReleaseSysCache(typeTup);
+
+	/* We no longer need the user relation open */
+	table_close(rel, AccessShareLock);
+
+	dict = (ZstdTrainingData *) DatumGetPointer(dictDatum);
+	if (!dict || dict->nitems == 0)
+		return false;
+
+	/*
+	 * 3) Train a Zstd dictionary in-memory.
+	 */
+	samples_buffer = dict->sample_buffer;
+	sample_sizes = dict->sample_sizes;
+	nitems = dict->nitems;
+
+	dictionary_size = (!attopt ? DEFAULT_ZSTD_DICT_SIZE
+					   : attopt->zstd_dict_size);
+
+	/* Allocate buffer for dictionary training result */
+	dict_data = palloc(dictionary_size);
+	dict_size = ZDICT_trainFromBuffer(dict_data,
+									  dictionary_size,
+									  samples_buffer,
+									  sample_sizes,
+									  nitems);
+	if (ZDICT_isError(dict_size))
+	{
+		elog(LOG, "Zstd dictionary training failed: %s",
+			 ZDICT_getErrorName(dict_size));
+		pfree(dict_data);
+		return false;
+	}
+
+	/*
+	 * Finalize dictionary to embed a custom dictID. E.g. We can get a new Oid
+	 * from pg_zstd_dictionaries here *before* we build the bytea. But for
+	 * brevity, let's do it after opening pg_zstd_dictionaries (so we can do
+	 * the dictionary insertion + ID assignment in one place).
+	 *
+	 * 4) Insert dictionary into pg_zstd_dictionaries We do that by opening
+	 * the ZstdDictionariesRelation, generating a new dictid, forming a tuple,
+	 * and inserting it.
+	 *
+	 * finalize to embed that 'dictid' in the dictionary itself
+	 */
+	{
+		ZDICT_params_t fParams;
+		size_t		final_dict_size;
+
+		/* Open the catalog relation with ShareRowExclusiveLock */
+		catalogRel = table_open(ZstdDictionariesRelationId, ShareRowExclusiveLock);
+		catTupDesc = RelationGetDescr(catalogRel);
+		dictid = GetNewDictId(catalogRel, ZstdDictidIndexId, Anum_pg_zstd_dictionaries_dictid);
+
+		memset(&fParams, 0, sizeof(fParams));
+		fParams.dictID = dictid;	/* embed the newly allocated Oid as the
+									 * dictID */
+
+		final_dict_size = ZDICT_finalizeDictionary(
+												   dict_data,	/* output buffer (reuse) */
+												   dictionary_size, /* capacity */
+												   dict_data,	/* input dictionary from
+																 * train step */
+												   dict_size,	/* size from train step */
+												   samples_buffer,
+												   sample_sizes,
+												   nitems,
+												   fParams);
+
+		/* Verify that the embedded dictionary ID matches the expected value */
+		if (dictid != (Oid) ZDICT_getDictID(dict_data, final_dict_size))
+			elog(ERROR, "Zstd dictionary ID mismatch");
+
+		if (ZDICT_isError(final_dict_size))
+		{
+			elog(LOG, "Zstd dictionary finalization failed: %s",
+				 ZDICT_getErrorName(final_dict_size));
+			pfree(dict_data);
+			table_close(catalogRel, ShareRowExclusiveLock);
+			return false;
+		}
+
+		/* Now copy that finalized dictionary into a bytea. */
+		{
+			/* We’ll store this bytea in pg_zstd_dictionaries. */
+			Datum		values[Natts_pg_zstd_dictionaries];
+			bool		nulls[Natts_pg_zstd_dictionaries];
+			HeapTuple	tup;
+
+			bytea	   *dict_bytea = (bytea *) palloc(VARHDRSZ + final_dict_size);
+
+			SET_VARSIZE(dict_bytea, VARHDRSZ + final_dict_size);
+			memcpy(VARDATA(dict_bytea), dict_data, final_dict_size);
+
+			MemSet(values, 0, sizeof(values));
+			MemSet(nulls, false, sizeof(nulls));
+
+			values[Anum_pg_zstd_dictionaries_dictid - 1] = ObjectIdGetDatum(dictid);
+			values[Anum_pg_zstd_dictionaries_dict - 1] = PointerGetDatum(dict_bytea);
+
+			tup = heap_form_tuple(catTupDesc, values, nulls);
+			CatalogTupleInsert(catalogRel, tup);
+			heap_freetuple(tup);
+
+			pfree(dict_bytea);
+		}
+
+		pfree(dict_data);
+	}
+
+	pfree(samples_buffer);
+	pfree(sample_sizes);
+	pfree(dict);
+
+	/*
+	 * 5) Update pg_attribute.attoptions with "zstd_dictid" => dictid so the
+	 * column knows which dictionary to use at compression time.
+	 */
+	{
+		Relation	attRel = table_open(AttributeRelationId, RowExclusiveLock);
+		HeapTuple	atttup,
+					newtuple;
+		Datum		attoptionsDatum,
+					newOptions;
+		bool		isnull;
+		Datum		repl_val[Natts_pg_attribute];
+		bool		repl_null[Natts_pg_attribute];
+		bool		repl_repl[Natts_pg_attribute];
+		DefElem    *def;
+
+		atttup = SearchSysCacheAttNum(relid, attno);
+		if (!HeapTupleIsValid(atttup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column number %d of relation \"%u\" does not exist",
+							attno, relid)));
+
+		/* Build new attoptions with zstd_dictid=... */
+		def = makeDefElem("zstd_dictid",
+						  (Node *) makeString(psprintf("%u", dictid)),
+						  -1);
+
+		attoptionsDatum = SysCacheGetAttr(ATTNUM, atttup,
+										  Anum_pg_attribute_attoptions,
+										  &isnull);
+		newOptions = transformRelOptions(isnull ? (Datum) 0 : attoptionsDatum,
+										 list_make1(def),
+										 NULL, NULL,
+										 false, false);
+		/* Validate them (throws error if invalid) */
+		(void) attribute_reloptions(newOptions, true);
+
+		MemSet(repl_null, false, sizeof(repl_null));
+		MemSet(repl_repl, false, sizeof(repl_repl));
+
+		if (newOptions != (Datum) 0)
+			repl_val[Anum_pg_attribute_attoptions - 1] = newOptions;
+		else
+			repl_null[Anum_pg_attribute_attoptions - 1] = true;
+
+		repl_repl[Anum_pg_attribute_attoptions - 1] = true;
+
+		newtuple = heap_modify_tuple(atttup,
+									 RelationGetDescr(attRel),
+									 repl_val,
+									 repl_null,
+									 repl_repl);
+
+		CatalogTupleUpdate(attRel, &newtuple->t_self, newtuple);
+		heap_freetuple(newtuple);
+
+		ReleaseSysCache(atttup);
+
+		table_close(attRel, NoLock);
+	}
+
+	/**
+     * Done inserting dictionary and updating attribute.
+     * Unlock the table (locks remain held until transaction commit)
+     */
+	table_close(catalogRel, NoLock);
+
+	return true;
+#else
+	return false;
+#endif
+}
+
+/*
+ * Acquire a new unique DictId for a relation.
+ *
+ * Assumes the relation is already locked with ShareRowExclusiveLock,
+ * ensuring that concurrent transactions cannot generate duplicate DictIds.
+ */
+pg_attribute_unused()
+static Oid
+GetNewDictId(Relation relation, Oid indexId, AttrNumber dictIdColumn)
+{
+	Relation	indexRel = index_open(indexId, AccessShareLock);
+	Oid			maxDictId = InvalidDictId;
+	SysScanDesc scan;
+	HeapTuple	tuple;
+	bool		collision;
+	ScanKeyData key;
+	Oid			newDictId;
+
+	/* Retrieve the maximum existing DictId by scanning in reverse order */
+	scan = systable_beginscan_ordered(relation, indexRel, SnapshotAny, 0, NULL);
+	tuple = systable_getnext_ordered(scan, BackwardScanDirection);
+	if (HeapTupleIsValid(tuple))
+	{
+		Datum		value;
+		bool		isNull;
+
+		value = heap_getattr(tuple, dictIdColumn, RelationGetDescr(relation), &isNull);
+		if (!isNull)
+			maxDictId = DatumGetObjectId(value);
+	}
+	systable_endscan(scan);
+
+	newDictId = maxDictId + 1;
+	Assert(newDictId != InvalidDictId);
+
+	/* Check that the new DictId is indeed unique */
+	ScanKeyInit(&key,
+				dictIdColumn,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(newDictId));
+
+	scan = systable_beginscan(relation, indexRel->rd_id, true,
+							  SnapshotAny, 1, &key);
+	collision = HeapTupleIsValid(systable_getnext(scan));
+	systable_endscan(scan);
+
+	if (collision)
+		ereport(ERROR,
+				(errcode(ERRCODE_INTERNAL_ERROR),
+				 errmsg("unexpected collision for new DictId %d", newDictId)));
+
+	return newDictId;
+}
+
+/*
+ * get_zstd_dict - Fetches the ZSTD dictionary from the catalog
+ *
+ * dictid: The Oid of the dictionary to fetch.
+ *
+ * Returns: A pointer to a bytea containing the dictionary data.
+ */
+bytea *
+get_zstd_dict(Oid dictid)
+{
+	HeapTuple	tuple;
+	Datum		datum;
+	bool		isNull;
+	bytea	   *dict_bytea;
+	bytea	   *result;
+	Size		bytea_len;
+
+	/* Fetch the dictionary tuple from the syscache */
+	tuple = SearchSysCache1(ZSTDDICTIDOID, ObjectIdGetDatum(dictid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR, (errmsg("Cache lookup failed for dictid %u", dictid)));
+
+	/* Get the dictionary attribute from the tuple */
+	datum = SysCacheGetAttr(ATTNUM, tuple, Anum_pg_zstd_dictionaries_dict, &isNull);
+	if (isNull)
+		ereport(ERROR, (errmsg("Dictionary not found for dictid %u", dictid)));
+
+	dict_bytea = DatumGetByteaP(datum);
+	if (dict_bytea == NULL)
+		ereport(ERROR, (errmsg("Failed to fetch dictionary")));
+
+	/* Determine the total size of the bytea (header + data) */
+	bytea_len = VARSIZE(dict_bytea);
+
+	result = palloc(bytea_len);
+	memcpy(result, dict_bytea, bytea_len);
+
+	/* Release the syscache tuple; the returned bytea is now independent */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * zstd_dictionary_builder
+ *    Acquire samples from a column, store them in a SampleCollector,
+ *    filter them, then build a ZstdTrainingData struct.
+ */
+Datum
+zstd_dictionary_builder(PG_FUNCTION_ARGS)
+{
+	ZstdTrainingData *dict = palloc0(sizeof(ZstdTrainingData));
+	Relation	rel = (Relation) PG_GETARG_POINTER(0);
+	Form_pg_attribute att = (Form_pg_attribute) PG_GETARG_POINTER(1);
+	TupleDesc	tupleDesc = RelationGetDescr(rel);
+
+	/* Acquire up to TARG_ROWS sample rows. */
+	HeapTuple  *sample_rows = palloc(TARG_ROWS * sizeof(HeapTuple));
+	double		totalrows = 0,
+				totaldeadrows = 0;
+	int			num_sampled = acquire_sample_rows(rel, 0, sample_rows,
+												  TARG_ROWS,
+												  &totalrows,
+												  &totaldeadrows);
+
+	/* Create a collector to accumulate raw varlena samples. */
+	size_t		filtered_sample_count = 0;
+	size_t		filtered_samples_size = 0;
+	char	   *samples_buffer;
+	size_t	   *sample_sizes;
+	size_t		current_offset = 0;
+
+	SampleCollector *collector = palloc(sizeof(SampleCollector));
+
+	collector->samples = palloc(num_sampled * sizeof(SampleEntry));
+	collector->sample_count = 0;
+
+	/* Extract column data from each sampled row. */
+	for (int i = 0; i < num_sampled; i++)
+	{
+		bool		isnull;
+		Datum		value;
+
+		CHECK_FOR_INTERRUPTS();
+
+		value = heap_getattr(sample_rows[i],
+							 att->attnum,
+							 tupleDesc,
+							 &isnull);
+		if (!isnull)
+		{
+			struct varlena *attr;
+			size_t		size;
+			void	   *data;
+			SampleEntry entry;
+			int			idx;
+
+			attr = (struct varlena *) PG_DETOAST_DATUM(value);
+			size = VARSIZE_ANY_EXHDR(attr);
+
+			if (filtered_samples_size + size > MaxAllocSize)
+				break;
+
+			data = palloc(size);
+			memcpy(data, VARDATA_ANY(attr), size);
+
+			entry.data = data;
+			entry.size = size;
+
+			idx = collector->sample_count;
+			collector->samples[idx] = entry;
+			collector->sample_count++;
+
+			filtered_samples_size += size;
+			filtered_sample_count++;
+		}
+	}
+
+	if (filtered_sample_count == 0)
+	{
+		/* No samples were collected, or they were too large. */
+		PG_RETURN_POINTER(dict);
+	}
+
+	/* Allocate a buffer for all sample data, plus an array of sample sizes. */
+	samples_buffer = palloc(filtered_samples_size);
+	sample_sizes = palloc(filtered_sample_count * sizeof(size_t));
+
+	/*
+	 * Concatenate the samples into samples_buffer, recording each sample's
+	 * size in sample_sizes.
+	 */
+	current_offset = 0;
+	for (int i = 0; i < filtered_sample_count; i++)
+	{
+		memcpy(samples_buffer + current_offset,
+			   collector->samples[i].data,
+			   collector->samples[i].size);
+
+		sample_sizes[i] = collector->samples[i].size;
+		current_offset += collector->samples[i].size;
+
+		pfree(collector->samples[i].data);
+	}
+	pfree(collector->samples);
+	pfree(collector);
+
+	dict->sample_buffer = samples_buffer;
+	dict->sample_sizes = sample_sizes;
+	dict->nitems = filtered_sample_count;
+
+	PG_RETURN_POINTER(dict);
+}
+
+Datum
+build_zstd_dict_for_attribute(PG_FUNCTION_ARGS)
+{
+#ifndef USE_ZSTD
+	PG_RETURN_BOOL(false);
+#else
+	text	   *tablename = PG_GETARG_TEXT_PP(0);
+	RangeVar   *tablerel;
+	Oid			tableoid = InvalidOid;
+	AttrNumber	attno = PG_GETARG_INT32(1);
+	bool		success;
+
+	/* Look up table name. */
+	tablerel = makeRangeVarFromNameList(textToQualifiedNameList(tablename));
+	tableoid = RangeVarGetRelid(tablerel, NoLock, false);
+	success = build_zstd_dictionary_internal(tableoid, attno);
+	PG_RETURN_BOOL(success);
+#endif
+}
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 2b5fbdcbd8..2b5500f45f 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -55,7 +55,7 @@
 #include "utils/sortsupport.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
-
+#include "parser/analyze.h"
 
 /* Per-index data for ANALYZE */
 typedef struct AnlIndexData
@@ -85,9 +85,6 @@ static void compute_index_stats(Relation onerel, double totalrows,
 								MemoryContext col_context);
 static VacAttrStats *examine_attribute(Relation onerel, int attnum,
 									   Node *index_expr);
-static int	acquire_sample_rows(Relation onerel, int elevel,
-								HeapTuple *rows, int targrows,
-								double *totalrows, double *totaldeadrows);
 static int	compare_rows(const void *a, const void *b, void *arg);
 static int	acquire_inherited_sample_rows(Relation onerel, int elevel,
 										  HeapTuple *rows, int targrows,
@@ -1195,7 +1192,7 @@ block_sampling_read_stream_next(ReadStream *stream,
  * block.  The previous sampling method put too much credence in the row
  * density near the start of the table.
  */
-static int
+int
 acquire_sample_rows(Relation onerel, int elevel,
 					HeapTuple *rows, int targrows,
 					double *totalrows, double *totaldeadrows)
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 3cb3ca1cca..c583e48167 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -95,6 +95,7 @@ typedef struct
 	bool		updateTypmodout;
 	bool		updateAnalyze;
 	bool		updateSubscript;
+	bool		updateGenerateDictionary;
 	/* New values for relevant attributes */
 	char		storage;
 	Oid			receiveOid;
@@ -103,6 +104,7 @@ typedef struct
 	Oid			typmodoutOid;
 	Oid			analyzeOid;
 	Oid			subscriptOid;
+	Oid			buildZstdDictionary;
 } AlterTypeRecurseParams;
 
 /* Potentially set by pg_upgrade_support functions */
@@ -122,6 +124,7 @@ static Oid	findTypeSendFunction(List *procname, Oid typeOid);
 static Oid	findTypeTypmodinFunction(List *procname);
 static Oid	findTypeTypmodoutFunction(List *procname);
 static Oid	findTypeAnalyzeFunction(List *procname, Oid typeOid);
+static Oid	findTypeGenerateDictionaryFunction(List *procname, Oid typeOid);
 static Oid	findTypeSubscriptingFunction(List *procname, Oid typeOid);
 static Oid	findRangeSubOpclass(List *opcname, Oid subtype);
 static Oid	findRangeCanonicalFunction(List *procname, Oid typeOid);
@@ -162,6 +165,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	List	   *typmodoutName = NIL;
 	List	   *analyzeName = NIL;
 	List	   *subscriptName = NIL;
+	List	   *generateDictionaryName = NIL;
 	char		category = TYPCATEGORY_USER;
 	bool		preferred = false;
 	char		delimiter = DEFAULT_TYPDELIM;
@@ -190,6 +194,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	DefElem    *alignmentEl = NULL;
 	DefElem    *storageEl = NULL;
 	DefElem    *collatableEl = NULL;
+	DefElem    *generateDictionaryEl = NULL;
 	Oid			inputOid;
 	Oid			outputOid;
 	Oid			receiveOid = InvalidOid;
@@ -198,6 +203,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	Oid			typmodoutOid = InvalidOid;
 	Oid			analyzeOid = InvalidOid;
 	Oid			subscriptOid = InvalidOid;
+	Oid			buildZstdDictionary = InvalidOid;
 	char	   *array_type;
 	Oid			array_oid;
 	Oid			typoid;
@@ -323,6 +329,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 			defelp = &storageEl;
 		else if (strcmp(defel->defname, "collatable") == 0)
 			defelp = &collatableEl;
+		else if (strcmp(defel->defname, "build_zstd_dict") == 0)
+			defelp = &generateDictionaryEl;
 		else
 		{
 			/* WARNING, not ERROR, for historical backwards-compatibility */
@@ -455,6 +463,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	}
 	if (collatableEl)
 		collation = defGetBoolean(collatableEl) ? DEFAULT_COLLATION_OID : InvalidOid;
+	if (generateDictionaryEl)
+		generateDictionaryName = defGetQualifiedName(generateDictionaryEl);
 
 	/*
 	 * make sure we have our required definitions
@@ -516,6 +526,15 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 					 errmsg("element type cannot be specified without a subscripting function")));
 	}
 
+	if (generateDictionaryName)
+	{
+		if (internalLength != -1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("type build_zstd_dict function must be specified only if data type is variable length.")));
+		buildZstdDictionary = findTypeGenerateDictionaryFunction(generateDictionaryName, typoid);
+	}
+
 	/*
 	 * Check permissions on functions.  We choose to require the creator/owner
 	 * of a type to also own the underlying functions.  Since creating a type
@@ -550,6 +569,9 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	if (analyzeOid && !object_ownercheck(ProcedureRelationId, analyzeOid, GetUserId()))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
 					   NameListToString(analyzeName));
+	if (buildZstdDictionary && !object_ownercheck(ProcedureRelationId, buildZstdDictionary, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
+					   NameListToString(generateDictionaryName));
 	if (subscriptOid && !object_ownercheck(ProcedureRelationId, subscriptOid, GetUserId()))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
 					   NameListToString(subscriptName));
@@ -601,7 +623,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array Dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   collation);	/* type's collation */
+				   collation,	/* type's collation */
+				   buildZstdDictionary);	/* build_zstd_dict procedure */
 	Assert(typoid == address.objectId);
 
 	/*
@@ -643,7 +666,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   collation);		/* type's collation */
+			   collation,		/* type's collation */
+			   InvalidOid);		/* build_zstd_dict procedure */
 
 	pfree(array_type);
 
@@ -706,6 +730,7 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 	Oid			receiveProcedure;
 	Oid			sendProcedure;
 	Oid			analyzeProcedure;
+	Oid			buildZstdDictionary;
 	bool		byValue;
 	char		category;
 	char		delimiter;
@@ -842,6 +867,9 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 	/* Analysis function */
 	analyzeProcedure = baseType->typanalyze;
 
+	/* Generate dictionary function */
+	buildZstdDictionary = baseType->typebuildzstddictionary;
+
 	/*
 	 * Domains don't need a subscript function, since they are not
 	 * subscriptable on their own.  If the base type is subscriptable, the
@@ -1078,7 +1106,8 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 				   basetypeMod, /* typeMod value */
 				   typNDims,	/* Array dimensions for base type */
 				   typNotNull,	/* Type NOT NULL */
-				   domaincoll); /* type's collation */
+				   domaincoll,	/* type's collation */
+				   buildZstdDictionary);	/* build_zstd_dict procedure */
 
 	/*
 	 * Create the array type that goes with it.
@@ -1119,7 +1148,8 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   domaincoll);		/* type's collation */
+			   domaincoll,		/* type's collation */
+			   InvalidOid);		/* build_zstd_dict procedure */
 
 	pfree(domainArrayName);
 
@@ -1241,7 +1271,8 @@ DefineEnum(CreateEnumStmt *stmt)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* type's collation */
+				   InvalidOid,	/* type's collation */
+				   InvalidOid); /* generate dictionary procedure - default */
 
 	/* Enter the enum's values into pg_enum */
 	EnumValuesCreate(enumTypeAddr.objectId, stmt->vals);
@@ -1282,7 +1313,8 @@ DefineEnum(CreateEnumStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   InvalidOid);		/* type's collation */
+			   InvalidOid,		/* type's collation */
+			   InvalidOid);		/* generate dictionary procedure - default */
 
 	pfree(enumArrayName);
 
@@ -1583,7 +1615,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* type's collation (ranges never have one) */
+				   InvalidOid,	/* type's collation (ranges never have one) */
+				   InvalidOid); /* generate dictionary procedure - default */
 	Assert(typoid == InvalidOid || typoid == address.objectId);
 	typoid = address.objectId;
 
@@ -1650,7 +1683,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* type's collation (ranges never have one) */
+				   InvalidOid,	/* type's collation (ranges never have one) */
+				   InvalidOid); /* generate dictionary procedure - default */
 	Assert(multirangeOid == mltrngaddress.objectId);
 
 	/* Create the entry in pg_range */
@@ -1693,7 +1727,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   InvalidOid);		/* typcollation */
+			   InvalidOid,		/* typcollation */
+			   InvalidOid);		/* generate dictionary procedure - default */
 
 	pfree(rangeArrayName);
 
@@ -1732,7 +1767,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   InvalidOid);		/* typcollation */
+			   InvalidOid,		/* typcollation */
+			   InvalidOid);		/* generate dictionary procedure - default */
 
 	/* And create the constructor functions for this range type */
 	makeRangeConstructors(typeName, typeNamespace, typoid, rangeSubtype);
@@ -2257,6 +2293,31 @@ findTypeAnalyzeFunction(List *procname, Oid typeOid)
 	return procOid;
 }
 
+static Oid
+findTypeGenerateDictionaryFunction(List *procname, Oid typeOid)
+{
+	Oid			argList[2];
+	Oid			procOid;
+
+	argList[0] = OIDOID;
+	argList[1] = INT4OID;
+
+	procOid = LookupFuncName(procname, 2, argList, true);
+	if (!OidIsValid(procOid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("function %s does not exist",
+						func_signature_string(procname, 1, NIL, argList))));
+
+	if (get_func_rettype(procOid) != INTERNALOID)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("type build zstd dictionary function %s must return type %s",
+						NameListToString(procname), "internal")));
+
+	return procOid;
+}
+
 static Oid
 findTypeSubscriptingFunction(List *procname, Oid typeOid)
 {
@@ -4440,6 +4501,19 @@ AlterType(AlterTypeStmt *stmt)
 			/* Replacing a subscript function requires superuser. */
 			requireSuper = true;
 		}
+		else if (strcmp(defel->defname, "build_zstd_dict") == 0)
+		{
+			if (defel->arg != NULL)
+				atparams.buildZstdDictionary =
+					findTypeGenerateDictionaryFunction(defGetQualifiedName(defel),
+													   typeOid);
+			else
+				atparams.buildZstdDictionary = InvalidOid;	/* NONE, remove function */
+
+			atparams.updateGenerateDictionary = true;
+			/* Replacing a canonical function requires superuser. */
+			requireSuper = true;
+		}
 
 		/*
 		 * The rest of the options that CREATE accepts cannot be changed.
@@ -4602,6 +4676,11 @@ AlterTypeRecurse(Oid typeOid, bool isImplicitArray,
 		replaces[Anum_pg_type_typsubscript - 1] = true;
 		values[Anum_pg_type_typsubscript - 1] = ObjectIdGetDatum(atparams->subscriptOid);
 	}
+	if (atparams->updateGenerateDictionary)
+	{
+		replaces[Anum_pg_type_typebuildzstddictionary - 1] = true;
+		values[Anum_pg_type_typebuildzstddictionary - 1] = ObjectIdGetDatum(atparams->buildZstdDictionary);
+	}
 
 	newtup = heap_modify_tuple(tup, RelationGetDescr(catalog),
 							   values, nulls, replaces);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index e455657170..40e29452c8 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5184,6 +5184,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_COMPRESSION_ID:
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index ad25cbb39c..e03ac8dddc 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -453,6 +453,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd", TOAST_ZSTD_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2d1de9c37b..47773e2919 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -731,7 +731,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/src/bin/pg_amcheck/t/004_verify_heapam.pl b/src/bin/pg_amcheck/t/004_verify_heapam.pl
index 2a3af2666f..3030998e58 100644
--- a/src/bin/pg_amcheck/t/004_verify_heapam.pl
+++ b/src/bin/pg_amcheck/t/004_verify_heapam.pl
@@ -75,7 +75,7 @@ use Test::More;
 #    xx                     t_bits: x			offset = 23		C
 #    xx xx xx xx xx xx xx xx   'a': xxxxxxxx	offset = 24		LL
 #    xx xx xx xx xx xx xx xx   'b': xxxxxxxx	offset = 32		CCCCCCCC
-#    xx xx xx xx xx xx xx xx   'c': xxxxxxxx	offset = 40		CCllLL
+#    xx xx xx xx xx xx xx xx   'c': xxxxxxxx	offset = 40		CCllLLL
 #    xx xx xx xx xx xx xx xx      : xxxxxxxx	 ...continued
 #    xx xx                        : xx			 ...continued
 #
@@ -83,8 +83,8 @@ use Test::More;
 # it is convenient enough to do it this way.  We define packing code
 # constants here, where they can be compared easily against the layout.
 
-use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLL';
-use constant HEAPTUPLE_PACK_LENGTH => 58;    # Total size
+use constant HEAPTUPLE_PACK_CODE => 'LLLSSSSSCCLLCCCCCCCCCCllLLL';
+use constant HEAPTUPLE_PACK_LENGTH => 62;    # Total size
 
 # Read a tuple of our table from a heap page.
 #
@@ -130,7 +130,8 @@ sub read_tuple
 		c_va_rawsize => shift,
 		c_va_extinfo => shift,
 		c_va_valueid => shift,
-		c_va_toastrelid => shift);
+		c_va_toastrelid => shift,
+		c_va_cmp_alg => shift);
 	# Stitch together the text for column 'b'
 	$tup{b} = join('', map { chr($tup{"b_body$_"}) } (1 .. 7));
 	return \%tup;
@@ -163,7 +164,8 @@ sub write_tuple
 		$tup->{b_body6}, $tup->{b_body7},
 		$tup->{c_va_header}, $tup->{c_va_vartag},
 		$tup->{c_va_rawsize}, $tup->{c_va_extinfo},
-		$tup->{c_va_valueid}, $tup->{c_va_toastrelid});
+		$tup->{c_va_valueid}, $tup->{c_va_toastrelid}, 
+		$tup->{c_va_cmp_alg});
 	sysseek($fh, $offset, 0)
 	  or BAIL_OUT("sysseek failed: $!");
 	defined(syswrite($fh, $buffer, HEAPTUPLE_PACK_LENGTH))
@@ -496,7 +498,7 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
 		$tup->{t_hoff} += 128;
 
 		push @expected,
-		  qr/${$header}data begins at offset 152 beyond the tuple length 58/,
+		  qr/${$header}data begins at offset 152 beyond the tuple length 62/,
 		  qr/${$header}tuple data should begin at byte 24, but actually begins at byte 152 \(3 attributes, no nulls\)/;
 	}
 	elsif ($offnum == 6)
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4f4ad2ee15..02bec765ad 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8965,7 +8965,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
 						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
-						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
+						 "array_to_string(ARRAY(SELECT x FROM unnest(a.attoptions) AS x \n"
+						 "WHERE x NOT LIKE 'zstd_dictid=%'), ', ') AS attoptions, \n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
 						 "pg_catalog.array_to_string(ARRAY("
@@ -11784,12 +11785,14 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 	char	   *typmodout;
 	char	   *typanalyze;
 	char	   *typsubscript;
+	char	   *typebuildzstddictionary;
 	Oid			typreceiveoid;
 	Oid			typsendoid;
 	Oid			typmodinoid;
 	Oid			typmodoutoid;
 	Oid			typanalyzeoid;
 	Oid			typsubscriptoid;
+	Oid			typebuildzstddictionaryoid;
 	char	   *typcategory;
 	char	   *typispreferred;
 	char	   *typdelim;
@@ -11822,10 +11825,18 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 		if (fout->remoteVersion >= 140000)
 			appendPQExpBufferStr(query,
 								 "typsubscript, "
-								 "typsubscript::pg_catalog.oid AS typsubscriptoid ");
+								 "typsubscript::pg_catalog.oid AS typsubscriptoid, ");
 		else
 			appendPQExpBufferStr(query,
-								 "'-' AS typsubscript, 0 AS typsubscriptoid ");
+								 "'-' AS typsubscript, 0 AS typsubscriptoid, ");
+
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query,
+								 "typebuildzstddictionary, "
+								 "typebuildzstddictionary::pg_catalog.oid AS typebuildzstddictionaryoid ");
+		else
+			appendPQExpBufferStr(query,
+								 "'-' AS typebuildzstddictionary, 0 AS typebuildzstddictionaryoid ");
 
 		appendPQExpBufferStr(query, "FROM pg_catalog.pg_type "
 							 "WHERE oid = $1");
@@ -11850,12 +11861,14 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 	typmodout = PQgetvalue(res, 0, PQfnumber(res, "typmodout"));
 	typanalyze = PQgetvalue(res, 0, PQfnumber(res, "typanalyze"));
 	typsubscript = PQgetvalue(res, 0, PQfnumber(res, "typsubscript"));
+	typebuildzstddictionary = PQgetvalue(res, 0, PQfnumber(res, "typebuildzstddictionary"));
 	typreceiveoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typreceiveoid")));
 	typsendoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typsendoid")));
 	typmodinoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typmodinoid")));
 	typmodoutoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typmodoutoid")));
 	typanalyzeoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typanalyzeoid")));
 	typsubscriptoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typsubscriptoid")));
+	typebuildzstddictionaryoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typebuildzstddictionaryoid")));
 	typcategory = PQgetvalue(res, 0, PQfnumber(res, "typcategory"));
 	typispreferred = PQgetvalue(res, 0, PQfnumber(res, "typispreferred"));
 	typdelim = PQgetvalue(res, 0, PQfnumber(res, "typdelim"));
@@ -11911,7 +11924,8 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 		appendPQExpBuffer(q, ",\n    TYPMOD_OUT = %s", typmodout);
 	if (OidIsValid(typanalyzeoid))
 		appendPQExpBuffer(q, ",\n    ANALYZE = %s", typanalyze);
-
+	if (OidIsValid(typebuildzstddictionaryoid))
+		appendPQExpBuffer(q, ",\n    BUILD_ZSTD_DICT = %s", typebuildzstddictionary);
 	if (strcmp(typcollatable, "t") == 0)
 		appendPQExpBufferStr(q, ",\n    COLLATABLE = true");
 
@@ -17170,6 +17184,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 					case 'l':
 						cmname = "lz4";
 						break;
+					case 'z':
+						cmname = "zstd";
+						break;
 					default:
 						cmname = NULL;
 						break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9..0ba37bb175 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2167,8 +2167,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'z' ? "zstd" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612cee..afff1f80e5 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -38,7 +38,8 @@ typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_ZSTD_COMPRESSION_ID = 2,
+	TOAST_INVALID_COMPRESSION_ID = 3
 } ToastCompressionId;
 
 /*
@@ -48,10 +49,15 @@ typedef enum ToastCompressionId
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_COMPRESSION			'z'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
 
+#define InvalidDictId						0
+#define DEFAULT_ZSTD_CMP_LEVEL				3	/* Reffered from
+												 * ZSTD_CLEVEL_DEFAULT */
+#define DEFAULT_ZSTD_DICT_SIZE 				(4 * 1024)	/* 4 KB */
 
 /* pglz compression/decompression routines */
 extern struct varlena *pglz_compress_datum(const struct varlena *value);
@@ -65,6 +71,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd compression/decompression routines */
+extern struct varlena *zstd_compress_datum(const struct varlena *value, Oid dictid, int zstd_level);
+extern struct varlena *zstd_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb..5df84e5e80 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -33,6 +33,8 @@ typedef struct
 	int32		tai_size;
 	uint8		tai_colflags;
 	char		tai_compression;
+	Oid			zstd_dictid;
+	int			zstd_level;
 } ToastAttrInfo;
 
 /*
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1..26528850ba 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -25,6 +25,7 @@ typedef struct toast_compress_header
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	uint32		tcinfo;			/* 2 bits for compression method and 30 bits
 								 * external size; see va_extinfo */
+	uint32		ext_alg;
 } toast_compress_header;
 
 /*
@@ -33,19 +34,31 @@ typedef struct toast_compress_header
  */
 #define TOAST_COMPRESS_EXTSIZE(ptr) \
 	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
-
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
-	do { \
-		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
-		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+#define TOAST_COMPRESS_METHOD(PTR)                       													  		\
+	( ((((toast_compress_header *) (PTR))->tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG ) 	\
+		? (((toast_compress_header *) (PTR))->ext_alg)     													 		\
+		: ( (((toast_compress_header *) (PTR))->tcinfo) >> VARLENA_EXTSIZE_BITS ) )
+
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) 										\
+	do { 																										\
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); 													\
+		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || 														\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID  || 													\
+				(cm_method) == TOAST_ZSTD_COMPRESSION_ID); 														\
+		/* If the compression method is less than TOAST_ZSTD_COMPRESSION_ID, don't use ext_alg */ 				\
+		if ((cm_method) < TOAST_ZSTD_COMPRESSION_ID) { 															\
+			((toast_compress_header *) (ptr))->tcinfo = 														\
+				(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); 										\
+		} else { 																								\
+			/* For compression methods after lz4, use 'VARLENA_EXTENDED_COMPRESSION_FLAG' 						\
+				in the top bits of tcinfo to indicate compression algorithm is stored in ext_alg.	*/			\
+			((toast_compress_header *) (ptr))->tcinfo = 														\
+			(len) | ((uint32)VARLENA_EXTENDED_COMPRESSION_FLAG << VARLENA_EXTSIZE_BITS); 						\
+			((toast_compress_header *) (ptr))->ext_alg = (cm_method); 											\
+		} 																										\
 	} while (0)
 
-extern Datum toast_compress_datum(Datum value, char cmethod);
+extern Datum toast_compress_datum(Datum value, char cmethod, Oid zstd_dictid, int zstd_level);
 extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 2bbc7805fe..1ecd76dd31 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -81,7 +81,8 @@ CATALOG_HEADERS := \
 	pg_publication_namespace.h \
 	pg_publication_rel.h \
 	pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h \
+	pg_zstd_dictionaries.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
 
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index f0962e17b3..d8a14432bd 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202503031
+#define CATALOG_VERSION_NO	202503051
 
 #endif
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index ec1cf467f6..e9cb6d911c 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -69,6 +69,7 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_zstd_dictionaries.h',
 ]
 
 # The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 134b3dd868..376375c055 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12450,4 +12450,14 @@
   proargtypes => 'int4',
   prosrc => 'gist_stratnum_common' },
 
+# ZSTD generate dictionary training functions
+{ oid => '9241', descr => 'ZSTD generate dictionary support',
+  proname => 'zstd_dictionary_builder', prorettype => 'internal',
+  proargtypes => 'internal internal',
+  prosrc => 'zstd_dictionary_builder' },
+
+{ oid => '9242', descr => 'Build zstd dictionaries for a column.',
+  proname => 'build_zstd_dict_for_attribute', prorettype => 'bool',
+  proargtypes => 'text int4',
+  prosrc => 'build_zstd_dict_for_attribute' },
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 6dca77e0a2..58a389a78c 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -40,7 +40,7 @@
   descr => 'variable-length string, binary values escaped',
   typname => 'bytea', typlen => '-1', typbyval => 'f', typcategory => 'U',
   typinput => 'byteain', typoutput => 'byteaout', typreceive => 'bytearecv',
-  typsend => 'byteasend', typalign => 'i', typstorage => 'x' },
+  typsend => 'byteasend', typalign => 'i', typstorage => 'x', typebuildzstddictionary => 'zstd_dictionary_builder' },
 { oid => '18', array_type_oid => '1002', descr => 'single character',
   typname => 'char', typlen => '1', typbyval => 't', typcategory => 'Z',
   typinput => 'charin', typoutput => 'charout', typreceive => 'charrecv',
@@ -83,7 +83,7 @@
   typname => 'text', typlen => '-1', typbyval => 'f', typcategory => 'S',
   typispreferred => 't', typinput => 'textin', typoutput => 'textout',
   typreceive => 'textrecv', typsend => 'textsend', typalign => 'i',
-  typstorage => 'x', typcollation => 'default' },
+  typstorage => 'x', typebuildzstddictionary => 'zstd_dictionary_builder', typcollation => 'default' },
 { oid => '26', array_type_oid => '1028',
   descr => 'object identifier(oid), maximum 4 billion',
   typname => 'oid', typlen => '4', typbyval => 't', typcategory => 'N',
@@ -446,7 +446,7 @@
   typname => 'jsonb', typlen => '-1', typbyval => 'f', typcategory => 'U',
   typsubscript => 'jsonb_subscript_handler', typinput => 'jsonb_in',
   typoutput => 'jsonb_out', typreceive => 'jsonb_recv', typsend => 'jsonb_send',
-  typalign => 'i', typstorage => 'x' },
+  typalign => 'i', typstorage => 'x', typebuildzstddictionary => 'zstd_dictionary_builder' },
 { oid => '4072', array_type_oid => '4073', descr => 'JSON path',
   typname => 'jsonpath', typlen => '-1', typbyval => 'f', typcategory => 'U',
   typinput => 'jsonpath_in', typoutput => 'jsonpath_out',
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index ff666711a5..bd82da8a88 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -227,6 +227,11 @@ CATALOG(pg_type,1247,TypeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(71,TypeRelati
 	 */
 	Oid			typcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation);
 
+	/*
+	 * Custom generate dictionary procedure for the datatype (0 selects the
+	 * default).
+	 */
+	regproc		typebuildzstddictionary BKI_DEFAULT(-) BKI_ARRAY_DEFAULT(-) BKI_LOOKUP_OPT(pg_proc);
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 
 	/*
@@ -380,7 +385,8 @@ extern ObjectAddress TypeCreate(Oid newTypeOid,
 								int32 typeMod,
 								int32 typNDims,
 								bool typeNotNull,
-								Oid typeCollation);
+								Oid typeCollation,
+								Oid generateDictionaryProcedure);
 
 extern void GenerateTypeDependencies(HeapTuple typeTuple,
 									 Relation typeCatalog,
diff --git a/src/include/catalog/pg_zstd_dictionaries.h b/src/include/catalog/pg_zstd_dictionaries.h
new file mode 100644
index 0000000000..b200ab541e
--- /dev/null
+++ b/src/include/catalog/pg_zstd_dictionaries.h
@@ -0,0 +1,53 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_zstd_dictionaries.h
+ *	  definition of the "zstd dictionay" system catalog (pg_zstd_dictionaries)
+ *
+ * src/include/catalog/pg_zstd_dictionaries.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_ZSTD_DICTIONARIES_H
+#define PG_ZSTD_DICTIONARIES_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
+
+/* ----------------
+ *		pg_zstd_dictionaries definition.  cpp turns this into
+ *		typedef struct FormData_pg_zstd_dictionaries
+ * ----------------
+ */
+CATALOG(pg_zstd_dictionaries,9946,ZstdDictionariesRelationId)
+{
+	Oid			dictid BKI_FORCE_NOT_NULL;
+
+	/*
+	 * variable-length fields start here, but we allow direct access to dict
+	 */
+	bytea		dict BKI_FORCE_NOT_NULL;
+} FormData_pg_zstd_dictionaries;
+
+/* Pointer type to a tuple with the format of pg_zstd_dictionaries relation */
+typedef FormData_pg_zstd_dictionaries *Form_pg_zstd_dictionaries;
+
+DECLARE_TOAST(pg_zstd_dictionaries, 9947, 9948);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_zstd_dictionaries_dictid_index, 9949, ZstdDictidIndexId, pg_zstd_dictionaries, btree(dictid oid_ops));
+
+MAKE_SYSCACHE(ZSTDDICTIDOID, pg_zstd_dictionaries_dictid_index, 128);
+
+typedef struct ZstdTrainingData
+{
+	char	   *sample_buffer;	/* Pointer to the raw sample buffer */
+	size_t	   *sample_sizes;	/* Array of sample sizes */
+	int			nitems;			/* Number of sample sizes */
+} ZstdTrainingData;
+
+extern bytea *get_zstd_dict(Oid dictid);
+
+#endif							/* PG_ZSTD_DICTIONARIES_H */
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index f1bd18c49f..e494436870 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -17,6 +17,7 @@
 #include "nodes/params.h"
 #include "nodes/queryjumble.h"
 #include "parser/parse_node.h"
+#include "access/htup.h"
 
 /* Hook for plugins to get control at end of parse analysis */
 typedef void (*post_parse_analyze_hook_type) (ParseState *pstate,
@@ -64,4 +65,8 @@ extern List *BuildOnConflictExcludedTargetlist(Relation targetrel,
 
 extern SortGroupClause *makeSortGroupClauseForSetOp(Oid rescoltype, bool require_hash);
 
+extern int	acquire_sample_rows(Relation onerel, int elevel,
+								HeapTuple *rows, int targrows,
+								double *totalrows, double *totaldeadrows);
+
 #endif							/* ANALYZE_H */
diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h
index f684a772af..93c6c59e08 100644
--- a/src/include/utils/attoptcache.h
+++ b/src/include/utils/attoptcache.h
@@ -21,6 +21,12 @@ typedef struct AttributeOpts
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	float8		n_distinct;
 	float8		n_distinct_inherited;
+	double		zstd_dictid;	/* Oid is a 32-bit unsigned integer, but
+								 * relopt_int is limited to INT_MAX, so it
+								 * cannot represent the full range of Oid
+								 * values. */
+	int			zstd_dict_size;
+	int			zstd_cmp_level;
 } AttributeOpts;
 
 extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d499..11e0bf53ad 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -36,14 +36,17 @@ typedef struct varatt_external
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+	uint32		va_cmp_alg;		/* The additional compression algorithms
+								 * information. */
 }			varatt_external;
 
 /*
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
  */
-#define VARLENA_EXTSIZE_BITS	30
-#define VARLENA_EXTSIZE_MASK	((1U << VARLENA_EXTSIZE_BITS) - 1)
+#define VARLENA_EXTSIZE_BITS				30
+#define VARLENA_EXTSIZE_MASK				((1U << VARLENA_EXTSIZE_BITS) - 1)
+#define VARLENA_EXTENDED_COMPRESSION_FLAG	0x3
 
 /*
  * struct varatt_indirect is a "TOAST pointer" representing an out-of-line
@@ -122,6 +125,13 @@ typedef union
 								 * compression method; see va_extinfo */
 		char		va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
 	}			va_compressed;
+	struct
+	{
+		uint32		va_header;
+		uint32		va_tcinfo;
+		uint32		va_cmp_alg;
+		char		va_data[FLEXIBLE_ARRAY_MEMBER];
+	}			va_compressed_ext;
 } varattrib_4b;
 
 typedef struct
@@ -242,7 +252,14 @@ typedef struct
 #endif							/* WORDS_BIGENDIAN */
 
 #define VARDATA_4B(PTR)		(((varattrib_4b *) (PTR))->va_4byte.va_data)
-#define VARDATA_4B_C(PTR)	(((varattrib_4b *) (PTR))->va_compressed.va_data)
+/*
+ * If va_tcinfo >> VARLENA_EXTSIZE_BITS == VARLENA_EXTENDED_COMPRESSION_FLAG
+ * use va_compressed_ext; otherwise, use the va_compressed.
+ */
+#define VARDATA_4B_C(PTR)                                                   								  \
+( (((varattrib_4b *)(PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG \
+  ? ((varattrib_4b *)(PTR))->va_compressed_ext.va_data                        								  \
+  : ((varattrib_4b *)(PTR))->va_compressed.va_data )
 #define VARDATA_1B(PTR)		(((varattrib_1b *) (PTR))->va_data)
 #define VARDATA_1B_E(PTR)	(((varattrib_1b_e *) (PTR))->va_data)
 
@@ -252,6 +269,7 @@ typedef struct
 
 #define VARHDRSZ_EXTERNAL		offsetof(varattrib_1b_e, va_data)
 #define VARHDRSZ_COMPRESSED		offsetof(varattrib_4b, va_compressed.va_data)
+#define VARHDRSZ_COMPRESSED_EXT	offsetof(varattrib_4b, va_compressed_ext.va_data)
 #define VARHDRSZ_SHORT			offsetof(varattrib_1b, va_data)
 
 #define VARATT_SHORT_MAX		0x7F
@@ -327,22 +345,54 @@ typedef struct
 /* Decompressed size and compression method of a compressed-in-line Datum */
 #define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
-#define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+/*
+ *  - "Extended" format is indicated by (va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG
+ *  - For the non-extended formats, the method code is stored in the top bits of va_tcinfo.
+ *  - In the extended format, the method code is stored in va_cmp_alg instead.
+ */
+#define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR)                       										  		\
+( ((((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG ) 	\
+  ? (((varattrib_4b *) (PTR))->va_compressed_ext.va_cmp_alg)     												  		\
+  : ( (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo) >> VARLENA_EXTSIZE_BITS))
 
 /* Same for external Datums; but note argument is a struct varatt_external */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
-#define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
-	((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS)
-
-#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
-	do { \
-		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
-	} while (0)
+/*
+ * If ((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG,
+ * that means the compression method code is in va_cmp_alg.
+ * Otherwise, we return the top two bits directly from va_extinfo.
+ */
+#define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer)  									\
+( ( ((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG ) \
+  ? (toast_pointer).va_cmp_alg                           										\
+  : ((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS) )
+
+#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) 	\
+    do { 																		\
+        /* If desired, keep or expand the Assert checks for known methods: */ 	\
+        Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || 							\
+               (cm) == TOAST_LZ4_COMPRESSION_ID || 								\
+			   (cm) == TOAST_ZSTD_COMPRESSION_ID); 								\
+        if ((cm) < TOAST_ZSTD_COMPRESSION_ID) 									\
+        { 																		\
+            /* Store the actual method in va_extinfo */ 						\
+			(toast_pointer).va_extinfo = (uint32)(len) 							\
+                | ((uint32)(cm) << VARLENA_EXTSIZE_BITS); 						\
+            /* Clear or set va_cmp_alg to 0 */ 									\
+            (toast_pointer).va_cmp_alg = 0; 									\
+        } 																		\
+        else 																	\
+        { 																		\
+            /* Store VARLENA_EXTENDED_COMPRESSION_FLAG in the top bits,			\
+			 meaning "extended" method. */ 										\
+            (toast_pointer).va_extinfo = (uint32)(len) |						\
+                ((uint32)VARLENA_EXTENDED_COMPRESSION_FLAG 						\
+						<< VARLENA_EXTSIZE_BITS);								\
+            /* Put the real method code in va_cmp_alg */ 						\
+            (toast_pointer).va_cmp_alg = (cm); 									\
+        } 																		\
+    } while (0)
 
 /*
  * Testing whether an externally-stored value is compressed now requires
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 4dd9ee7200..94495388ad 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -238,10 +238,11 @@ NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd.
+SET default_toast_compression = 'zstd';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 -- test alter compression method
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out
index 7bd7642b4b..0ce4915217 100644
--- a/src/test/regress/expected/compression_1.out
+++ b/src/test/regress/expected/compression_1.out
@@ -233,6 +233,9 @@ HINT:  Available values: pglz.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
 HINT:  Available values: pglz.
+SET default_toast_compression = 'zstd';
+ERROR:  invalid value for parameter "default_toast_compression": "zstd"
+HINT:  Available values: pglz.
 SET default_toast_compression = 'lz4';
 ERROR:  invalid value for parameter "default_toast_compression": "lz4"
 HINT:  Available values: pglz.
diff --git a/src/test/regress/expected/compression_zstd.out b/src/test/regress/expected/compression_zstd.out
new file mode 100644
index 0000000000..7de110a90a
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd.out
@@ -0,0 +1,123 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd CASCADE;
+NOTICE:  table "cmdata_zstd" does not exist, skipping
+CREATE TABLE cmdata_zstd (
+    f1 TEXT COMPRESSION zstd
+);
+ERROR:  compression method zstd not supported
+DETAIL:  This functionality requires the server to be built with zstd support.
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 10...
+                    ^
+QUERY:  INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004))
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
+-- Create a helper function to generate extra-large values.
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+$$
+    SELECT string_agg(md5(g::text), '')
+    FROM generate_series(1,256) g
+$$;
+-- Insert 5 extra-large rows to force externally stored compression.
+DO $$
+BEGIN
+  FOR i IN 1..5 LOOP
+    INSERT INTO cmdata_zstd (f1)
+    VALUES (large_val() || repeat('a', 4000));
+  END LOOP;
+END $$;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: INSERT INTO cmdata_zstd (f1)
+                    ^
+QUERY:  INSERT INTO cmdata_zstd (f1)
+    VALUES (large_val() || repeat('a', 4000))
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd;
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd
+GROUP BY pg_column_compression(f1);
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 3: FROM cmdata_zstd
+             ^
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 2: FROM cmdata_zstd;
+             ^
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_2;
+NOTICE:  table "cmdata_zstd_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPR...
+                                         ^
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_2;
+DROP TABLE cmdata_zstd_2;
+ERROR:  table "cmdata_zstd_2" does not exist
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd;
+NOTICE:  materialized view "compressmv_zstd" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd AS
+  SELECT f1 FROM cmdata_zstd;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 2:   SELECT f1 FROM cmdata_zstd;
+                         ^
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd;
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd;
+ERROR:  relation "compressmv_zstd" does not exist
+LINE 2: FROM compressmv_zstd;
+             ^
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd
+SET f1 = f1 || ' UPDATED';
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: UPDATE cmdata_zstd
+               ^
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 2: FROM cmdata_zstd;
+             ^
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd;
+ERROR:  materialized view "compressmv_zstd" does not exist
+DROP TABLE cmdata_zstd;
+ERROR:  table "cmdata_zstd" does not exist
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_zstd_1.out b/src/test/regress/expected/compression_zstd_1.out
new file mode 100644
index 0000000000..a540c99b37
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_1.out
@@ -0,0 +1,181 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd CASCADE;
+NOTICE:  table "cmdata_zstd" does not exist, skipping
+CREATE TABLE cmdata_zstd (
+    f1 TEXT COMPRESSION zstd
+);
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+-- Create a helper function to generate extra-large values.
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+$$
+    SELECT string_agg(md5(g::text), '')
+    FROM generate_series(1,256) g
+$$;
+-- Insert 5 extra-large rows to force externally stored compression.
+DO $$
+BEGIN
+  FOR i IN 1..5 LOOP
+    INSERT INTO cmdata_zstd (f1)
+    VALUES (large_val() || repeat('a', 4000));
+  END LOOP;
+END $$;
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd;
+                                      Table "public.cmdata_zstd"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd
+GROUP BY pg_column_compression(f1);
+ compression_method | row_count 
+--------------------+-----------
+ zstd               |        20
+(1 row)
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd;
+                     data_slice                     
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+(20 rows)
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_2;
+NOTICE:  table "cmdata_zstd_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_2;
+                                     Table "public.cmdata_zstd_2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+DROP TABLE cmdata_zstd_2;
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd;
+NOTICE:  materialized view "compressmv_zstd" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd AS
+  SELECT f1 FROM cmdata_zstd;
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd;
+                              Materialized view "public.compressmv_zstd"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1
+   FROM cmdata_zstd;
+
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd;
+ mv_compression 
+----------------
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+(20 rows)
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd
+SET f1 = f1 || ' UPDATED';
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd;
+ preview 
+---------
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+(20 rows)
+
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd;
+DROP TABLE cmdata_zstd;
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..ac5da3f5ab 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -71,6 +71,7 @@ NOTICE:  checking pg_type {typmodout} => pg_proc {oid}
 NOTICE:  checking pg_type {typanalyze} => pg_proc {oid}
 NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
+NOTICE:  checking pg_type {typebuildzstddictionary} => pg_proc {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 37b6d21e1f..407a0644f8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_zstd memoize stats predicate
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index 490595fcfb..e29909558f 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -102,6 +102,7 @@ CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 SET default_toast_compression = 'I do not exist compression';
+SET default_toast_compression = 'zstd';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 
diff --git a/src/test/regress/sql/compression_zstd.sql b/src/test/regress/sql/compression_zstd.sql
new file mode 100644
index 0000000000..7cf93e3de2
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd.sql
@@ -0,0 +1,97 @@
+\set HIDE_TOAST_COMPRESSION false
+
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd CASCADE;
+CREATE TABLE cmdata_zstd (
+    f1 TEXT COMPRESSION zstd
+);
+
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+
+-- Create a helper function to generate extra-large values.
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+$$
+    SELECT string_agg(md5(g::text), '')
+    FROM generate_series(1,256) g
+$$;
+
+-- Insert 5 extra-large rows to force externally stored compression.
+DO $$
+BEGIN
+  FOR i IN 1..5 LOOP
+    INSERT INTO cmdata_zstd (f1)
+    VALUES (large_val() || repeat('a', 4000));
+  END LOOP;
+END $$;
+
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd;
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd
+GROUP BY pg_column_compression(f1);
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd;
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_2;
+CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_2;
+DROP TABLE cmdata_zstd_2;
+
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd;
+CREATE MATERIALIZED VIEW compressmv_zstd AS
+  SELECT f1 FROM cmdata_zstd;
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd;
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd;
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd
+SET f1 = f1 || ' UPDATED';
+
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd;
+
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd;
+DROP TABLE cmdata_zstd;
+
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9840060997..adb94ee7fd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -886,6 +886,7 @@ FormData_pg_ts_parser
 FormData_pg_ts_template
 FormData_pg_type
 FormData_pg_user_mapping
+FormData_pg_zstd_dictionaries
 FormExtraData_pg_attribute
 Form_pg_aggregate
 Form_pg_am
@@ -945,6 +946,7 @@ Form_pg_ts_parser
 Form_pg_ts_template
 Form_pg_type
 Form_pg_user_mapping
+Form_pg_zstd_dictionaries
 FormatNode
 FreeBlockNumberArray
 FreeListData
@@ -2582,6 +2584,8 @@ STARTUPINFO
 STRLEN
 SV
 SYNCHRONIZATION_BARRIER
+SampleCollector
+SampleEntry
 SampleScan
 SampleScanGetSampleSize_function
 SampleScanState
@@ -3306,6 +3310,7 @@ ZSTD_cParameter
 ZSTD_inBuffer
 ZSTD_outBuffer
 ZstdCompressorState
+ZstdTrainingData
 _SPI_connection
 _SPI_plan
 __m128i

base-commit: 39de4f157d3ac0b889bb276c2487fe160578f967
-- 
2.47.1

#2Kirill Reshke
reshkekirill@gmail.com
In reply to: Nikhil Kumar Veldanda (#1)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Thu, 6 Mar 2025 at 08:43, Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Hi all,

The ZStandard compression algorithm [1][2], though not currently used for TOAST compression in PostgreSQL, offers significantly improved compression ratios compared to lz4/pglz in both dictionary-based and non-dictionary modes. Attached find for review my patch to add ZStandard compression to Postgres. In tests this patch used with a pre-trained dictionary achieved up to four times the compression ratio of LZ4, while ZStandard without a dictionary outperformed LZ4/pglz by about two times during compression of data.

Notably, this is the first compression algorithm for Postgres that can make use of a dictionary to provide higher levels of compression, but dictionaries have to be generated and maintained, and so I’ve had to break new ground in that regard. To use the dictionary support requires training and storing a dictionary for a given variable-length column type. On a variable-length column, a SQL function will be called. It will sample the column’s data and feed it into the ZStandard training API which will return a dictionary. In the example, the column is of JSONB type. The SQL function takes the table name and the attribute number as inputs. If the training is successful, it will return true; otherwise, it will return false.

‘’‘
test=# select build_zstd_dict_for_attribute('"public"."zstd"', 1);
build_zstd_dict_for_attribute
-------------------------------
t
(1 row)
‘’‘

The sampling logic and data to feed to the ZStandard training API can vary by data type. The patch includes an method to write other type-specific training functions and includes a default for JSONB, TEXT and BYTEA. There is a new option called ‘build_zstd_dict’ that takes a function name as input in ‘CREATE TYPE’. In this way anyone can write their own type-specific training function by handling sampling logic and returning the necessary information for the ZStandard training API in “ZstdTrainingData” format.

```
typedef struct ZstdTrainingData
{
char *sample_buffer; /* Pointer to the raw sample buffer */
size_t *sample_sizes; /* Array of sample sizes */
int nitems; /* Number of sample sizes */
} ZstdTrainingData;
```
This information is feed into the ZStandard train API, which generates a dictionary and inserts it into the dictionary catalog table. Additionally, we update the ‘pg_attribute’ attribute options to include the unique dictionary ID for that specific attribute. During compression, based on the available dictionary ID, we retrieve the dictionary and use it to compress the documents. I’ve created standard training function (`zstd_dictionary_builder`) for JSONB, TEXT, and BYTEA.

We store dictionary and dictid in the new catalog table ‘pg_zstd_dictionaries’

```
test=# \d pg_zstd_dictionaries
Table "pg_catalog.pg_zstd_dictionaries"
Column | Type | Collation | Nullable | Default
--------+-------+-----------+----------+---------
dictid | oid | | not null |
dict | bytea | | not null |
Indexes:
"pg_zstd_dictionaries_dictid_index" PRIMARY KEY, btree (dictid)
```

This is the entire ZStandard dictionary infrastructure. A column can have multiple dictionaries. The latest dictionary will be identified by the pg_attribute attoptions. We never delete dictionaries once they are generated. If a dictionary is not provided and attcompression is set to zstd, we compress with ZStandard without dictionary. For decompression, the zstd-compressed frame contains a dictionary identifier (dictid) that indicates the dictionary used for compression. By retrieving this dictid from the zstd frame, we then fetch the corresponding dictionary and perform decompression.

#############################################################################

Enter toast compression framework changes,

We identify a compressed datum compression algorithm using the top two bits of va_tcinfo (varattrib_4b.va_compressed).
It is possible to have four compression methods. However, based on previous community email discussions regarding toast compression changes[3], the idea of using it for a new compression algorithm has been rejected, and a suggestion has been made to extend it which I’ve implemented in this patch. This change necessitates an update to ‘varattrib_4b’ and ‘varatt_external’ on disk structures. I’ve made sure that this changes are backward compatible.

```
typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;
struct /* Compressed-in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method; see va_extinfo */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
} va_compressed;
struct
{
uint32 va_header;
uint32 va_tcinfo;
uint32 va_cmp_alg;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;

typedef struct varatt_external
{
int32 va_rawsize; /* Original data size (includes header) */
uint32 va_extinfo; /* External saved size (without header) and
* compression method */
Oid va_valueid; /* Unique ID of value within TOAST table */
Oid va_toastrelid; /* RelID of TOAST table containing it */
uint32 va_cmp_alg; /* The additional compression algorithms
* information. */
} varatt_external;
```

As I need to update this structs, I’ve made changes to the existing macros. Additionally added compression and decompression routines related to ZStandard as needed. These are major design changes in the patch to incorporate ZStandard with dictionary compression.

Please let me know what you think about all this. Are there any concerns with my approach? In particular, I would appreciate your thoughts on the on-disk changes that result from this.

kind regards,

Nikhil Veldanda
Amazon Web Services: https://aws.amazon.com

[1] https://facebook.github.io/zstd/
[2] https://github.com/facebook/zstd
[3] /messages/by-id/YoMiNmkztrslDbNS@paquier.xyz

Hi!
I generally love this idea, however I am not convinced in-core support
this is the right direction here. Maybe we can introduce some API
infrastructure here to allow delegating compression to extension's?
This is merely my opinion; perhaps dealing with a redo is not
worthwhile.

I did a brief lookup on patch v1. I feel like this is too much for a
single patch. Take, for example this change:

```
-#define NO_LZ4_SUPPORT() \
+#define NO_METHOD_SUPPORT(method) \
  ereport(ERROR, \
  (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
- errmsg("compression method lz4 not supported"), \
- errdetail("This functionality requires the server to be built with
lz4 support.")))
+ errmsg("compression method %s not supported", method), \
+ errdetail("This functionality requires the server to be built with
%s support.", method)))
 ```

This could be a separate preliminary refactoring patch in series.
Perhaps we need to divide the patch into smaller pieces if we follow
the suggested course of this thread (in-core support).

I will try to give another in-depth look here soon.

--
Best regards,
Kirill Reshke

#3Yura Sokolov
y.sokolov@postgrespro.ru
In reply to: Nikhil Kumar Veldanda (#1)
Re: ZStandard (with dictionaries) compression support for TOAST compression

06.03.2025 08:32, Nikhil Kumar Veldanda пишет:

Hi all,

The ZStandard compression algorithm [1][2], though not currently used for
TOAST compression in PostgreSQL, offers significantly improved compression
ratios compared to lz4/pglz in both dictionary-based and non-dictionary
modes. Attached find for review my patch to add ZStandard compression to
Postgres. In tests this patch used with a pre-trained dictionary achieved
up to four times the compression ratio of LZ4, while ZStandard without a
dictionary outperformed LZ4/pglz by about two times during compression of data.

Notably, this is the first compression algorithm for Postgres that can make
use of a dictionary to provide higher levels of compression, but
dictionaries have to be generated and maintained, and so I’ve had to break
new ground in that regard. To use the dictionary support requires training
and storing a dictionary for a given variable-length column type. On a
variable-length column, a SQL function will be called. It will sample the
column’s data and feed it into the ZStandard training API which will return
a dictionary. In the example, the column is of JSONB type. The SQL function
takes the table name and the attribute number as inputs. If the training is
successful, it will return true; otherwise, it will return false.

‘’‘
test=# select build_zstd_dict_for_attribute('"public"."zstd"', 1);
build_zstd_dict_for_attribute
-------------------------------
t
(1 row)
‘’‘

The sampling logic and data to feed to the ZStandard training API can vary
by data type. The patch includes an method to write other type-specific
training functions and includes a default for JSONB, TEXT and BYTEA. There
is a new option called ‘build_zstd_dict’ that takes a function name as
input in ‘CREATE TYPE’. In this way anyone can write their own type-
specific training function by handling sampling logic and returning the
necessary information for the ZStandard training API in “ZstdTrainingData”
format.

```
typedef struct ZstdTrainingData
{
char *sample_buffer; /* Pointer to the raw sample buffer */
size_t *sample_sizes; /* Array of sample sizes */
int nitems; /* Number of sample sizes */
} ZstdTrainingData;
```
This information is feed into the ZStandard train API, which generates a
dictionary and inserts it into the dictionary catalog table. Additionally,
we update the ‘pg_attribute’ attribute options to include the unique
dictionary ID for that specific attribute. During compression, based on the
available dictionary ID, we retrieve the dictionary and use it to compress
the documents. I’ve created standard training function
(`zstd_dictionary_builder`) for JSONB, TEXT, and BYTEA. 

We store dictionary and dictid in the new catalog table ‘pg_zstd_dictionaries’

```
test=# \d pg_zstd_dictionaries
Table "pg_catalog.pg_zstd_dictionaries"
Column | Type | Collation | Nullable | Default
--------+-------+-----------+----------+---------
dictid | oid | | not null |
dict | bytea | | not null |
Indexes:
"pg_zstd_dictionaries_dictid_index" PRIMARY KEY, btree (dictid)
``` 

This is the entire ZStandard dictionary infrastructure. A column can have
multiple dictionaries. The latest dictionary will be identified by the
pg_attribute attoptions. We never delete dictionaries once they are
generated. If a dictionary is not provided and attcompression is set to
zstd, we compress with ZStandard without dictionary. For decompression, the
zstd-compressed frame contains a dictionary identifier (dictid) that
indicates the dictionary used for compression. By retrieving this dictid
from the zstd frame, we then fetch the corresponding dictionary and perform
decompression.

#############################################################################

Enter toast compression framework changes,

We identify a compressed datum compression algorithm using the top two bits
of va_tcinfo (varattrib_4b.va_compressed). 
It is possible to have four compression methods. However, based on previous
community email discussions regarding toast compression changes[3], the
idea of using it for a new compression algorithm has been rejected, and a
suggestion has been made to extend it which I’ve implemented in this patch.
This change necessitates an update to ‘varattrib_4b’ and ‘varatt_external’
on disk structures. I’ve made sure that this changes are backward compatible. 

```
typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;
struct /* Compressed-in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method; see va_extinfo */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
} va_compressed;
struct
{
uint32 va_header;
uint32 va_tcinfo;
uint32 va_cmp_alg;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;

typedef struct varatt_external
{
int32 va_rawsize; /* Original data size (includes header) */
uint32 va_extinfo; /* External saved size (without header) and
* compression method */
Oid va_valueid; /* Unique ID of value within TOAST table */
Oid va_toastrelid; /* RelID of TOAST table containing it */
uint32 va_cmp_alg; /* The additional compression algorithms
* information. */
} varatt_external;
```

As I need to update this structs, I’ve made changes to the existing macros.
Additionally added compression and decompression routines related to
ZStandard as needed. These are major design changes in the patch to
incorporate ZStandard with dictionary compression. 

Please let me know what you think about all this. Are there any concerns
with my approach? In particular, I would appreciate your thoughts on the
on-disk changes that result from this.

kind regards,

Nikhil Veldanda
Amazon Web Services: https://aws.amazon.com <https://aws.amazon.com/&gt;

[1] https://facebook.github.io/zstd/ <https://facebook.github.io/zstd/&gt;
[2] https://github.com/facebook/zstd <https://github.com/facebook/zstd&gt;
[3] /messages/by-id/flat/
YoMiNmkztrslDbNS%40paquier.xyz </messages/by-id/flat/
YoMiNmkztrslDbNS%40paquier.xyz>

Overall idea is great.

I just want to mention LZ4 also have API to use dictionary. Its dictionary
will be as simple as "virtually prepended" text (in contrast to complex
ZStd dictionary format).

I mean, it would be great if "dictionary" will be common property for
different algorithms.

On the other hand, zstd have "super fast" mode which is actually a bit
faster than LZ4 and compresses a bit better. So may be support for
different algos is not essential. (But then we need a way to change
compression level to that "super fast" mode.)

-------
regards
Yura Sokolov aka funny-falcon

#4Aleksander Alekseev
aleksander@timescale.com
In reply to: Nikhil Kumar Veldanda (#1)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Nikhil,

Many thanks for working on this. I proposed a similar patch some time
ago [1]/messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com but the overall feedback was somewhat mixed so I choose to
focus on something else. Thanks for peeking this up.

test=# select build_zstd_dict_for_attribute('"public"."zstd"', 1);
build_zstd_dict_for_attribute
-------------------------------
t
(1 row)

Did you have a chance to familiarize yourself with the corresponding
discussion [1]/messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com and probably the previous threads? Particularly it was
pointed out that dictionaries should be built automatically during
VACUUM. We also discussed a special syntax for the feature, besides
other things.

[1]: /messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com

--
Best regards,
Aleksander Alekseev

#5Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Yura Sokolov (#3)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi,

Overall idea is great.

I just want to mention LZ4 also have API to use dictionary. Its dictionary
will be as simple as "virtually prepended" text (in contrast to complex
ZStd dictionary format).

I mean, it would be great if "dictionary" will be common property for
different algorithms.

On the other hand, zstd have "super fast" mode which is actually a bit
faster than LZ4 and compresses a bit better. So may be support for
different algos is not essential. (But then we need a way to change
compression level to that "super fast" mode.)

zstd compression level and zstd dictionary size is configurable at
attribute level using ALTER TABLE. Default zstd level is 3 and dict
size is 4KB. For super fast mode level can be set to 1.

```
test=# alter table zstd alter column doc set compression zstd;
ALTER TABLE
test=# alter table zstd alter column doc set(zstd_cmp_level = 1);
ALTER TABLE
test=# select * from pg_attribute where attrelid = 'zstd'::regclass
and attname = 'doc';
 attrelid | attname | atttypid | attlen | attnum | atttypmod |
attndims | attbyval | attalign | attstorage | attcompre
ssion | attnotnull | atthasdef | atthasmissing | attidentity |
attgenerated | attisdropped | attislocal | attinhcount
| attcollation | attstattarget | attacl |            attoptions
    | attfdwoptions | attmissingval
----------+---------+----------+--------+--------+-----------+----------+----------+----------+------------+----------
------+------------+-----------+---------------+-------------+--------------+--------------+------------+-------------
+--------------+---------------+--------+----------------------------------+---------------+---------------
    16389 | doc     |     3802 |     -1 |      1 |        -1 |
0 | f        | i        | x          | z
      | f          | f         | f             |             |
     | f            | t          |           0
|            0 |               |        |
{zstd_dictid=1,zstd_cmp_level=1} |               |
(1 row)
```
#6Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Aleksander Alekseev (#4)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi

On Thu, Mar 6, 2025 at 5:35 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

Hi Nikhil,

Many thanks for working on this. I proposed a similar patch some time
ago [1] but the overall feedback was somewhat mixed so I choose to
focus on something else. Thanks for peeking this up.

test=# select build_zstd_dict_for_attribute('"public"."zstd"', 1);
build_zstd_dict_for_attribute
-------------------------------
t
(1 row)

Did you have a chance to familiarize yourself with the corresponding
discussion [1] and probably the previous threads? Particularly it was
pointed out that dictionaries should be built automatically during
VACUUM. We also discussed a special syntax for the feature, besides
other things.

[1]: /messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com

Restricting dictionary generation to the vacuum process is not ideal
because it limits user control and flexibility. Compression efficiency
is highly dependent on data distribution, which can change
dynamically. By allowing users to generate dictionaries on demand via
an API, they can optimize compression when they detect inefficiencies
rather than waiting for a vacuum process, which may not align with
their needs.

Additionally, since all dictionaries are stored in the catalog table
anyway, users can generate and manage them independently without
interfering with the system’s automatic maintenance tasks. This
approach ensures better adaptability to real-world scenarios where
compression performance needs to be monitored and adjusted in real
time.

---
Nikhil Veldanda

#7Yura Sokolov
y.sokolov@postgrespro.ru
In reply to: Nikhil Kumar Veldanda (#5)
Re: ZStandard (with dictionaries) compression support for TOAST compression

06.03.2025 19:29, Nikhil Kumar Veldanda пишет:

Hi,

Overall idea is great.

I just want to mention LZ4 also have API to use dictionary. Its dictionary
will be as simple as "virtually prepended" text (in contrast to complex
ZStd dictionary format).

I mean, it would be great if "dictionary" will be common property for
different algorithms.

On the other hand, zstd have "super fast" mode which is actually a bit
faster than LZ4 and compresses a bit better. So may be support for
different algos is not essential. (But then we need a way to change
compression level to that "super fast" mode.)

zstd compression level and zstd dictionary size is configurable at
attribute level using ALTER TABLE. Default zstd level is 3 and dict
size is 4KB. For super fast mode level can be set to 1.

No. Super-fast mode levels are negative. See parsing "--fast" parameter in
`programs/zstdcli.c` in zstd's repository and definition of ZSTD_minCLevel().

So, to support "super-fast" mode you have to accept negative compression
levels. I didn't check, probably you're already support them?

-------
regards
Yura Sokolov aka funny-falcon

#8Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Yura Sokolov (#7)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Yura,

So, to support "super-fast" mode you have to accept negative compression
levels. I didn't check, probably you're already support them?

The key point I want to emphasize is that both zstd compression levels
and dictionary size should be configurable based on user preferences
at attribute level.

---
Nikhil Veldanda

#9Robert Haas
robertmhaas@gmail.com
In reply to: Nikhil Kumar Veldanda (#1)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Thu, Mar 6, 2025 at 12:43 AM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Notably, this is the first compression algorithm for Postgres that can make use of a dictionary to provide higher levels of compression, but dictionaries have to be generated and maintained,

I think that solving the problems around using a dictionary is going
to be really hard. Can we see some evidence that the results will be
worth it?

--
Robert Haas
EDB: http://www.enterprisedb.com

#10Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#9)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Robert Haas <robertmhaas@gmail.com> writes:

On Thu, Mar 6, 2025 at 12:43 AM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Notably, this is the first compression algorithm for Postgres that can make use of a dictionary to provide higher levels of compression, but dictionaries have to be generated and maintained,

I think that solving the problems around using a dictionary is going
to be really hard. Can we see some evidence that the results will be
worth it?

BTW, this is hardly the first such attempt. See [1]/messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com for a prior
attempt at something fairly similar, which ended up going nowhere.
It'd be wise to understand why that failed before pressing forward.

Note that the thread title for [1]/messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com is pretty misleading, as the
original discussion about JSONB-specific compression soon migrated
to discussion of compressing TOAST data using dictionaries. At
least from a ten-thousand-foot viewpoint, that seems like exactly
what you're proposing here. I see that you dismissed [1]/messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com as
irrelevant upthread, but I think you'd better look closer.

regards, tom lane

[1]: /messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com

#11Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Robert Haas (#9)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Robert,

I think that solving the problems around using a dictionary is going
to be really hard. Can we see some evidence that the results will be
worth it?

With the latest patch I've shared,

Using a Kaggle dataset of Nintendo-related tweets[1]https://www.kaggle.com/code/dcalambas/nintendo-tweets-analysis/data, we leveraged
PostgreSQL's acquire_sample_rows function to quickly gather just 1,000
sample rows for a specific attribute out of 104695 rows. These raw
samples were passed into Zstd's sampling buffer, generating a custom
dictionary. This dictionary was then directly used to compress the
documents, resulting in 62% of space savings after compressed:

```
test=# \dt+
List of tables
Schema | Name | Type | Owner | Persistence | Access
method | Size | Description
--------+----------------+-------+----------+-------------+---------------+--------+-------------
public | lz4 | table | nikhilkv | permanent | heap
| 297 MB |
public | pglz | table | nikhilkv | permanent | heap
| 259 MB |
public | zstd_with_dict | table | nikhilkv | permanent | heap
| 114 MB |
public | zstd_wo_dict | table | nikhilkv | permanent | heap
| 210 MB |
(4 rows)
```

We've observed similarly strong results on other datasets as well with
using dictionaries.

[1]: https://www.kaggle.com/code/dcalambas/nintendo-tweets-analysis/data

---
Nikhil Veldanda

#12Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Tom Lane (#10)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Tom,

On Thu, Mar 6, 2025 at 11:33 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

On Thu, Mar 6, 2025 at 12:43 AM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Notably, this is the first compression algorithm for Postgres that can make use of a dictionary to provide higher levels of compression, but dictionaries have to be generated and maintained,

I think that solving the problems around using a dictionary is going
to be really hard. Can we see some evidence that the results will be
worth it?

BTW, this is hardly the first such attempt. See [1] for a prior
attempt at something fairly similar, which ended up going nowhere.
It'd be wise to understand why that failed before pressing forward.

Note that the thread title for [1] is pretty misleading, as the
original discussion about JSONB-specific compression soon migrated
to discussion of compressing TOAST data using dictionaries. At
least from a ten-thousand-foot viewpoint, that seems like exactly
what you're proposing here. I see that you dismissed [1] as
irrelevant upthread, but I think you'd better look closer.

regards, tom lane

[1] /messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com

Thank you for highlighting the previous discussion—I reviewed [1]/messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com
closely. While both methods involve dictionary-based compression, the
approach I'm proposing differs significantly.

The previous method explicitly extracted string values from JSONB and
assigned unique OIDs to each entry, resulting in distinct dictionary
entries for every unique value. In contrast, this approach directly
leverages Zstandard's dictionary training API. We provide raw data
samples to Zstd, which generates a dictionary of a specified size.
This dictionary is then stored in a catalog table and used to compress
subsequent inserts for the specific attribute it was trained on.

Key differences include:

1. No new data types are required.
2. Attributes can optionally have multiple dictionaries; the latest
dictionary is used during compression, and the exact dictionary used
during compression is retrieved and applied for decompression.
3. Compression utilizes Zstandard's trained dictionaries when available.

Additionally, I have provided an option for users to define custom
sampling and training logic, as directly passing raw buffers to the
training API may not always yield optimal results, especially for
certain custom variable-length data types. This flexibility motivates
the necessary adjustments to `pg_type`.

I would greatly appreciate your feedback or any additional suggestions
you might have.

[1]: /messages/by-id/CAJ7c6TOtAB0z1UrksvGTStNE-herK-43bj22=5xVBg7S4vr5rQ@mail.gmail.com

Best regards,
Nikhil Veldanda

#13Aleksander Alekseev
aleksander@timescale.com
In reply to: Nikhil Kumar Veldanda (#12)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Nikhil,

Thank you for highlighting the previous discussion—I reviewed [1]
closely. While both methods involve dictionary-based compression, the
approach I'm proposing differs significantly.

The previous method explicitly extracted string values from JSONB and
assigned unique OIDs to each entry, resulting in distinct dictionary
entries for every unique value. In contrast, this approach directly
leverages Zstandard's dictionary training API. We provide raw data
samples to Zstd, which generates a dictionary of a specified size.
This dictionary is then stored in a catalog table and used to compress
subsequent inserts for the specific attribute it was trained on.

[...]

You didn't read closely enough I'm afraid. As Tom pointed out, the
title of the thread is misleading. On top of that there are several
separate threads. I did my best to cross-reference them, but
apparently didn't do good enough.

Initially I proposed to add ZSON extension [1]https://github.com/afiskon/zson[2]/messages/by-id/CAJ7c6TP3fCC9TNKJBQAcEf4c=L7XQZ7QvuUayLgjhNQMD_5M_A@mail.gmail.com to the PostgreSQL
core. However the idea evolved into TOAST improvements that don't
require a user to use special types. You may also find interesting the
related "Pluggable TOASTer" discussion [3]/messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru. The idea there was rather
different but the discussion about extending TOAST pointers so that in
the future we can use something else than ZSTD is relevant.

You will find the recent summary of the reached agreements somewhere
around this message [4]/messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com, take a look at the thread a bit above and
below it.

I believe this effort is important. You can't, however, simply discard
everything that was discussed in this area for the past several years.
If you want to succeed of course. No one will look at your patch if it
doesn't account for all the previous discussions. I'm sorry, I know
it's disappointing. This being said you should have done better
research before submitting the code. You could just ask if anyone was
working on something like this before and save a lot of time.

Personally I would suggest starting with one little step toward
compression dictionaries. Particularly focusing on extendability of
TOAST pointers. You are going to need to store dictionary ids there
and allow using other compression algorithms in the future. This will
require something like a varint/utf8-like bitmask for this. See the
previous discussions.

[1]: https://github.com/afiskon/zson
[2]: /messages/by-id/CAJ7c6TP3fCC9TNKJBQAcEf4c=L7XQZ7QvuUayLgjhNQMD_5M_A@mail.gmail.com
[3]: /messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru
[4]: /messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com

--
Best regards,
Aleksander Alekseev

#14Aleksander Alekseev
aleksander@timescale.com
In reply to: Robert Haas (#9)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Robert,

I think that solving the problems around using a dictionary is going
to be really hard. Can we see some evidence that the results will be
worth it?

Compression dictionaries give a good compression ratio (~50%) and also
increase TPS a bit (5-10%) due to better buffer cache utilization. At
least according to synthetic and not trustworthy benchmarks I did some
years ago [1]https://github.com/afiskon/zson/blob/master/docs/benchmark.md. The result may be very dependent on the actual data of
course, not to mention particular implementation of the idea.

[1]: https://github.com/afiskon/zson/blob/master/docs/benchmark.md

--
Best regards,
Aleksander Alekseev

#15Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Aleksander Alekseev (#13)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi,

I reviewed the discussions, and while most agreements focused on
changes to the toast pointer, the design I propose requires no
modifications to it. I’ve carefully considered the design choices made
previously, and I recognize Zstd’s clear advantages in compression
efficiency and performance over algorithms like PGLZ and LZ4, we can
integrate it without altering the existing toast pointer
(varatt_external) structure.

By simply using the top two bits of the va_extinfo field (setting them
to '11') in `varatt_external`, we can signal an alternative
compression algorithm, clearly distinguishing new methods from legacy
ones. The specific algorithm used would then be recorded in the
va_cmp_alg field.

This approach addresses the issues raised in the summarized thread[1]/messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com
and to leverage dictionaries for the data that can stay in-line. While
my initial patch includes modifications to toast_pointer due to a
single dependency on (pg_column_compression), those changes aren’t
strictly necessary; resolving that dependency separately would make
the overall design even less intrusive.

Here’s an illustrative structure:
```
typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;
struct /* Current Compressed format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original size and compression method */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
} va_compressed;
struct /* Extended compression format */
{
uint32 va_header;
uint32 va_tcinfo;
uint32 va_cmp_alg;
uint32 va_cmp_dictid;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;

typedef struct varatt_external
{
int32 va_rawsize; /* Original data size (includes header) */
uint32 va_extinfo; /* External saved size (without header) and
* compression method */ `11` indicates new compression methods.
Oid va_valueid; /* Unique ID of value within TOAST table */
Oid va_toastrelid; /* RelID of TOAST table containing it */
} varatt_external;
```

Decompression flow remains straightforward: once a datum is identified
as external, we detoast it, then we identify the compression algorithm
using `
TOAST_COMPRESS_METHOD` macro which refers to a varattrib_4b structure
not a toast pointer. We retrieve the compression algorithm from either
va_tcinfo or va_cmp_alg based on adjusted macros, and decompress
accordingly.

In summary, integrating Zstandard into the TOAST framework in this
minimally invasive way should yield substantial benefits.

[1]: /messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com

Best regards,
Nikhil Veldanda

On Fri, Mar 7, 2025 at 3:42 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

Show quoted text

Hi Nikhil,

Thank you for highlighting the previous discussion—I reviewed [1]
closely. While both methods involve dictionary-based compression, the
approach I'm proposing differs significantly.

The previous method explicitly extracted string values from JSONB and
assigned unique OIDs to each entry, resulting in distinct dictionary
entries for every unique value. In contrast, this approach directly
leverages Zstandard's dictionary training API. We provide raw data
samples to Zstd, which generates a dictionary of a specified size.
This dictionary is then stored in a catalog table and used to compress
subsequent inserts for the specific attribute it was trained on.

[...]

You didn't read closely enough I'm afraid. As Tom pointed out, the
title of the thread is misleading. On top of that there are several
separate threads. I did my best to cross-reference them, but
apparently didn't do good enough.

Initially I proposed to add ZSON extension [1][2] to the PostgreSQL
core. However the idea evolved into TOAST improvements that don't
require a user to use special types. You may also find interesting the
related "Pluggable TOASTer" discussion [3]. The idea there was rather
different but the discussion about extending TOAST pointers so that in
the future we can use something else than ZSTD is relevant.

You will find the recent summary of the reached agreements somewhere
around this message [4], take a look at the thread a bit above and
below it.

I believe this effort is important. You can't, however, simply discard
everything that was discussed in this area for the past several years.
If you want to succeed of course. No one will look at your patch if it
doesn't account for all the previous discussions. I'm sorry, I know
it's disappointing. This being said you should have done better
research before submitting the code. You could just ask if anyone was
working on something like this before and save a lot of time.

Personally I would suggest starting with one little step toward
compression dictionaries. Particularly focusing on extendability of
TOAST pointers. You are going to need to store dictionary ids there
and allow using other compression algorithms in the future. This will
require something like a varint/utf8-like bitmask for this. See the
previous discussions.

[1]: https://github.com/afiskon/zson
[2]: /messages/by-id/CAJ7c6TP3fCC9TNKJBQAcEf4c=L7XQZ7QvuUayLgjhNQMD_5M_A@mail.gmail.com
[3]: /messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru
[4]: /messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com

--
Best regards,
Aleksander Alekseev

#16Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Nikhil Kumar Veldanda (#15)
2 attachment(s)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi all,

Attached an updated version of the patch. Specifically, I've removed
changes related to the TOAST pointer structure. This proposal is
different from earlier discussions on this topic[1]/messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com, where extending
the TOAST pointer was considered essential for enabling
dictionary-based compression.

Key improvements introduced in this proposal:

1. No Changes to TOAST Pointer: The existing TOAST pointer structure
remains untouched, simplifying integration and minimizing potential
disruptions.

2. Extensible Design: The solution is structured to seamlessly
incorporate future compression algorithms beyond zstd [2]https://github.com/facebook/zstd, providing
greater flexibility and future-proofing.

3. Inline Data Compression with Dictionary Support: Crucially, this
approach supports dictionary-based compression for inline data.
Dictionaries are highly effective for compressing small-sized
documents, providing substantial storage savings. Please refer to the
attached image from the zstd README[2]https://github.com/facebook/zstd for supporting evidence.
Omitting dictionary-based compression for inline data would
significantly reduce these benefits. For example, under previous
design constraints [3]/messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com, if a 16KB document compressed down to 256
bytes using a dictionary, storing this inline would not have been
feasible. The current proposal addresses this limitation, thereby
fully leveraging dictionary-based compression.

I believe this solution effectively addresses the limitations
identified in our earlier discussions [1]/messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com[3]/messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com.

Feedback on this approach would be greatly appreciated, I welcome any
feedback or suggestions you might have.

References:
[1]: /messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com
[2]: https://github.com/facebook/zstd
[3]: /messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com

```
typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;
struct /* Compressed-in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method; see va_extinfo */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
} va_compressed;
struct
{
uint32 va_header;
uint32 va_tcinfo;
uint32 va_cmp_alg;
uint32 va_cmp_dictid;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;
```
Additional algorithm information and dictid is stored in varattrib_4b.

Best regards,
Nikhil Veldanda

On Fri, Mar 7, 2025 at 5:35 PM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Show quoted text

Hi,

I reviewed the discussions, and while most agreements focused on
changes to the toast pointer, the design I propose requires no
modifications to it. I’ve carefully considered the design choices made
previously, and I recognize Zstd’s clear advantages in compression
efficiency and performance over algorithms like PGLZ and LZ4, we can
integrate it without altering the existing toast pointer
(varatt_external) structure.

By simply using the top two bits of the va_extinfo field (setting them
to '11') in `varatt_external`, we can signal an alternative
compression algorithm, clearly distinguishing new methods from legacy
ones. The specific algorithm used would then be recorded in the
va_cmp_alg field.

This approach addresses the issues raised in the summarized thread[1]
and to leverage dictionaries for the data that can stay in-line. While
my initial patch includes modifications to toast_pointer due to a
single dependency on (pg_column_compression), those changes aren’t
strictly necessary; resolving that dependency separately would make
the overall design even less intrusive.

Here’s an illustrative structure:
```
typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;
struct /* Current Compressed format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original size and compression method */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
} va_compressed;
struct /* Extended compression format */
{
uint32 va_header;
uint32 va_tcinfo;
uint32 va_cmp_alg;
uint32 va_cmp_dictid;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;

typedef struct varatt_external
{
int32 va_rawsize; /* Original data size (includes header) */
uint32 va_extinfo; /* External saved size (without header) and
* compression method */ `11` indicates new compression methods.
Oid va_valueid; /* Unique ID of value within TOAST table */
Oid va_toastrelid; /* RelID of TOAST table containing it */
} varatt_external;
```

Decompression flow remains straightforward: once a datum is identified
as external, we detoast it, then we identify the compression algorithm
using `
TOAST_COMPRESS_METHOD` macro which refers to a varattrib_4b structure
not a toast pointer. We retrieve the compression algorithm from either
va_tcinfo or va_cmp_alg based on adjusted macros, and decompress
accordingly.

In summary, integrating Zstandard into the TOAST framework in this
minimally invasive way should yield substantial benefits.

[1] /messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com

Best regards,
Nikhil Veldanda

On Fri, Mar 7, 2025 at 3:42 AM Aleksander Alekseev
<aleksander@timescale.com> wrote:

Hi Nikhil,

Thank you for highlighting the previous discussion—I reviewed [1]
closely. While both methods involve dictionary-based compression, the
approach I'm proposing differs significantly.

The previous method explicitly extracted string values from JSONB and
assigned unique OIDs to each entry, resulting in distinct dictionary
entries for every unique value. In contrast, this approach directly
leverages Zstandard's dictionary training API. We provide raw data
samples to Zstd, which generates a dictionary of a specified size.
This dictionary is then stored in a catalog table and used to compress
subsequent inserts for the specific attribute it was trained on.

[...]

You didn't read closely enough I'm afraid. As Tom pointed out, the
title of the thread is misleading. On top of that there are several
separate threads. I did my best to cross-reference them, but
apparently didn't do good enough.

Initially I proposed to add ZSON extension [1][2] to the PostgreSQL
core. However the idea evolved into TOAST improvements that don't
require a user to use special types. You may also find interesting the
related "Pluggable TOASTer" discussion [3]. The idea there was rather
different but the discussion about extending TOAST pointers so that in
the future we can use something else than ZSTD is relevant.

You will find the recent summary of the reached agreements somewhere
around this message [4], take a look at the thread a bit above and
below it.

I believe this effort is important. You can't, however, simply discard
everything that was discussed in this area for the past several years.
If you want to succeed of course. No one will look at your patch if it
doesn't account for all the previous discussions. I'm sorry, I know
it's disappointing. This being said you should have done better
research before submitting the code. You could just ask if anyone was
working on something like this before and save a lot of time.

Personally I would suggest starting with one little step toward
compression dictionaries. Particularly focusing on extendability of
TOAST pointers. You are going to need to store dictionary ids there
and allow using other compression algorithms in the future. This will
require something like a varint/utf8-like bitmask for this. See the
previous discussions.

[1]: https://github.com/afiskon/zson
[2]: /messages/by-id/CAJ7c6TP3fCC9TNKJBQAcEf4c=L7XQZ7QvuUayLgjhNQMD_5M_A@mail.gmail.com
[3]: /messages/by-id/224711f9-83b7-a307-b17f-4457ab73aa0a@sigaev.ru
[4]: /messages/by-id/CAJ7c6TPSN06C+5cYSkyLkQbwN1C+pUNGmx+VoGCA-SPLCszC8w@mail.gmail.com

--
Best regards,
Aleksander Alekseev

Attachments:

v2-0001-Add-ZStandard-with-dictionaries-compression-suppo.patchapplication/octet-stream; name=v2-0001-Add-ZStandard-with-dictionaries-compression-suppo.patchDownload
From bb4d6120cc466a515268ddc80e7229f1b00f0801 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Sat, 8 Mar 2025 13:44:38 +0000
Subject: [PATCH v2] Add ZStandard (with dictionaries) compression support for
 TOAST

---
 contrib/amcheck/verify_heapam.c               |   1 +
 doc/src/sgml/catalogs.sgml                    |  55 ++
 doc/src/sgml/ref/create_type.sgml             |  21 +-
 src/backend/access/brin/brin_tuple.c          |  21 +-
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/indextuple.c        |  16 +-
 src/backend/access/common/reloptions.c        |  36 +-
 src/backend/access/common/toast_compression.c | 275 ++++++++-
 src/backend/access/common/toast_internals.c   |  17 +-
 src/backend/access/table/toast_helper.c       |  21 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/heap.c                    |   8 +-
 src/backend/catalog/meson.build               |   1 +
 src/backend/catalog/pg_type.c                 |  11 +-
 src/backend/catalog/pg_zstd_dictionaries.c    | 566 ++++++++++++++++++
 src/backend/commands/analyze.c                |   7 +-
 src/backend/commands/typecmds.c               |  99 ++-
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/pg_dump/pg_dump.c                     |  25 +-
 src/bin/psql/describe.c                       |   5 +-
 src/include/access/toast_compression.h        |  13 +-
 src/include/access/toast_helper.h             |   2 +
 src/include/access/toast_internals.h          |  51 +-
 src/include/catalog/Makefile                  |   3 +-
 src/include/catalog/catversion.h              |   2 +-
 src/include/catalog/meson.build               |   1 +
 src/include/catalog/pg_proc.dat               |  10 +
 src/include/catalog/pg_type.dat               |   6 +-
 src/include/catalog/pg_type.h                 |   8 +-
 src/include/catalog/pg_zstd_dictionaries.h    |  53 ++
 src/include/parser/analyze.h                  |   5 +
 src/include/utils/attoptcache.h               |   6 +
 src/include/varatt.h                          |  67 ++-
 src/test/regress/expected/compression.out     |   5 +-
 src/test/regress/expected/compression_1.out   |   3 +
 .../regress/expected/compression_zstd.out     | 123 ++++
 .../regress/expected/compression_zstd_1.out   | 181 ++++++
 src/test/regress/expected/oidjoins.out        |   1 +
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression.sql          |   1 +
 src/test/regress/sql/compression_zstd.sql     |  97 +++
 src/tools/pgindent/typedefs.list              |   5 +
 44 files changed, 1763 insertions(+), 90 deletions(-)
 create mode 100644 src/backend/catalog/pg_zstd_dictionaries.c
 create mode 100644 src/include/catalog/pg_zstd_dictionaries.h
 create mode 100644 src/test/regress/expected/compression_zstd.out
 create mode 100644 src/test/regress/expected/compression_zstd_1.out
 create mode 100644 src/test/regress/sql/compression_zstd.sql

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 827312306f..f01cc940e3 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1700,6 +1700,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb05063555..ed4c51a678 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -369,6 +369,12 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-zstd-dictionaries"><structname>pg_zstd_dictionaries</structname></link></entry>
+      <entry>Zstandard dictionaries</entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
@@ -9779,4 +9785,53 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </table>
  </sect1>
 
+
+<sect1 id="catalog-pg-zstd-dictionaries">
+  <title><structname>pg_zstd_dictionaries</structname></title>
+
+  <indexterm zone="catalog-pg-zstd-dictionaries">
+    <primary>pg_zstd_dictionaries</primary>
+  </indexterm>
+
+  <para>
+    The catalog <structname>pg_zstd_dictionaries</structname> maintains the dictionaries essential for Zstandard compression and decompression.
+  </para>
+
+  <table>
+    <title><structname>pg_zstd_dictionaries</structname> Columns</title>
+    <tgroup cols="1">
+      <thead>
+        <row>
+          <entry role="catalog_table_entry">
+            <para role="column_definition">Column Type</para>
+            <para>Description</para>
+          </entry>
+        </row>
+      </thead>
+      <tbody>
+        <row>
+          <entry role="catalog_table_entry">
+            <para role="column_definition">
+              <structfield>dictid</structfield> <type>oid</type>
+            </para>
+            <para>
+              Dictionary identifier; a non-null OID that uniquely identifies a dictionary.
+            </para>
+          </entry>
+        </row>
+        <row>
+          <entry role="catalog_table_entry">
+            <para role="column_definition">
+              <structfield>dict</structfield> <type>bytea</type>
+            </para>
+            <para>
+              Variable-length field containing the zstd dictionary data. This field must not be null.
+            </para>
+          </entry>
+        </row>
+      </tbody>
+    </tgroup>
+  </table>
+</sect1>
+
 </chapter>
diff --git a/doc/src/sgml/ref/create_type.sgml b/doc/src/sgml/ref/create_type.sgml
index 994dfc6526..ad4cf2f8b3 100644
--- a/doc/src/sgml/ref/create_type.sgml
+++ b/doc/src/sgml/ref/create_type.sgml
@@ -56,6 +56,7 @@ CREATE TYPE <replaceable class="parameter">name</replaceable> (
     [ , ELEMENT = <replaceable class="parameter">element</replaceable> ]
     [ , DELIMITER = <replaceable class="parameter">delimiter</replaceable> ]
     [ , COLLATABLE = <replaceable class="parameter">collatable</replaceable> ]
+    [ , BUILD_ZSTD_DICT = <replaceable class="parameter">zstd_training_function</replaceable> ]
 )
 
 CREATE TYPE <replaceable class="parameter">name</replaceable>
@@ -211,7 +212,8 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
    <replaceable class="parameter">type_modifier_input_function</replaceable>,
    <replaceable class="parameter">type_modifier_output_function</replaceable>,
    <replaceable class="parameter">analyze_function</replaceable>, and
-   <replaceable class="parameter">subscript_function</replaceable>
+   <replaceable class="parameter">subscript_function</replaceable>, and
+   <replaceable class="parameter">zstd_training_function</replaceable>
    are optional.  Generally these functions have to be coded in C
    or another low-level language.
   </para>
@@ -491,6 +493,15 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
    make use of the collation information; this does not happen
    automatically merely by marking the type collatable.
   </para>
+
+  <para>
+    The optional <replaceable class="parameter">zstd_training_function</replaceable>
+    performs type-specific sample collection for a column of the corresponding data type.
+    By default, for <type>jsonb</type>, <type>text</type>, and <type>bytea</type> data types, the function <literal>zstd_dictionary_builder</literal> is defined. It attempts to gather samples for a column 
+    and returns a sample buffer for zstd dictionary training. The training function must be declared to accept two arguments of type <type>internal</type> and return an <type>internal</type> result. 
+    The detailed information for zstd training function is provided in <filename>src/backend/catalog/pg_zstd_dictionaries.c</filename>.
+  </para>
+
   </refsect2>
 
   <refsect2 id="sql-createtype-array" xreflabel="Array Types">
@@ -846,6 +857,14 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
      </para>
     </listitem>
    </varlistentry>
+   <varlistentry>
+    <term><replaceable class="parameter">build_zstd_dict</replaceable></term>
+    <listitem>
+        <para>
+        Specifies the name of a function that performs sampling and provides the logic necessary to generate a sample buffer for zstd training.
+        </para>
+    </listitem>
+   </varlistentry>
   </variablelist>
  </refsect1>
 
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6d..02f1996ede 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -40,7 +40,7 @@
 #include "access/tupmacs.h"
 #include "utils/datum.h"
 #include "utils/memutils.h"
-
+#include "utils/attoptcache.h"
 
 /*
  * This enables de-toasting of index entries.  Needed until VACUUM is
@@ -222,7 +222,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 				 atttype->typstorage == TYPSTORAGE_MAIN))
 			{
 				Datum		cvalue;
-				char		compression;
+				CompressionInfo cmp = {.cmethod = InvalidCompressionMethod,.dictid = InvalidDictId,.zstd_level = DEFAULT_ZSTD_LEVEL};
+
 				Form_pg_attribute att = TupleDescAttr(brdesc->bd_tupdesc,
 													  keyno);
 
@@ -233,11 +234,19 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 				 * default method.
 				 */
 				if (att->atttypid == atttype->type_id)
-					compression = att->attcompression;
-				else
-					compression = InvalidCompressionMethod;
+					cmp.cmethod = att->attcompression;
 
-				cvalue = toast_compress_datum(value, compression);
+				if (cmp.cmethod == TOAST_ZSTD_COMPRESSION)
+				{
+					AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+					if (aopt != NULL)
+					{
+						cmp.zstd_level = aopt->zstd_level;
+						cmp.dictid = (Oid) aopt->dictid;
+					}
+				}
+				cvalue = toast_compress_datum(value, cmp);
 
 				if (DatumGetPointer(cvalue) != NULL)
 				{
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 6265178774..b57a9f024c 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -246,10 +246,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
 				TOAST_PGLZ_COMPRESSION_ID)
@@ -485,6 +485,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +530,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 1986b943a2..9cf5aabf51 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -21,6 +21,7 @@
 #include "access/htup_details.h"
 #include "access/itup.h"
 #include "access/toast_internals.h"
+#include "utils/attoptcache.h"
 
 /*
  * This enables de-toasting of index entries.  Needed until VACUUM is
@@ -124,8 +125,19 @@ index_form_tuple_context(TupleDesc tupleDescriptor,
 		{
 			Datum		cvalue;
 
-			cvalue = toast_compress_datum(untoasted_values[i],
-										  att->attcompression);
+			CompressionInfo cmp = {.cmethod = att->attcompression,.dictid = InvalidDictId,.zstd_level = DEFAULT_ZSTD_LEVEL};
+
+			if (cmp.cmethod == TOAST_ZSTD_COMPRESSION)
+			{
+				AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+				if (aopt != NULL)
+				{
+					cmp.zstd_level = aopt->zstd_level;
+					cmp.dictid = (Oid) aopt->dictid;
+				}
+			}
+			cvalue = toast_compress_datum(untoasted_values[i], cmp);
 
 			if (DatumGetPointer(cvalue) != NULL)
 			{
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 59fb53e770..7c71fb3492 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -34,6 +34,7 @@
 #include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "access/toast_compression.h"
 
 /*
  * Contents of pg_class.reloptions
@@ -389,7 +390,26 @@ static relopt_int intRelOpts[] =
 		},
 		-1, 0, 1024
 	},
-
+	{
+		{
+			"zstd_dict_size",
+			"Max dict size for zstd",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_DICT_SIZE, 0, 112640	/* Max dict size(110 KB), 0
+											 * indicates Don't use dictionary
+											 * for compression */
+	},
+	{
+		{
+			"zstd_level",
+			"Set column's ZSTD compression level",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_LEVEL, 1, 22
+	},
 	/* list terminator */
 	{{NULL}}
 };
@@ -478,6 +498,15 @@ static relopt_real realRelOpts[] =
 		},
 		0, -1.0, DBL_MAX
 	},
+	{
+		{
+			"dictid",
+			"Current dictid for column",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		InvalidDictId, InvalidDictId, UINT32_MAX
+	},
 	{
 		{
 			"vacuum_cleanup_index_scale_factor",
@@ -2093,7 +2122,10 @@ attribute_reloptions(Datum reloptions, bool validate)
 {
 	static const relopt_parse_elt tab[] = {
 		{"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)},
-		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}
+		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)},
+		{"dictid", RELOPT_TYPE_REAL, offsetof(AttributeOpts, dictid)},
+		{"zstd_dict_size", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_dict_size)},
+		{"zstd_level", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_level)},
 	};
 
 	return (bytea *) build_reloptions(reloptions, validate,
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97..2a271b8bcd 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,19 +17,26 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#include <zdict.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
+#include "catalog/pg_zstd_dictionaries.h"
+#include "access/toast_internals.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
 
-#define NO_LZ4_SUPPORT() \
+#define NO_METHOD_SUPPORT(method) \
 	ereport(ERROR, \
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
-			 errmsg("compression method lz4 not supported"), \
-			 errdetail("This functionality requires the server to be built with lz4 support.")))
+			 errmsg("compression method %s not supported", method), \
+			 errdetail("This functionality requires the server to be built with %s support.", method)))
 
 /*
  * Compress a varlena using PGLZ.
@@ -139,7 +146,7 @@ struct varlena *
 lz4_compress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	NO_METHOD_SUPPORT("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		valsize;
@@ -182,7 +189,7 @@ struct varlena *
 lz4_decompress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	NO_METHOD_SUPPORT("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -215,7 +222,7 @@ struct varlena *
 lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	NO_METHOD_SUPPORT("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -266,7 +273,13 @@ toast_get_compression_id(struct varlena *attr)
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) >= TOAST_ZSTD_COMPRESSION_ID)
+		{
+			struct varlena *compressed_attr = detoast_external_attr(attr);
+
+			cmid = TOAST_COMPRESS_METHOD(compressed_attr);
+		}
+		else
 			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 	}
 	else if (VARATT_IS_COMPRESSED(attr))
@@ -289,10 +302,17 @@ CompressionNameToMethod(const char *compression)
 	else if (strcmp(compression, "lz4") == 0)
 	{
 #ifndef USE_LZ4
-		NO_LZ4_SUPPORT();
+		NO_METHOD_SUPPORT("lz4");
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd") == 0)
+	{
+#ifndef USE_ZSTD
+		NO_METHOD_SUPPORT("zstd");
+#endif
+		return TOAST_ZSTD_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -309,8 +329,247 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_COMPRESSION:
+			return "zstd";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
 	}
 }
+
+/* Compress datum using ZSTD with optional dictionary (using cdict) */
+struct varlena *
+zstd_compress_datum(const struct varlena *value, Oid dictid, int zstd_level)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	void	   *dest;
+	size_t		cmp_size,
+				ret;
+	ZSTD_CCtx  *cctx = ZSTD_createCCtx();
+	ZSTD_CDict *cdict = NULL;
+
+	if (!cctx)
+		ereport(ERROR, (errmsg("Failed to create ZSTD compression context")));
+
+	ret = ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, zstd_level);
+	if (ZSTD_isError(ret))
+	{
+		ZSTD_freeCCtx(cctx);
+		ereport(ERROR, (errmsg("Failed to reference ZSTD compression level: %s", ZSTD_getErrorName(ret))));
+	}
+
+	if (dictid != InvalidDictId)
+	{
+		bytea	   *dict_bytea = get_zstd_dict(dictid);
+		const void *dict_buffer = VARDATA_ANY(dict_bytea);
+		uint32		dict_size = VARSIZE_ANY(dict_bytea) - VARHDRSZ;
+
+		cdict = ZSTD_createCDict(dict_buffer, dict_size, zstd_level);
+		pfree(dict_bytea);
+
+		if (!cdict)
+		{
+			ZSTD_freeCCtx(cctx);
+			ereport(ERROR, (errmsg("Failed to create ZSTD compression dictionary")));
+		}
+
+		ret = ZSTD_CCtx_refCDict(cctx, cdict);
+		if (ZSTD_isError(ret))
+		{
+			ZSTD_freeCDict(cdict);
+			ZSTD_freeCCtx(cctx);
+			ereport(ERROR, (errmsg("Failed to reference ZSTD dictionary: %s", ZSTD_getErrorName(ret))));
+		}
+	}
+
+	/* Allocate space for the compressed varlena (header + data) */
+	compressed = (struct varlena *) palloc(max_size + VARHDRSZ_COMPRESSED_EXT);
+	dest = (char *) compressed + VARHDRSZ_COMPRESSED_EXT;
+
+	/* Compress the data */
+	cmp_size = ZSTD_compress2(cctx, dest, max_size, VARDATA_ANY(value), valsize);
+
+	/* Cleanup */
+	ZSTD_freeCDict(cdict);
+	ZSTD_freeCCtx(cctx);
+
+	if (ZSTD_isError(cmp_size))
+	{
+		pfree(compressed);
+		ereport(ERROR, (errmsg("ZSTD compression failed: %s", ZSTD_getErrorName(cmp_size))));
+	}
+
+	/*
+	 * If compression did not reduce size, return NULL so that the
+	 * uncompressed data is stored
+	 */
+	if (cmp_size > valsize)
+	{
+		pfree(compressed);
+		return NULL;
+	}
+
+	/* Set the compressed size in the varlena header */
+	SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARHDRSZ_COMPRESSED_EXT);
+	return compressed;
+
+#else
+	NO_METHOD_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		cmp_size_exhdr = VARSIZE_4B(value) - VARHDRSZ_COMPRESSED_EXT;
+	Oid			dictid;
+	struct varlena *result;
+	size_t		uncmp_size,
+				ret;
+	ZSTD_DCtx  *dctx = ZSTD_createDCtx();
+	ZSTD_DDict *ddict = NULL;
+
+	if (!dctx)
+		ereport(ERROR, (errmsg("Failed to create ZSTD decompression context")));
+
+
+	dictid = (Oid) VARDATA_COMPRESSED_GET_DICTID(value);
+
+	if (dictid != InvalidDictId)
+	{
+		bytea	   *dict_bytea = get_zstd_dict(dictid);
+		const void *dict_buffer = VARDATA_ANY(dict_bytea);
+		uint32		dict_size = VARSIZE_ANY(dict_bytea) - VARHDRSZ;
+
+		ddict = ZSTD_createDDict(dict_buffer, dict_size);
+		pfree(dict_bytea);
+
+		if (!ddict)
+		{
+			ZSTD_freeDCtx(dctx);
+			ereport(ERROR, (errmsg("Failed to create ZSTD compression dictionary")));
+		}
+
+		ret = ZSTD_DCtx_refDDict(dctx, ddict);
+		if (ZSTD_isError(ret))
+		{
+			ZSTD_freeDDict(ddict);
+			ZSTD_freeDCtx(dctx);
+			ereport(ERROR, (errmsg("Failed to reference ZSTD dictionary: %s", ZSTD_getErrorName(ret))));
+		}
+	}
+
+	/* Allocate space for the uncompressed data */
+	result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+	uncmp_size = ZSTD_decompressDCtx(dctx,
+									 VARDATA(result),
+									 actual_size_exhdr,
+									 VARDATA_4B_C(value),
+									 cmp_size_exhdr);
+
+	/* Cleanup */
+	ZSTD_freeDDict(ddict);
+	ZSTD_freeDCtx(dctx);
+
+	if (ZSTD_isError(uncmp_size))
+	{
+		pfree(result);
+		ereport(ERROR, (errmsg("ZSTD decompression failed: %s", ZSTD_getErrorName(uncmp_size))));
+	}
+
+	/* Set final size in the varlena header */
+	SET_VARSIZE(result, uncmp_size + VARHDRSZ);
+	return result;
+
+#else
+	NO_METHOD_SUPPORT("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum using the streaming API and optional dictionary */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	ZSTD_DCtx  *dctx = ZSTD_createDCtx();
+	ZSTD_DDict *ddict = NULL;
+	Oid			dictid;
+	uint32		cmp_size_exhdr = VARSIZE_4B(value) - VARHDRSZ_COMPRESSED_EXT;
+	size_t		ret;
+
+	if (dctx == NULL)
+		elog(ERROR, "could not create zstd decompression context");
+
+	/* Extract the dictionary ID from the compressed frame */
+	dictid = (Oid) ZSTD_getDictID_fromFrame(VARDATA_4B_C(value), cmp_size_exhdr);
+
+	if (dictid != InvalidDictId)
+	{
+		bytea	   *dict_bytea = get_zstd_dict(dictid);
+		const void *dict_buffer = VARDATA_ANY(dict_bytea);
+		uint32		dict_size = VARSIZE_ANY(dict_bytea) - VARHDRSZ;
+
+		/* Create and bind the dictionary to the decompression context */
+		ddict = ZSTD_createDDict(dict_buffer, dict_size);
+		pfree(dict_bytea);
+
+		if (!ddict)
+		{
+			ZSTD_freeDCtx(dctx);
+			ereport(ERROR, (errmsg("Failed to create ZSTD compression dictionary")));
+		}
+
+		ret = ZSTD_DCtx_refDDict(dctx, ddict);
+		if (ZSTD_isError(ret))
+		{
+			ZSTD_freeDDict(ddict);
+			ZSTD_freeDCtx(dctx);
+			ereport(ERROR, (errmsg("Failed to reference ZSTD dictionary: %s", ZSTD_getErrorName(ret))));
+		}
+	}
+
+	inBuf.src = (char *) value + VARHDRSZ_COMPRESSED_EXT;
+	inBuf.size = VARSIZE(value) - VARHDRSZ_COMPRESSED_EXT;
+	inBuf.pos = 0;
+
+	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+	outBuf.dst = (char *) result + VARHDRSZ;
+	outBuf.size = slicelength;
+	outBuf.pos = 0;
+
+	/* Common decompression loop */
+	while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+	{
+		ret = ZSTD_decompressStream(dctx, &outBuf, &inBuf);
+		if (ZSTD_isError(ret))
+		{
+			pfree(result);
+			ZSTD_freeDDict(ddict);
+			ZSTD_freeDCtx(dctx);
+			elog(ERROR, "zstd decompression failed: %s", ZSTD_getErrorName(ret));
+		}
+	}
+
+	/* Cleanup */
+	ZSTD_freeDDict(ddict);
+	ZSTD_freeDCtx(dctx);
+
+	Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+	SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+	return result;
+#else
+	NO_METHOD_SUPPORT("zstd");
+	return NULL;
+#endif
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346c..e6c877fd0a 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -43,11 +43,12 @@ static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
  * ----------
  */
 Datum
-toast_compress_datum(Datum value, char cmethod)
+toast_compress_datum(Datum value, CompressionInfo cmp)
 {
 	struct varlena *tmp = NULL;
 	int32		valsize;
 	ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID;
+	uint32		dictid = cmp.dictid;
 
 	Assert(!VARATT_IS_EXTERNAL(DatumGetPointer(value)));
 	Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value)));
@@ -55,13 +56,13 @@ toast_compress_datum(Datum value, char cmethod)
 	valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(value));
 
 	/* If the compression method is not valid, use the current default */
-	if (!CompressionMethodIsValid(cmethod))
-		cmethod = default_toast_compression;
+	if (!CompressionMethodIsValid(cmp.cmethod))
+		cmp.cmethod = default_toast_compression;
 
 	/*
 	 * Call appropriate compression routine for the compression method.
 	 */
-	switch (cmethod)
+	switch (cmp.cmethod)
 	{
 		case TOAST_PGLZ_COMPRESSION:
 			tmp = pglz_compress_datum((const struct varlena *) value);
@@ -71,8 +72,12 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			tmp = zstd_compress_datum((const struct varlena *) value, cmp.dictid, cmp.zstd_level);
+			cmid = TOAST_ZSTD_COMPRESSION_ID;
+			break;
 		default:
-			elog(ERROR, "invalid compression method %c", cmethod);
+			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
 
 	if (tmp == NULL)
@@ -92,7 +97,7 @@ toast_compress_datum(Datum value, char cmethod)
 	{
 		/* successful compression */
 		Assert(cmid != TOAST_INVALID_COMPRESSION_ID);
-		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid);
+		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid, dictid);
 		return PointerGetDatum(tmp);
 	}
 	else
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d..968dd9f7c0 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -19,7 +19,8 @@
 #include "access/toast_internals.h"
 #include "catalog/pg_type_d.h"
 #include "varatt.h"
-
+#include "utils/attoptcache.h"
+#include "access/toast_compression.h"
 
 /*
  * Prepare to TOAST a tuple.
@@ -55,6 +56,18 @@ toast_tuple_init(ToastTupleContext *ttc)
 		ttc->ttc_attr[i].tai_colflags = 0;
 		ttc->ttc_attr[i].tai_oldexternal = NULL;
 		ttc->ttc_attr[i].tai_compression = att->attcompression;
+		ttc->ttc_attr[i].dictid = InvalidDictId;
+		ttc->ttc_attr[i].zstd_level = DEFAULT_ZSTD_LEVEL;
+		if (att->attcompression == TOAST_ZSTD_COMPRESSION)
+		{
+			AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+			if (aopt)
+			{
+				ttc->ttc_attr[i].dictid = (Oid) aopt->dictid;
+				ttc->ttc_attr[i].zstd_level = aopt->zstd_level;
+			}
+		}
 
 		if (ttc->ttc_oldvalues != NULL)
 		{
@@ -230,7 +243,11 @@ toast_tuple_try_compression(ToastTupleContext *ttc, int attribute)
 	Datum		new_value;
 	ToastAttrInfo *attr = &ttc->ttc_attr[attribute];
 
-	new_value = toast_compress_datum(*value, attr->tai_compression);
+	CompressionInfo cmp = {.cmethod = attr->tai_compression,
+		.dictid = attr->dictid,
+	.zstd_level = attr->zstd_level};
+
+	new_value = toast_compress_datum(*value, cmp);
 
 	if (DatumGetPointer(new_value) != NULL)
 	{
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c090094ed0..282afbcef5 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -46,7 +46,8 @@ OBJS = \
 	pg_subscription.o \
 	pg_type.o \
 	storage.o \
-	toasting.o
+	toasting.o \
+	pg_zstd_dictionaries.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bf..493963b1b8 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1071,7 +1071,9 @@ AddNewRelationType(const char *typeName,
 				   -1,			/* typmod */
 				   0,			/* array dimensions for typBaseType */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* rowtypes never have a collation */
+				   InvalidOid,	/* rowtypes never have a collation */
+				   InvalidOid	/* generate dictionary procedure - default */
+		);
 }
 
 /* --------------------------------
@@ -1394,7 +1396,9 @@ heap_create_with_catalog(const char *relname,
 				   -1,			/* typmod */
 				   0,			/* array dimensions for typBaseType */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* rowtypes never have a collation */
+				   InvalidOid,	/* rowtypes never have a collation */
+				   InvalidOid	/* generate dictionary procedure - default */
+			);
 
 		pfree(relarrayname);
 	}
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 1958ea9238..8f0413189c 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -34,6 +34,7 @@ backend_sources += files(
   'pg_type.c',
   'storage.c',
   'toasting.c',
+  'pg_zstd_dictionaries.c',
 )
 
 
diff --git a/src/backend/catalog/pg_type.c b/src/backend/catalog/pg_type.c
index b36f81afb9..bbed8f64ad 100644
--- a/src/backend/catalog/pg_type.c
+++ b/src/backend/catalog/pg_type.c
@@ -120,6 +120,7 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId)
 	values[Anum_pg_type_typtypmod - 1] = Int32GetDatum(-1);
 	values[Anum_pg_type_typndims - 1] = Int32GetDatum(0);
 	values[Anum_pg_type_typcollation - 1] = ObjectIdGetDatum(InvalidOid);
+	values[Anum_pg_type_typebuildzstddictionary - 1] = ObjectIdGetDatum(InvalidOid);
 	nulls[Anum_pg_type_typdefaultbin - 1] = true;
 	nulls[Anum_pg_type_typdefault - 1] = true;
 	nulls[Anum_pg_type_typacl - 1] = true;
@@ -223,7 +224,8 @@ TypeCreate(Oid newTypeOid,
 		   int32 typeMod,
 		   int32 typNDims,		/* Array dimensions for baseType */
 		   bool typeNotNull,
-		   Oid typeCollation)
+		   Oid typeCollation,
+		   Oid generateDictionaryProcedure)
 {
 	Relation	pg_type_desc;
 	Oid			typeObjectId;
@@ -378,6 +380,7 @@ TypeCreate(Oid newTypeOid,
 	values[Anum_pg_type_typtypmod - 1] = Int32GetDatum(typeMod);
 	values[Anum_pg_type_typndims - 1] = Int32GetDatum(typNDims);
 	values[Anum_pg_type_typcollation - 1] = ObjectIdGetDatum(typeCollation);
+	values[Anum_pg_type_typebuildzstddictionary - 1] = ObjectIdGetDatum(generateDictionaryProcedure);
 
 	/*
 	 * initialize the default binary value for this type.  Check for nulls of
@@ -679,6 +682,12 @@ GenerateTypeDependencies(HeapTuple typeTuple,
 		add_exact_object_address(&referenced, addrs_normal);
 	}
 
+	if (OidIsValid(typeForm->typebuildzstddictionary))
+	{
+		ObjectAddressSet(referenced, ProcedureRelationId, typeForm->typebuildzstddictionary);
+		add_exact_object_address(&referenced, addrs_normal);
+	}
+
 	if (OidIsValid(typeForm->typsubscript))
 	{
 		ObjectAddressSet(referenced, ProcedureRelationId, typeForm->typsubscript);
diff --git a/src/backend/catalog/pg_zstd_dictionaries.c b/src/backend/catalog/pg_zstd_dictionaries.c
new file mode 100644
index 0000000000..2d27f58221
--- /dev/null
+++ b/src/backend/catalog/pg_zstd_dictionaries.c
@@ -0,0 +1,566 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "access/heapam.h"
+#include "access/table.h"
+#include "access/relation.h"
+#include "access/tableam.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class_d.h"
+#include "catalog/pg_zstd_dictionaries.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
+#include "catalog/pg_type.h"
+#include "catalog/namespace.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/hsearch.h"
+#include "access/toast_compression.h"
+#include "utils/attoptcache.h"
+#include "parser/analyze.h"
+#include "common/hashfn.h"
+#include "nodes/makefuncs.h"
+#include "access/reloptions.h"
+#include "miscadmin.h"
+#include "access/genam.h"
+#include "executor/tuptable.h"
+#include "access/htup_details.h"
+#include "access/sdir.h"
+#include "utils/lsyscache.h"
+#include "utils/relcache.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
+#include "nodes/pg_list.h"
+
+#ifdef USE_ZSTD
+#include <zstd.h>
+#include <zdict.h>
+#endif
+
+#define TARG_ROWS 1000
+
+typedef struct SampleEntry SampleEntry;
+typedef struct SampleCollector SampleCollector;
+
+/* Structure to store a sample entry */
+struct SampleEntry
+{
+	void	   *data;			/* Pointer to sample data */
+	size_t		size;			/* Size of the sample */
+};
+
+/* Structure to collect samples along with a hash table for deduplication */
+struct SampleCollector
+{
+	SampleEntry *samples;		/* Dynamic array of pointers to SampleEntry */
+	int			sample_count;	/* Number of collected samples */
+};
+
+static bool build_zstd_dictionary_internal(Oid relid, AttrNumber attno);
+static Oid	GetNewDictId(Relation relation, Oid indexId, AttrNumber dictIdColumn);
+
+/* ----------------------------------------------------------------
+ * Zstandard dictionary training related methods
+ * ----------------------------------------------------------------
+ */
+
+/*
+ * build_zstd_dictionary_internal
+ *   1) Validate that the given (relid, attno) can have a Zstd compression enabled on heap relation
+ *   2) Call the type-specific dictionary builder
+ *   3) Train a dictionary via ZDICT_trainFromBuffer()
+ *   4) Insert dictionary into pg_zstd_dictionaries
+ *   5) Update pg_attribute.attoptions with dictid
+ */
+pg_attribute_unused()
+static bool
+build_zstd_dictionary_internal(Oid relid, AttrNumber attno)
+{
+#ifdef USE_ZSTD
+	Relation	catalogRel;
+	TupleDesc	catTupDesc;
+	Oid			dictid;
+	Relation	rel;
+	TupleDesc	tupleDesc;
+	Form_pg_attribute att;
+	AttributeOpts *attopt;
+	HeapTuple	typeTup;
+	Form_pg_type typeForm;
+	Oid			baseTypeOid;
+	Oid			train_func;
+	Datum		dictDatum;
+	ZstdTrainingData *dict;
+	char	   *samples_buffer;
+	size_t	   *sample_sizes;
+	size_t		nitems;
+	uint32		dictionary_size;
+	void	   *dict_data;
+	size_t		dict_size;
+
+	/* ----
+     * 1) Open user relation just to verify it's a normal table and has Zstd compression
+     * ----
+     */
+	rel = table_open(relid, AccessShareLock);
+	if (rel->rd_rel->relkind != RELKIND_RELATION)
+	{
+		table_close(rel, AccessShareLock);
+		return false;			/* not a regular table */
+	}
+
+	/* If the column doesn't use Zstd, nothing to do */
+	tupleDesc = RelationGetDescr(rel);
+	att = TupleDescAttr(tupleDesc, attno - 1);
+	if (att->attcompression != TOAST_ZSTD_COMPRESSION)
+	{
+		table_close(rel, AccessShareLock);
+		return false;
+	}
+
+	/* Check attoptions for user-requested dictionary size, etc. */
+	attopt = get_attribute_options(relid, attno);
+	if (attopt && attopt->zstd_dict_size == 0)
+	{
+		/* user explicitly says "no dictionary needed" */
+		table_close(rel, AccessShareLock);
+		return false;
+	}
+
+	/*
+	 * 2) Look up the type's custom dictionary builder function We'll call it
+	 * to get sample data. Then we can close 'rel' because we don't need it
+	 * open to do the actual Zdict training.
+	 */
+	typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(att->atttypid));
+	if (!HeapTupleIsValid(typeTup))
+	{
+		table_close(rel, AccessShareLock);
+		elog(ERROR, "cache lookup failed for type %u", att->atttypid);
+	}
+	typeForm = (Form_pg_type) GETSTRUCT(typeTup);
+
+	if (typeForm->typlen != -1)
+	{
+		ReleaseSysCache(typeTup);
+		table_close(rel, AccessShareLock);
+		return false;
+	}
+
+	/* Get the base type */
+	baseTypeOid = get_element_type(typeForm->oid);
+	train_func = InvalidOid;
+
+	if (OidIsValid(baseTypeOid))
+	{
+		HeapTuple	baseTypeTup;
+		Form_pg_type baseTypeForm;
+
+		/* It's an array type: get the base type's training function */
+		baseTypeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(baseTypeOid));
+		if (!HeapTupleIsValid(baseTypeTup))
+			ereport(ERROR,
+					(errmsg("Cache lookup failed for base type %u", baseTypeOid)));
+
+		baseTypeForm = (Form_pg_type) GETSTRUCT(baseTypeTup);
+		train_func = baseTypeForm->typebuildzstddictionary;
+		ReleaseSysCache(baseTypeTup);
+	}
+	else
+		train_func = typeForm->typebuildzstddictionary;
+
+	/* If the type does not supply a builder, skip */
+	if (!OidIsValid(train_func))
+	{
+		ReleaseSysCache(typeTup);
+		table_close(rel, AccessShareLock);
+		return false;
+	}
+
+	/* Call the type-specific builder. It should return ZstdTrainingData */
+	dictDatum = OidFunctionCall2(train_func,
+								 PointerGetDatum(rel),	/* pass relation ref */
+								 PointerGetDatum(att));
+	ReleaseSysCache(typeTup);
+
+	/* We no longer need the user relation open */
+	table_close(rel, AccessShareLock);
+
+	dict = (ZstdTrainingData *) DatumGetPointer(dictDatum);
+	if (!dict || dict->nitems == 0)
+		return false;
+
+	/*
+	 * 3) Train a Zstd dictionary in-memory.
+	 */
+	samples_buffer = dict->sample_buffer;
+	sample_sizes = dict->sample_sizes;
+	nitems = dict->nitems;
+
+	dictionary_size = (!attopt ? DEFAULT_ZSTD_DICT_SIZE
+					   : attopt->zstd_dict_size);
+
+	/* Allocate buffer for dictionary training result */
+	dict_data = palloc(dictionary_size);
+	dict_size = ZDICT_trainFromBuffer(dict_data,
+									  dictionary_size,
+									  samples_buffer,
+									  sample_sizes,
+									  nitems);
+	if (ZDICT_isError(dict_size))
+	{
+		elog(LOG, "Zstd dictionary training failed: %s",
+			 ZDICT_getErrorName(dict_size));
+		pfree(dict_data);
+		return false;
+	}
+
+	/* Open the catalog relation with ShareRowExclusiveLock */
+	catalogRel = table_open(ZstdDictionariesRelationId, ShareRowExclusiveLock);
+	catTupDesc = RelationGetDescr(catalogRel);
+	dictid = GetNewDictId(catalogRel, ZstdDictidIndexId, Anum_pg_zstd_dictionaries_dictid);
+
+	/* Now copy that finalized dictionary into a bytea. */
+	{
+		/* We’ll store this bytea in pg_zstd_dictionaries. */
+		Datum		values[Natts_pg_zstd_dictionaries];
+		bool		nulls[Natts_pg_zstd_dictionaries];
+		HeapTuple	tup;
+
+		bytea	   *dict_bytea = (bytea *) palloc(VARHDRSZ + dict_size);
+
+		SET_VARSIZE(dict_bytea, VARHDRSZ + dict_size);
+		memcpy(VARDATA(dict_bytea), dict_data, dict_size);
+
+		MemSet(values, 0, sizeof(values));
+		MemSet(nulls, false, sizeof(nulls));
+
+		values[Anum_pg_zstd_dictionaries_dictid - 1] = ObjectIdGetDatum(dictid);
+		values[Anum_pg_zstd_dictionaries_dict - 1] = PointerGetDatum(dict_bytea);
+
+		tup = heap_form_tuple(catTupDesc, values, nulls);
+		CatalogTupleInsert(catalogRel, tup);
+		heap_freetuple(tup);
+
+		pfree(dict_bytea);
+	}
+
+	pfree(dict_data);
+	pfree(samples_buffer);
+	pfree(sample_sizes);
+	pfree(dict);
+
+	/*
+	 * 5) Update pg_attribute.attoptions with "dictid" => dictid so the column
+	 * knows which dictionary to use at compression time.
+	 */
+	{
+		Relation	attRel = table_open(AttributeRelationId, RowExclusiveLock);
+		HeapTuple	atttup,
+					newtuple;
+		Datum		attoptionsDatum,
+					newOptions;
+		bool		isnull;
+		Datum		repl_val[Natts_pg_attribute];
+		bool		repl_null[Natts_pg_attribute];
+		bool		repl_repl[Natts_pg_attribute];
+		DefElem    *def;
+
+		atttup = SearchSysCacheAttNum(relid, attno);
+		if (!HeapTupleIsValid(atttup))
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column number %d of relation \"%u\" does not exist",
+							attno, relid)));
+
+		/* Build new attoptions with dictid=... */
+		def = makeDefElem("dictid",
+						  (Node *) makeString(psprintf("%u", dictid)),
+						  -1);
+
+		attoptionsDatum = SysCacheGetAttr(ATTNUM, atttup,
+										  Anum_pg_attribute_attoptions,
+										  &isnull);
+		newOptions = transformRelOptions(isnull ? (Datum) 0 : attoptionsDatum,
+										 list_make1(def),
+										 NULL, NULL,
+										 false, false);
+		/* Validate them (throws error if invalid) */
+		(void) attribute_reloptions(newOptions, true);
+
+		MemSet(repl_null, false, sizeof(repl_null));
+		MemSet(repl_repl, false, sizeof(repl_repl));
+
+		if (newOptions != (Datum) 0)
+			repl_val[Anum_pg_attribute_attoptions - 1] = newOptions;
+		else
+			repl_null[Anum_pg_attribute_attoptions - 1] = true;
+
+		repl_repl[Anum_pg_attribute_attoptions - 1] = true;
+
+		newtuple = heap_modify_tuple(atttup,
+									 RelationGetDescr(attRel),
+									 repl_val,
+									 repl_null,
+									 repl_repl);
+
+		CatalogTupleUpdate(attRel, &newtuple->t_self, newtuple);
+		heap_freetuple(newtuple);
+
+		ReleaseSysCache(atttup);
+
+		table_close(attRel, NoLock);
+	}
+
+	/**
+     * Done inserting dictionary and updating attribute.
+     * Unlock the table (locks remain held until transaction commit)
+     */
+	table_close(catalogRel, NoLock);
+
+	return true;
+#else
+	return false;
+#endif
+}
+
+/*
+ * Acquire a new unique DictId for a relation.
+ *
+ * Assumes the relation is already locked with ShareRowExclusiveLock,
+ * ensuring that concurrent transactions cannot generate duplicate DictIds.
+ */
+pg_attribute_unused()
+static Oid
+GetNewDictId(Relation relation, Oid indexId, AttrNumber dictIdColumn)
+{
+	Relation	indexRel = index_open(indexId, AccessShareLock);
+	Oid			maxDictId = InvalidDictId;
+	SysScanDesc scan;
+	HeapTuple	tuple;
+	bool		collision;
+	ScanKeyData key;
+	Oid			newDictId;
+
+	/* Retrieve the maximum existing DictId by scanning in reverse order */
+	scan = systable_beginscan_ordered(relation, indexRel, SnapshotAny, 0, NULL);
+	tuple = systable_getnext_ordered(scan, BackwardScanDirection);
+	if (HeapTupleIsValid(tuple))
+	{
+		Datum		value;
+		bool		isNull;
+
+		value = heap_getattr(tuple, dictIdColumn, RelationGetDescr(relation), &isNull);
+		if (!isNull)
+			maxDictId = DatumGetObjectId(value);
+	}
+	systable_endscan(scan);
+
+	newDictId = maxDictId + 1;
+	Assert(newDictId != InvalidDictId);
+
+	/* Check that the new DictId is indeed unique */
+	ScanKeyInit(&key,
+				dictIdColumn,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(newDictId));
+
+	scan = systable_beginscan(relation, indexRel->rd_id, true,
+							  SnapshotAny, 1, &key);
+	collision = HeapTupleIsValid(systable_getnext(scan));
+	systable_endscan(scan);
+
+	if (collision)
+		ereport(ERROR,
+				(errcode(ERRCODE_INTERNAL_ERROR),
+				 errmsg("unexpected collision for new DictId %d", newDictId)));
+
+	return newDictId;
+}
+
+/*
+ * get_zstd_dict - Fetches the ZSTD dictionary from the catalog
+ *
+ * dictid: The Oid of the dictionary to fetch.
+ *
+ * Returns: A pointer to a bytea containing the dictionary data.
+ */
+bytea *
+get_zstd_dict(Oid dictid)
+{
+	HeapTuple	tuple;
+	Datum		datum;
+	bool		isNull;
+	bytea	   *dict_bytea;
+	bytea	   *result;
+	Size		bytea_len;
+
+	/* Fetch the dictionary tuple from the syscache */
+	tuple = SearchSysCache1(ZSTDDICTIDOID, ObjectIdGetDatum(dictid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR, (errmsg("Cache lookup failed for dictid %u", dictid)));
+
+	/* Get the dictionary attribute from the tuple */
+	datum = SysCacheGetAttr(ATTNUM, tuple, Anum_pg_zstd_dictionaries_dict, &isNull);
+	if (isNull)
+		ereport(ERROR, (errmsg("Dictionary not found for dictid %u", dictid)));
+
+	dict_bytea = DatumGetByteaP(datum);
+	if (dict_bytea == NULL)
+		ereport(ERROR, (errmsg("Failed to fetch dictionary")));
+
+	/* Determine the total size of the bytea (header + data) */
+	bytea_len = VARSIZE(dict_bytea);
+
+	result = palloc(bytea_len);
+	memcpy(result, dict_bytea, bytea_len);
+
+	/* Release the syscache tuple; the returned bytea is now independent */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * zstd_dictionary_builder
+ *    Acquire samples from a column, store them in a SampleCollector,
+ *    filter them, then build a ZstdTrainingData struct.
+ */
+Datum
+zstd_dictionary_builder(PG_FUNCTION_ARGS)
+{
+	ZstdTrainingData *dict = palloc0(sizeof(ZstdTrainingData));
+	Relation	rel = (Relation) PG_GETARG_POINTER(0);
+	Form_pg_attribute att = (Form_pg_attribute) PG_GETARG_POINTER(1);
+	TupleDesc	tupleDesc = RelationGetDescr(rel);
+
+	/* Acquire up to TARG_ROWS sample rows. */
+	HeapTuple  *sample_rows = palloc(TARG_ROWS * sizeof(HeapTuple));
+	double		totalrows = 0,
+				totaldeadrows = 0;
+	int			num_sampled = acquire_sample_rows(rel, 0, sample_rows,
+												  TARG_ROWS,
+												  &totalrows,
+												  &totaldeadrows);
+
+	/* Create a collector to accumulate raw varlena samples. */
+	size_t		filtered_sample_count = 0;
+	size_t		filtered_samples_size = 0;
+	char	   *samples_buffer;
+	size_t	   *sample_sizes;
+	size_t		current_offset;
+	SampleCollector *collector;
+
+	if (num_sampled == 0)
+	{
+		pfree(sample_rows);
+		/* No samples were collected. */
+		PG_RETURN_POINTER(dict);
+	}
+
+	collector = palloc(sizeof(SampleCollector));
+	collector->samples = palloc(num_sampled * sizeof(SampleEntry));
+	collector->sample_count = 0;
+
+	/* Extract column data from each sampled row. */
+	for (int i = 0; i < num_sampled; i++)
+	{
+		bool		isnull;
+		Datum		value;
+
+		CHECK_FOR_INTERRUPTS();
+
+		value = heap_getattr(sample_rows[i],
+							 att->attnum,
+							 tupleDesc,
+							 &isnull);
+		if (!isnull)
+		{
+			struct varlena *attr;
+			size_t		size;
+			void	   *data;
+			SampleEntry entry;
+			int			idx;
+
+			attr = (struct varlena *) PG_DETOAST_DATUM(value);
+			size = VARSIZE_ANY_EXHDR(attr);
+
+			if (filtered_samples_size + size > MaxAllocSize)
+				break;
+
+			data = palloc(size);
+			memcpy(data, VARDATA_ANY(attr), size);
+
+			entry.data = data;
+			entry.size = size;
+
+			idx = collector->sample_count;
+			collector->samples[idx] = entry;
+			collector->sample_count++;
+
+			filtered_samples_size += size;
+			filtered_sample_count++;
+		}
+	}
+
+	if (filtered_sample_count == 0)
+	{
+		pfree(sample_rows);
+		pfree(collector->samples);
+		pfree(collector);
+		/* No samples were collected, or they were too large. */
+		PG_RETURN_POINTER(dict);
+	}
+
+	/* Allocate a buffer for all sample data, plus an array of sample sizes. */
+	samples_buffer = palloc(filtered_samples_size);
+	sample_sizes = palloc(filtered_sample_count * sizeof(size_t));
+
+	/*
+	 * Concatenate the samples into samples_buffer, recording each sample's
+	 * size in sample_sizes.
+	 */
+	current_offset = 0;
+	for (int i = 0; i < filtered_sample_count; i++)
+	{
+		memcpy(samples_buffer + current_offset,
+			   collector->samples[i].data,
+			   collector->samples[i].size);
+
+		sample_sizes[i] = collector->samples[i].size;
+		current_offset += collector->samples[i].size;
+
+		pfree(collector->samples[i].data);
+	}
+	pfree(sample_rows);
+	pfree(collector->samples);
+	pfree(collector);
+
+	dict->sample_buffer = samples_buffer;
+	dict->sample_sizes = sample_sizes;
+	dict->nitems = filtered_sample_count;
+
+	PG_RETURN_POINTER(dict);
+}
+
+Datum
+build_zstd_dict_for_attribute(PG_FUNCTION_ARGS)
+{
+#ifndef USE_ZSTD
+	PG_RETURN_BOOL(false);
+#else
+	text	   *tablename = PG_GETARG_TEXT_PP(0);
+	RangeVar   *tablerel;
+	Oid			tableoid = InvalidOid;
+	AttrNumber	attno = PG_GETARG_INT32(1);
+	bool		success;
+
+	/* Look up table name. */
+	tablerel = makeRangeVarFromNameList(textToQualifiedNameList(tablename));
+	tableoid = RangeVarGetRelid(tablerel, NoLock, false);
+	success = build_zstd_dictionary_internal(tableoid, attno);
+	PG_RETURN_BOOL(success);
+#endif
+}
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 2b5fbdcbd8..2b5500f45f 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -55,7 +55,7 @@
 #include "utils/sortsupport.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
-
+#include "parser/analyze.h"
 
 /* Per-index data for ANALYZE */
 typedef struct AnlIndexData
@@ -85,9 +85,6 @@ static void compute_index_stats(Relation onerel, double totalrows,
 								MemoryContext col_context);
 static VacAttrStats *examine_attribute(Relation onerel, int attnum,
 									   Node *index_expr);
-static int	acquire_sample_rows(Relation onerel, int elevel,
-								HeapTuple *rows, int targrows,
-								double *totalrows, double *totaldeadrows);
 static int	compare_rows(const void *a, const void *b, void *arg);
 static int	acquire_inherited_sample_rows(Relation onerel, int elevel,
 										  HeapTuple *rows, int targrows,
@@ -1195,7 +1192,7 @@ block_sampling_read_stream_next(ReadStream *stream,
  * block.  The previous sampling method put too much credence in the row
  * density near the start of the table.
  */
-static int
+int
 acquire_sample_rows(Relation onerel, int elevel,
 					HeapTuple *rows, int targrows,
 					double *totalrows, double *totaldeadrows)
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 3cb3ca1cca..c583e48167 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -95,6 +95,7 @@ typedef struct
 	bool		updateTypmodout;
 	bool		updateAnalyze;
 	bool		updateSubscript;
+	bool		updateGenerateDictionary;
 	/* New values for relevant attributes */
 	char		storage;
 	Oid			receiveOid;
@@ -103,6 +104,7 @@ typedef struct
 	Oid			typmodoutOid;
 	Oid			analyzeOid;
 	Oid			subscriptOid;
+	Oid			buildZstdDictionary;
 } AlterTypeRecurseParams;
 
 /* Potentially set by pg_upgrade_support functions */
@@ -122,6 +124,7 @@ static Oid	findTypeSendFunction(List *procname, Oid typeOid);
 static Oid	findTypeTypmodinFunction(List *procname);
 static Oid	findTypeTypmodoutFunction(List *procname);
 static Oid	findTypeAnalyzeFunction(List *procname, Oid typeOid);
+static Oid	findTypeGenerateDictionaryFunction(List *procname, Oid typeOid);
 static Oid	findTypeSubscriptingFunction(List *procname, Oid typeOid);
 static Oid	findRangeSubOpclass(List *opcname, Oid subtype);
 static Oid	findRangeCanonicalFunction(List *procname, Oid typeOid);
@@ -162,6 +165,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	List	   *typmodoutName = NIL;
 	List	   *analyzeName = NIL;
 	List	   *subscriptName = NIL;
+	List	   *generateDictionaryName = NIL;
 	char		category = TYPCATEGORY_USER;
 	bool		preferred = false;
 	char		delimiter = DEFAULT_TYPDELIM;
@@ -190,6 +194,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	DefElem    *alignmentEl = NULL;
 	DefElem    *storageEl = NULL;
 	DefElem    *collatableEl = NULL;
+	DefElem    *generateDictionaryEl = NULL;
 	Oid			inputOid;
 	Oid			outputOid;
 	Oid			receiveOid = InvalidOid;
@@ -198,6 +203,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	Oid			typmodoutOid = InvalidOid;
 	Oid			analyzeOid = InvalidOid;
 	Oid			subscriptOid = InvalidOid;
+	Oid			buildZstdDictionary = InvalidOid;
 	char	   *array_type;
 	Oid			array_oid;
 	Oid			typoid;
@@ -323,6 +329,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 			defelp = &storageEl;
 		else if (strcmp(defel->defname, "collatable") == 0)
 			defelp = &collatableEl;
+		else if (strcmp(defel->defname, "build_zstd_dict") == 0)
+			defelp = &generateDictionaryEl;
 		else
 		{
 			/* WARNING, not ERROR, for historical backwards-compatibility */
@@ -455,6 +463,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	}
 	if (collatableEl)
 		collation = defGetBoolean(collatableEl) ? DEFAULT_COLLATION_OID : InvalidOid;
+	if (generateDictionaryEl)
+		generateDictionaryName = defGetQualifiedName(generateDictionaryEl);
 
 	/*
 	 * make sure we have our required definitions
@@ -516,6 +526,15 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 					 errmsg("element type cannot be specified without a subscripting function")));
 	}
 
+	if (generateDictionaryName)
+	{
+		if (internalLength != -1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("type build_zstd_dict function must be specified only if data type is variable length.")));
+		buildZstdDictionary = findTypeGenerateDictionaryFunction(generateDictionaryName, typoid);
+	}
+
 	/*
 	 * Check permissions on functions.  We choose to require the creator/owner
 	 * of a type to also own the underlying functions.  Since creating a type
@@ -550,6 +569,9 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	if (analyzeOid && !object_ownercheck(ProcedureRelationId, analyzeOid, GetUserId()))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
 					   NameListToString(analyzeName));
+	if (buildZstdDictionary && !object_ownercheck(ProcedureRelationId, buildZstdDictionary, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
+					   NameListToString(generateDictionaryName));
 	if (subscriptOid && !object_ownercheck(ProcedureRelationId, subscriptOid, GetUserId()))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
 					   NameListToString(subscriptName));
@@ -601,7 +623,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array Dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   collation);	/* type's collation */
+				   collation,	/* type's collation */
+				   buildZstdDictionary);	/* build_zstd_dict procedure */
 	Assert(typoid == address.objectId);
 
 	/*
@@ -643,7 +666,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   collation);		/* type's collation */
+			   collation,		/* type's collation */
+			   InvalidOid);		/* build_zstd_dict procedure */
 
 	pfree(array_type);
 
@@ -706,6 +730,7 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 	Oid			receiveProcedure;
 	Oid			sendProcedure;
 	Oid			analyzeProcedure;
+	Oid			buildZstdDictionary;
 	bool		byValue;
 	char		category;
 	char		delimiter;
@@ -842,6 +867,9 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 	/* Analysis function */
 	analyzeProcedure = baseType->typanalyze;
 
+	/* Generate dictionary function */
+	buildZstdDictionary = baseType->typebuildzstddictionary;
+
 	/*
 	 * Domains don't need a subscript function, since they are not
 	 * subscriptable on their own.  If the base type is subscriptable, the
@@ -1078,7 +1106,8 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 				   basetypeMod, /* typeMod value */
 				   typNDims,	/* Array dimensions for base type */
 				   typNotNull,	/* Type NOT NULL */
-				   domaincoll); /* type's collation */
+				   domaincoll,	/* type's collation */
+				   buildZstdDictionary);	/* build_zstd_dict procedure */
 
 	/*
 	 * Create the array type that goes with it.
@@ -1119,7 +1148,8 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   domaincoll);		/* type's collation */
+			   domaincoll,		/* type's collation */
+			   InvalidOid);		/* build_zstd_dict procedure */
 
 	pfree(domainArrayName);
 
@@ -1241,7 +1271,8 @@ DefineEnum(CreateEnumStmt *stmt)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* type's collation */
+				   InvalidOid,	/* type's collation */
+				   InvalidOid); /* generate dictionary procedure - default */
 
 	/* Enter the enum's values into pg_enum */
 	EnumValuesCreate(enumTypeAddr.objectId, stmt->vals);
@@ -1282,7 +1313,8 @@ DefineEnum(CreateEnumStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   InvalidOid);		/* type's collation */
+			   InvalidOid,		/* type's collation */
+			   InvalidOid);		/* generate dictionary procedure - default */
 
 	pfree(enumArrayName);
 
@@ -1583,7 +1615,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* type's collation (ranges never have one) */
+				   InvalidOid,	/* type's collation (ranges never have one) */
+				   InvalidOid); /* generate dictionary procedure - default */
 	Assert(typoid == InvalidOid || typoid == address.objectId);
 	typoid = address.objectId;
 
@@ -1650,7 +1683,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* type's collation (ranges never have one) */
+				   InvalidOid,	/* type's collation (ranges never have one) */
+				   InvalidOid); /* generate dictionary procedure - default */
 	Assert(multirangeOid == mltrngaddress.objectId);
 
 	/* Create the entry in pg_range */
@@ -1693,7 +1727,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   InvalidOid);		/* typcollation */
+			   InvalidOid,		/* typcollation */
+			   InvalidOid);		/* generate dictionary procedure - default */
 
 	pfree(rangeArrayName);
 
@@ -1732,7 +1767,8 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   InvalidOid);		/* typcollation */
+			   InvalidOid,		/* typcollation */
+			   InvalidOid);		/* generate dictionary procedure - default */
 
 	/* And create the constructor functions for this range type */
 	makeRangeConstructors(typeName, typeNamespace, typoid, rangeSubtype);
@@ -2257,6 +2293,31 @@ findTypeAnalyzeFunction(List *procname, Oid typeOid)
 	return procOid;
 }
 
+static Oid
+findTypeGenerateDictionaryFunction(List *procname, Oid typeOid)
+{
+	Oid			argList[2];
+	Oid			procOid;
+
+	argList[0] = OIDOID;
+	argList[1] = INT4OID;
+
+	procOid = LookupFuncName(procname, 2, argList, true);
+	if (!OidIsValid(procOid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("function %s does not exist",
+						func_signature_string(procname, 1, NIL, argList))));
+
+	if (get_func_rettype(procOid) != INTERNALOID)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("type build zstd dictionary function %s must return type %s",
+						NameListToString(procname), "internal")));
+
+	return procOid;
+}
+
 static Oid
 findTypeSubscriptingFunction(List *procname, Oid typeOid)
 {
@@ -4440,6 +4501,19 @@ AlterType(AlterTypeStmt *stmt)
 			/* Replacing a subscript function requires superuser. */
 			requireSuper = true;
 		}
+		else if (strcmp(defel->defname, "build_zstd_dict") == 0)
+		{
+			if (defel->arg != NULL)
+				atparams.buildZstdDictionary =
+					findTypeGenerateDictionaryFunction(defGetQualifiedName(defel),
+													   typeOid);
+			else
+				atparams.buildZstdDictionary = InvalidOid;	/* NONE, remove function */
+
+			atparams.updateGenerateDictionary = true;
+			/* Replacing a canonical function requires superuser. */
+			requireSuper = true;
+		}
 
 		/*
 		 * The rest of the options that CREATE accepts cannot be changed.
@@ -4602,6 +4676,11 @@ AlterTypeRecurse(Oid typeOid, bool isImplicitArray,
 		replaces[Anum_pg_type_typsubscript - 1] = true;
 		values[Anum_pg_type_typsubscript - 1] = ObjectIdGetDatum(atparams->subscriptOid);
 	}
+	if (atparams->updateGenerateDictionary)
+	{
+		replaces[Anum_pg_type_typebuildzstddictionary - 1] = true;
+		values[Anum_pg_type_typebuildzstddictionary - 1] = ObjectIdGetDatum(atparams->buildZstdDictionary);
+	}
 
 	newtup = heap_modify_tuple(tup, RelationGetDescr(catalog),
 							   values, nulls, replaces);
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index cdf185ea00..36f32f8590 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5280,6 +5280,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_COMPRESSION_ID:
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index ad25cbb39c..e03ac8dddc 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -453,6 +453,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd", TOAST_ZSTD_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2d1de9c37b..47773e2919 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -731,7 +731,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 4f4ad2ee15..c2638fe4d8 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8965,7 +8965,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
 						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
-						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
+						 "array_to_string(ARRAY(SELECT x FROM unnest(a.attoptions) AS x \n"
+						 "WHERE x NOT LIKE 'dictid=%'), ', ') AS attoptions, \n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
 						 "pg_catalog.array_to_string(ARRAY("
@@ -11784,12 +11785,14 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 	char	   *typmodout;
 	char	   *typanalyze;
 	char	   *typsubscript;
+	char	   *typebuildzstddictionary;
 	Oid			typreceiveoid;
 	Oid			typsendoid;
 	Oid			typmodinoid;
 	Oid			typmodoutoid;
 	Oid			typanalyzeoid;
 	Oid			typsubscriptoid;
+	Oid			typebuildzstddictionaryoid;
 	char	   *typcategory;
 	char	   *typispreferred;
 	char	   *typdelim;
@@ -11822,10 +11825,18 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 		if (fout->remoteVersion >= 140000)
 			appendPQExpBufferStr(query,
 								 "typsubscript, "
-								 "typsubscript::pg_catalog.oid AS typsubscriptoid ");
+								 "typsubscript::pg_catalog.oid AS typsubscriptoid, ");
 		else
 			appendPQExpBufferStr(query,
-								 "'-' AS typsubscript, 0 AS typsubscriptoid ");
+								 "'-' AS typsubscript, 0 AS typsubscriptoid, ");
+
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query,
+								 "typebuildzstddictionary, "
+								 "typebuildzstddictionary::pg_catalog.oid AS typebuildzstddictionaryoid ");
+		else
+			appendPQExpBufferStr(query,
+								 "'-' AS typebuildzstddictionary, 0 AS typebuildzstddictionaryoid ");
 
 		appendPQExpBufferStr(query, "FROM pg_catalog.pg_type "
 							 "WHERE oid = $1");
@@ -11850,12 +11861,14 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 	typmodout = PQgetvalue(res, 0, PQfnumber(res, "typmodout"));
 	typanalyze = PQgetvalue(res, 0, PQfnumber(res, "typanalyze"));
 	typsubscript = PQgetvalue(res, 0, PQfnumber(res, "typsubscript"));
+	typebuildzstddictionary = PQgetvalue(res, 0, PQfnumber(res, "typebuildzstddictionary"));
 	typreceiveoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typreceiveoid")));
 	typsendoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typsendoid")));
 	typmodinoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typmodinoid")));
 	typmodoutoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typmodoutoid")));
 	typanalyzeoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typanalyzeoid")));
 	typsubscriptoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typsubscriptoid")));
+	typebuildzstddictionaryoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typebuildzstddictionaryoid")));
 	typcategory = PQgetvalue(res, 0, PQfnumber(res, "typcategory"));
 	typispreferred = PQgetvalue(res, 0, PQfnumber(res, "typispreferred"));
 	typdelim = PQgetvalue(res, 0, PQfnumber(res, "typdelim"));
@@ -11911,7 +11924,8 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 		appendPQExpBuffer(q, ",\n    TYPMOD_OUT = %s", typmodout);
 	if (OidIsValid(typanalyzeoid))
 		appendPQExpBuffer(q, ",\n    ANALYZE = %s", typanalyze);
-
+	if (OidIsValid(typebuildzstddictionaryoid))
+		appendPQExpBuffer(q, ",\n    BUILD_ZSTD_DICT = %s", typebuildzstddictionary);
 	if (strcmp(typcollatable, "t") == 0)
 		appendPQExpBufferStr(q, ",\n    COLLATABLE = true");
 
@@ -17170,6 +17184,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 					case 'l':
 						cmname = "lz4";
 						break;
+					case 'z':
+						cmname = "zstd";
+						break;
 					default:
 						cmname = NULL;
 						break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9..0ba37bb175 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2167,8 +2167,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'z' ? "zstd" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612cee..ddea1e0bcd 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -38,7 +38,8 @@ typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_ZSTD_COMPRESSION_ID = 2,
+	TOAST_INVALID_COMPRESSION_ID = 3
 } ToastCompressionId;
 
 /*
@@ -48,10 +49,15 @@ typedef enum ToastCompressionId
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_COMPRESSION			'z'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
 
+#define InvalidDictId						0
+#define DEFAULT_ZSTD_LEVEL				3	/* Reffered from
+												 * ZSTD_CLEVEL_DEFAULT */
+#define DEFAULT_ZSTD_DICT_SIZE 				(4 * 1024)	/* 4 KB */
 
 /* pglz compression/decompression routines */
 extern struct varlena *pglz_compress_datum(const struct varlena *value);
@@ -65,6 +71,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd compression/decompression routines */
+extern struct varlena *zstd_compress_datum(const struct varlena *value, Oid dictid, int zstd_level);
+extern struct varlena *zstd_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb..08bf3dfc67 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -33,6 +33,8 @@ typedef struct
 	int32		tai_size;
 	uint8		tai_colflags;
 	char		tai_compression;
+	Oid			dictid;
+	int			zstd_level;
 } ToastAttrInfo;
 
 /*
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1..9ba6a1e64a 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -27,25 +27,54 @@ typedef struct toast_compress_header
 								 * external size; see va_extinfo */
 } toast_compress_header;
 
+typedef struct toast_compress_header_ext
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	uint32		tcinfo;			/* 2 bits for compression method and 30 bits
+								 * external size; see va_extinfo */
+	uint32		ext_alg;		/* compression method */
+	uint32		dictid;			/* Dictionary Id */
+}			toast_compress_header_ext;
+
+typedef struct CompressionInfo
+{
+	char		cmethod;
+	Oid			dictid;
+	int			zstd_level;		/* ZSTD compression level */
+}			CompressionInfo;
+
 /*
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
 #define TOAST_COMPRESS_EXTSIZE(ptr) \
 	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
-
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
-	do { \
-		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
-		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+#define TOAST_COMPRESS_METHOD(PTR)                       													  		\
+	( ((((toast_compress_header *) (PTR))->tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG ) 	\
+		? (((toast_compress_header_ext *) (PTR))->ext_alg)     													 		\
+		: ( (((toast_compress_header *) (PTR))->tcinfo) >> VARLENA_EXTSIZE_BITS ) )
+
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method, dictid) 								\
+	do { 																										\
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); 													\
+		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || 														\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID || 														\
+				(cm_method) == TOAST_ZSTD_COMPRESSION_ID); 														\
+		/* If the compression method is less than TOAST_ZSTD_COMPRESSION_ID, don't use ext_alg */ 				\
+		if ((cm_method) < TOAST_ZSTD_COMPRESSION_ID) { 															\
+			((toast_compress_header *) (ptr))->tcinfo = 														\
+				(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); 										\
+		} else { 																								\
+			/* For compression methods after lz4, use 'VARLENA_EXTENDED_COMPRESSION_FLAG' 						\
+				in the top bits of tcinfo to indicate compression algorithm is stored in ext_alg.	*/			\
+			((toast_compress_header_ext *) (ptr))->tcinfo = 												\
+			(len) | ((uint32)VARLENA_EXTENDED_COMPRESSION_FLAG << VARLENA_EXTSIZE_BITS); 						\
+			((toast_compress_header_ext *) (ptr))->ext_alg = (cm_method); 									\
+			((toast_compress_header_ext *) (ptr))->dictid = (dictid);										\
+		} 																										\
 	} while (0)
 
-extern Datum toast_compress_datum(Datum value, char cmethod);
+extern Datum toast_compress_datum(Datum value, CompressionInfo cmp);
 extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 2bbc7805fe..1ecd76dd31 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -81,7 +81,8 @@ CATALOG_HEADERS := \
 	pg_publication_namespace.h \
 	pg_publication_rel.h \
 	pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h \
+	pg_zstd_dictionaries.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
 
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index f427a89618..7cea56c2d5 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202503071
+#define CATALOG_VERSION_NO	202503081
 
 #endif
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index ec1cf467f6..e9cb6d911c 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -69,6 +69,7 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_zstd_dictionaries.h',
 ]
 
 # The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index cede992b6e..2bcae1eb52 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12469,4 +12469,14 @@
   proargtypes => 'int4',
   prosrc => 'gist_stratnum_common' },
 
+# ZSTD generate dictionary training functions
+{ oid => '9241', descr => 'ZSTD generate dictionary support',
+  proname => 'zstd_dictionary_builder', prorettype => 'internal',
+  proargtypes => 'internal internal',
+  prosrc => 'zstd_dictionary_builder' },
+
+{ oid => '9242', descr => 'Build zstd dictionaries for a column.',
+  proname => 'build_zstd_dict_for_attribute', prorettype => 'bool',
+  proargtypes => 'text int4',
+  prosrc => 'build_zstd_dict_for_attribute' },
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 6dca77e0a2..58a389a78c 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -40,7 +40,7 @@
   descr => 'variable-length string, binary values escaped',
   typname => 'bytea', typlen => '-1', typbyval => 'f', typcategory => 'U',
   typinput => 'byteain', typoutput => 'byteaout', typreceive => 'bytearecv',
-  typsend => 'byteasend', typalign => 'i', typstorage => 'x' },
+  typsend => 'byteasend', typalign => 'i', typstorage => 'x', typebuildzstddictionary => 'zstd_dictionary_builder' },
 { oid => '18', array_type_oid => '1002', descr => 'single character',
   typname => 'char', typlen => '1', typbyval => 't', typcategory => 'Z',
   typinput => 'charin', typoutput => 'charout', typreceive => 'charrecv',
@@ -83,7 +83,7 @@
   typname => 'text', typlen => '-1', typbyval => 'f', typcategory => 'S',
   typispreferred => 't', typinput => 'textin', typoutput => 'textout',
   typreceive => 'textrecv', typsend => 'textsend', typalign => 'i',
-  typstorage => 'x', typcollation => 'default' },
+  typstorage => 'x', typebuildzstddictionary => 'zstd_dictionary_builder', typcollation => 'default' },
 { oid => '26', array_type_oid => '1028',
   descr => 'object identifier(oid), maximum 4 billion',
   typname => 'oid', typlen => '4', typbyval => 't', typcategory => 'N',
@@ -446,7 +446,7 @@
   typname => 'jsonb', typlen => '-1', typbyval => 'f', typcategory => 'U',
   typsubscript => 'jsonb_subscript_handler', typinput => 'jsonb_in',
   typoutput => 'jsonb_out', typreceive => 'jsonb_recv', typsend => 'jsonb_send',
-  typalign => 'i', typstorage => 'x' },
+  typalign => 'i', typstorage => 'x', typebuildzstddictionary => 'zstd_dictionary_builder' },
 { oid => '4072', array_type_oid => '4073', descr => 'JSON path',
   typname => 'jsonpath', typlen => '-1', typbyval => 'f', typcategory => 'U',
   typinput => 'jsonpath_in', typoutput => 'jsonpath_out',
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index ff666711a5..bd82da8a88 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -227,6 +227,11 @@ CATALOG(pg_type,1247,TypeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(71,TypeRelati
 	 */
 	Oid			typcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation);
 
+	/*
+	 * Custom generate dictionary procedure for the datatype (0 selects the
+	 * default).
+	 */
+	regproc		typebuildzstddictionary BKI_DEFAULT(-) BKI_ARRAY_DEFAULT(-) BKI_LOOKUP_OPT(pg_proc);
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 
 	/*
@@ -380,7 +385,8 @@ extern ObjectAddress TypeCreate(Oid newTypeOid,
 								int32 typeMod,
 								int32 typNDims,
 								bool typeNotNull,
-								Oid typeCollation);
+								Oid typeCollation,
+								Oid generateDictionaryProcedure);
 
 extern void GenerateTypeDependencies(HeapTuple typeTuple,
 									 Relation typeCatalog,
diff --git a/src/include/catalog/pg_zstd_dictionaries.h b/src/include/catalog/pg_zstd_dictionaries.h
new file mode 100644
index 0000000000..5b0b729283
--- /dev/null
+++ b/src/include/catalog/pg_zstd_dictionaries.h
@@ -0,0 +1,53 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_zstd_dictionaries.h
+ *	  definition of the "zstd dictionay" system catalog (pg_zstd_dictionaries)
+ *
+ * src/include/catalog/pg_zstd_dictionaries.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_ZSTD_DICTIONARIES_H
+#define PG_ZSTD_DICTIONARIES_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
+
+/* ----------------
+ *		pg_zstd_dictionaries definition.  cpp turns this into
+ *		typedef struct FormData_pg_zstd_dictionaries
+ * ----------------
+ */
+CATALOG(pg_zstd_dictionaries,9946,ZstdDictionariesRelationId)
+{
+	Oid			dictid BKI_FORCE_NOT_NULL;
+
+	/*
+	 * variable-length fields start here, but we allow direct access to dict
+	 */
+	bytea		dict BKI_FORCE_NOT_NULL;
+} FormData_pg_zstd_dictionaries;
+
+/* Pointer type to a tuple with the format of pg_zstd_dictionaries relation */
+typedef FormData_pg_zstd_dictionaries *Form_pg_zstd_dictionaries;
+
+DECLARE_TOAST(pg_zstd_dictionaries, 9947, 9948);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_zstd_dictionaries_dictid_index, 9949, ZstdDictidIndexId, pg_zstd_dictionaries, btree(dictid oid_ops));
+
+MAKE_SYSCACHE(ZSTDDICTIDOID, pg_zstd_dictionaries_dictid_index, 128);
+
+typedef struct ZstdTrainingData
+{
+	char	   *sample_buffer;	/* Pointer to the raw sample buffer */
+	size_t	   *sample_sizes;	/* Array of sample sizes */
+	size_t		nitems;			/* Number of sample sizes */
+} ZstdTrainingData;
+
+extern bytea *get_zstd_dict(Oid dictid);
+
+#endif							/* PG_ZSTD_DICTIONARIES_H */
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index f1bd18c49f..e494436870 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -17,6 +17,7 @@
 #include "nodes/params.h"
 #include "nodes/queryjumble.h"
 #include "parser/parse_node.h"
+#include "access/htup.h"
 
 /* Hook for plugins to get control at end of parse analysis */
 typedef void (*post_parse_analyze_hook_type) (ParseState *pstate,
@@ -64,4 +65,8 @@ extern List *BuildOnConflictExcludedTargetlist(Relation targetrel,
 
 extern SortGroupClause *makeSortGroupClauseForSetOp(Oid rescoltype, bool require_hash);
 
+extern int	acquire_sample_rows(Relation onerel, int elevel,
+								HeapTuple *rows, int targrows,
+								double *totalrows, double *totaldeadrows);
+
 #endif							/* ANALYZE_H */
diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h
index f684a772af..55a6ac6167 100644
--- a/src/include/utils/attoptcache.h
+++ b/src/include/utils/attoptcache.h
@@ -21,6 +21,12 @@ typedef struct AttributeOpts
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	float8		n_distinct;
 	float8		n_distinct_inherited;
+	double		dictid;			/* Oid is a 32-bit unsigned integer, but
+								 * relopt_int is limited to INT_MAX, so it
+								 * cannot represent the full range of Oid
+								 * values. */
+	int			zstd_dict_size;
+	int			zstd_level;
 } AttributeOpts;
 
 extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d499..5fb0ab8beb 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -42,8 +42,9 @@ typedef struct varatt_external
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
  */
-#define VARLENA_EXTSIZE_BITS	30
-#define VARLENA_EXTSIZE_MASK	((1U << VARLENA_EXTSIZE_BITS) - 1)
+#define VARLENA_EXTSIZE_BITS				30
+#define VARLENA_EXTSIZE_MASK				((1U << VARLENA_EXTSIZE_BITS) - 1)
+#define VARLENA_EXTENDED_COMPRESSION_FLAG	0x3
 
 /*
  * struct varatt_indirect is a "TOAST pointer" representing an out-of-line
@@ -122,6 +123,14 @@ typedef union
 								 * compression method; see va_extinfo */
 		char		va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
 	}			va_compressed;
+	struct
+	{
+		uint32		va_header;
+		uint32		va_tcinfo;
+		uint32		va_cmp_alg;
+		uint32		va_cmp_dictid;
+		char		va_data[FLEXIBLE_ARRAY_MEMBER];
+	}			va_compressed_ext;
 } varattrib_4b;
 
 typedef struct
@@ -242,7 +251,14 @@ typedef struct
 #endif							/* WORDS_BIGENDIAN */
 
 #define VARDATA_4B(PTR)		(((varattrib_4b *) (PTR))->va_4byte.va_data)
-#define VARDATA_4B_C(PTR)	(((varattrib_4b *) (PTR))->va_compressed.va_data)
+/*
+ * If va_tcinfo >> VARLENA_EXTSIZE_BITS == VARLENA_EXTENDED_COMPRESSION_FLAG
+ * use va_compressed_ext; otherwise, use the va_compressed.
+ */
+#define VARDATA_4B_C(PTR)                                                   								  \
+( (((varattrib_4b *)(PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG \
+  ? ((varattrib_4b *)(PTR))->va_compressed_ext.va_data                        								  \
+  : ((varattrib_4b *)(PTR))->va_compressed.va_data )
 #define VARDATA_1B(PTR)		(((varattrib_1b *) (PTR))->va_data)
 #define VARDATA_1B_E(PTR)	(((varattrib_1b_e *) (PTR))->va_data)
 
@@ -252,6 +268,7 @@ typedef struct
 
 #define VARHDRSZ_EXTERNAL		offsetof(varattrib_1b_e, va_data)
 #define VARHDRSZ_COMPRESSED		offsetof(varattrib_4b, va_compressed.va_data)
+#define VARHDRSZ_COMPRESSED_EXT	offsetof(varattrib_4b, va_compressed_ext.va_data)
 #define VARHDRSZ_SHORT			offsetof(varattrib_1b, va_data)
 
 #define VARATT_SHORT_MAX		0x7F
@@ -327,8 +344,20 @@ typedef struct
 /* Decompressed size and compression method of a compressed-in-line Datum */
 #define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
-#define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+/*
+ *  - "Extended" format is indicated by (va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG
+ *  - For the non-extended formats, the method code is stored in the top bits of va_tcinfo.
+ *  - In the extended format, the method code is stored in va_cmp_alg instead.
+ */
+#define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR)                       										  		\
+( ((((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG ) 	\
+  ? (((varattrib_4b *) (PTR))->va_compressed_ext.va_cmp_alg)     												  		\
+  : ( (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo) >> VARLENA_EXTSIZE_BITS))
+
+#define VARDATA_COMPRESSED_GET_DICTID(PTR)                       										  					\
+  ( ((((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG ) 	\
+	? (((varattrib_4b *) (PTR))->va_compressed_ext.va_cmp_dictid)     												  		\
+	: InvalidDictId)
 
 /* Same for external Datums; but note argument is a struct varatt_external */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
@@ -336,13 +365,27 @@ typedef struct
 #define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
 	((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS)
 
-#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
-	do { \
-		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
-	} while (0)
+#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) 	\
+    do { 																		\
+        /* If desired, keep or expand the Assert checks for known methods: */ 	\
+        Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || 							\
+               (cm) == TOAST_LZ4_COMPRESSION_ID || 								\
+			   (cm) == TOAST_ZSTD_COMPRESSION_ID); 								\
+        if ((cm) < TOAST_ZSTD_COMPRESSION_ID) 									\
+        { 																		\
+            /* Store the actual method in va_extinfo */ 						\
+			(toast_pointer).va_extinfo = (uint32)(len) 							\
+                | ((uint32)(cm) << VARLENA_EXTSIZE_BITS); 						\
+        } 																		\
+        else 																	\
+        { 																		\
+            /* Store VARLENA_EXTENDED_COMPRESSION_FLAG in the top bits,			\
+			 meaning "extended" method. */ 										\
+            (toast_pointer).va_extinfo = (uint32)(len) |						\
+                ((uint32)VARLENA_EXTENDED_COMPRESSION_FLAG 						\
+						<< VARLENA_EXTSIZE_BITS);								\
+        } 																		\
+    } while (0)
 
 /*
  * Testing whether an externally-stored value is compressed now requires
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 4dd9ee7200..94495388ad 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -238,10 +238,11 @@ NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd.
+SET default_toast_compression = 'zstd';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 -- test alter compression method
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out
index 7bd7642b4b..0ce4915217 100644
--- a/src/test/regress/expected/compression_1.out
+++ b/src/test/regress/expected/compression_1.out
@@ -233,6 +233,9 @@ HINT:  Available values: pglz.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
 HINT:  Available values: pglz.
+SET default_toast_compression = 'zstd';
+ERROR:  invalid value for parameter "default_toast_compression": "zstd"
+HINT:  Available values: pglz.
 SET default_toast_compression = 'lz4';
 ERROR:  invalid value for parameter "default_toast_compression": "lz4"
 HINT:  Available values: pglz.
diff --git a/src/test/regress/expected/compression_zstd.out b/src/test/regress/expected/compression_zstd.out
new file mode 100644
index 0000000000..7de110a90a
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd.out
@@ -0,0 +1,123 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd CASCADE;
+NOTICE:  table "cmdata_zstd" does not exist, skipping
+CREATE TABLE cmdata_zstd (
+    f1 TEXT COMPRESSION zstd
+);
+ERROR:  compression method zstd not supported
+DETAIL:  This functionality requires the server to be built with zstd support.
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 10...
+                    ^
+QUERY:  INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004))
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
+-- Create a helper function to generate extra-large values.
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+$$
+    SELECT string_agg(md5(g::text), '')
+    FROM generate_series(1,256) g
+$$;
+-- Insert 5 extra-large rows to force externally stored compression.
+DO $$
+BEGIN
+  FOR i IN 1..5 LOOP
+    INSERT INTO cmdata_zstd (f1)
+    VALUES (large_val() || repeat('a', 4000));
+  END LOOP;
+END $$;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: INSERT INTO cmdata_zstd (f1)
+                    ^
+QUERY:  INSERT INTO cmdata_zstd (f1)
+    VALUES (large_val() || repeat('a', 4000))
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd;
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd
+GROUP BY pg_column_compression(f1);
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 3: FROM cmdata_zstd
+             ^
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 2: FROM cmdata_zstd;
+             ^
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_2;
+NOTICE:  table "cmdata_zstd_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPR...
+                                         ^
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_2;
+DROP TABLE cmdata_zstd_2;
+ERROR:  table "cmdata_zstd_2" does not exist
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd;
+NOTICE:  materialized view "compressmv_zstd" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd AS
+  SELECT f1 FROM cmdata_zstd;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 2:   SELECT f1 FROM cmdata_zstd;
+                         ^
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd;
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd;
+ERROR:  relation "compressmv_zstd" does not exist
+LINE 2: FROM compressmv_zstd;
+             ^
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd
+SET f1 = f1 || ' UPDATED';
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: UPDATE cmdata_zstd
+               ^
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 2: FROM cmdata_zstd;
+             ^
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd;
+ERROR:  materialized view "compressmv_zstd" does not exist
+DROP TABLE cmdata_zstd;
+ERROR:  table "cmdata_zstd" does not exist
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_zstd_1.out b/src/test/regress/expected/compression_zstd_1.out
new file mode 100644
index 0000000000..a540c99b37
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_1.out
@@ -0,0 +1,181 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd CASCADE;
+NOTICE:  table "cmdata_zstd" does not exist, skipping
+CREATE TABLE cmdata_zstd (
+    f1 TEXT COMPRESSION zstd
+);
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+-- Create a helper function to generate extra-large values.
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+$$
+    SELECT string_agg(md5(g::text), '')
+    FROM generate_series(1,256) g
+$$;
+-- Insert 5 extra-large rows to force externally stored compression.
+DO $$
+BEGIN
+  FOR i IN 1..5 LOOP
+    INSERT INTO cmdata_zstd (f1)
+    VALUES (large_val() || repeat('a', 4000));
+  END LOOP;
+END $$;
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd;
+                                      Table "public.cmdata_zstd"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd
+GROUP BY pg_column_compression(f1);
+ compression_method | row_count 
+--------------------+-----------
+ zstd               |        20
+(1 row)
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd;
+                     data_slice                     
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+ fceea167a5a36dedd4bea2543c9f0f895fb98ab9159f51fd02
+(20 rows)
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_2;
+NOTICE:  table "cmdata_zstd_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_2;
+                                     Table "public.cmdata_zstd_2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+DROP TABLE cmdata_zstd_2;
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd;
+NOTICE:  materialized view "compressmv_zstd" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd AS
+  SELECT f1 FROM cmdata_zstd;
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd;
+                              Materialized view "public.compressmv_zstd"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1
+   FROM cmdata_zstd;
+
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd;
+ mv_compression 
+----------------
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+(20 rows)
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd
+SET f1 = f1 || ' UPDATED';
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd;
+ preview 
+---------
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+(20 rows)
+
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd;
+DROP TABLE cmdata_zstd;
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..ac5da3f5ab 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -71,6 +71,7 @@ NOTICE:  checking pg_type {typmodout} => pg_proc {oid}
 NOTICE:  checking pg_type {typanalyze} => pg_proc {oid}
 NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
+NOTICE:  checking pg_type {typebuildzstddictionary} => pg_proc {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 37b6d21e1f..407a0644f8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_zstd memoize stats predicate
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index 490595fcfb..e29909558f 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -102,6 +102,7 @@ CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 SET default_toast_compression = 'I do not exist compression';
+SET default_toast_compression = 'zstd';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 
diff --git a/src/test/regress/sql/compression_zstd.sql b/src/test/regress/sql/compression_zstd.sql
new file mode 100644
index 0000000000..7cf93e3de2
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd.sql
@@ -0,0 +1,97 @@
+\set HIDE_TOAST_COMPRESSION false
+
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd CASCADE;
+CREATE TABLE cmdata_zstd (
+    f1 TEXT COMPRESSION zstd
+);
+
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+
+-- Create a helper function to generate extra-large values.
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+$$
+    SELECT string_agg(md5(g::text), '')
+    FROM generate_series(1,256) g
+$$;
+
+-- Insert 5 extra-large rows to force externally stored compression.
+DO $$
+BEGIN
+  FOR i IN 1..5 LOOP
+    INSERT INTO cmdata_zstd (f1)
+    VALUES (large_val() || repeat('a', 4000));
+  END LOOP;
+END $$;
+
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd;
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd
+GROUP BY pg_column_compression(f1);
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd;
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_2;
+CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_2;
+DROP TABLE cmdata_zstd_2;
+
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd;
+CREATE MATERIALIZED VIEW compressmv_zstd AS
+  SELECT f1 FROM cmdata_zstd;
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd;
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd;
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd
+SET f1 = f1 || ' UPDATED';
+
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd;
+
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd;
+DROP TABLE cmdata_zstd;
+
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9840060997..adb94ee7fd 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -886,6 +886,7 @@ FormData_pg_ts_parser
 FormData_pg_ts_template
 FormData_pg_type
 FormData_pg_user_mapping
+FormData_pg_zstd_dictionaries
 FormExtraData_pg_attribute
 Form_pg_aggregate
 Form_pg_am
@@ -945,6 +946,7 @@ Form_pg_ts_parser
 Form_pg_ts_template
 Form_pg_type
 Form_pg_user_mapping
+Form_pg_zstd_dictionaries
 FormatNode
 FreeBlockNumberArray
 FreeListData
@@ -2582,6 +2584,8 @@ STARTUPINFO
 STRLEN
 SV
 SYNCHRONIZATION_BARRIER
+SampleCollector
+SampleEntry
 SampleScan
 SampleScanGetSampleSize_function
 SampleScanState
@@ -3306,6 +3310,7 @@ ZSTD_cParameter
 ZSTD_inBuffer
 ZSTD_outBuffer
 ZstdCompressorState
+ZstdTrainingData
 _SPI_connection
 _SPI_plan
 __m128i

base-commit: 8021c77769e90cc804121d61a1bb7bcc4652d48b
-- 
2.47.1

image.pngimage/png; name=image.pngDownload
�PNG


IHDRJt:s��OiCCPICC ProfileH��WXS��[R!D@J�M����H/���$@(1&;���k,+�
����+�b]�bAee]\��&�e_��|����?g�9���{�@��K���&y�|YlHkrr
��P���8�r)'::�2������������g�-ZB�\
q�P.���x�@*��(����|���XG��Z�3U�Y��U���M|,�����|Y&}�g2�F�$B�b�}��f!^�
��s������t2���>���g�`U,��(�Ks�s��t�������V�,Yh�2f���93��X�����(��@q�p�^��Y���=j#�sa�����8�+��Clq�$72b��(C����C����x�� �����lN�f����!�r��g|��J�����J�����1����$���#!��8R�>d�Z�����)b��X@,IBT�XY�,8v�~O�|8v�t��9���g���r�=����`}"	'aXG$�1�P��'�$	q*�������v���!{<@���� ���
�-���S��K���U~����h�?�� ���t0dq[oC/�S�>��L C�����	���B�;D" 0�+��4�Ur�NuuC}J���<r��bPI2�A"x�?<��*�1�������0���@&b�Q���[����Pb0�7�}qo<^�au����p_�	O������.����"�(/'�.�<������[AM7<���Pg��w��pp?8�d�C~+������zBCv'
JC�����a��6������Q��>�o�H����_e_�������a�v��5c
�����V�������+nx��Ar���5���*3)w�u�q������WnF���83+���_�'8�c9;9�����^o�b�+����W|N
������|%������E
���
Y����|s��������x��;�� ��(��4�}\�20��A1(��P�����@hg���2�
n�;p�t����!!4���#&�%b�8#l�	B"�X$IC2	�@�!K�Rd-R�lGj���1�r	iGn#��O�=����j�Z��Q6�A��xt*���D���J��B�����2z�B�����0&f�9`l��Ea)X&�`%XV��aM�9_���^�N�8w�+8O��L|�����z�~����	4�!���E�&2	���2�.�Q�y���	��D"�hM��{1��M�K\A�B�O<Ml'>"��H$}�=��E���I��M���S�k�n�[����L&��%�"ry�$��)�E�bI��DQ��9�U���&�J7�U�jM���S������:�y�]�+55535O�5��"�r�j���S�V�S����+�W��V?�~[��F����Rh�����Y�}�[
���OC��P�R�^���:�nI�����e���+�^M���&W���@�R��f�f�Ck�V�V��
�=Z���i��������K�wh��~���.C�X���8���!�X��t�uJu������j���&�����=�����VL3���y���|?�hg�h��1uc��y�7V�_O�W��_���{}�~�~����{���A��,���
z����+[2���_QC;�X���;[
����B��F����3�������4�1a����M���2������rY��s�>SC�PS��v�6�f�f	fEf����S������[��,L,&Y�������b�����hy�����U��2��g�z�<�B�Z��64?��6U67l��l��-�W�P;7�,�J�+������~�}�8�8�q�qU�:�8����E�
�/�[�O�f�������r�v:���=!lB���	:�9�+�o��\�]�4��t�w�nu���p������������������#�c�G'[��^���I��\�������+������9�{��M��(��s�#3��v�._�o����]~�~|�*�����B�]�O9��l�^���Y���7\/�|��@,0$�$�-H;(!�"�~�Ypfpmp_�[���������5��<#��W����v.\=<.�"�a�]�,�i:)l��Iw#-#%�
Q ��.�^�u����1������'�b��^�c�M���:> ~U���EBK"=15�&�MR`������'��|9� Y���BJIL���?%h��)��n���S����zi����i'�����N#�%��I����W���y����\�F�s��p��G�#Z+z����6�Y�O����,����^1W\!~���-�MNT��������y����cmI�����g�K������^37�����v��Tyc���oU�(�Q<(�-�,x;+q���Z�%�[���Y>�iap�s����-�L�-��`>g�����-�.]��(dQ�b����?9�-�kI����FK-}�M�7������e���}�+��m���M�?�K~*u*-+��B����&|W������m��Wm]M\-Y���oM�Z���k����~=k}���6L�p���l�F�F�������M�Vo�X�Uq�2�r�f���7��"�rm����mF�J���^����!������vw�x�3q���?��2�U���n������s555{���Ek�={S�^������n�~�������o�v
?�r�}������GGK���9�}
Y
]��������4y7=�x|w�is�	��NRO.=9p��T�i���3�g�Lo�sv���b���?����^�\8u��b�%�K�~b��p��r}�[����~>���V��J�U��M��O^��v�z��o�n\�y��#��Vgjg�-��g�so�����w�%�-��y������_m����u�A����q�<<z�X��c��'�'eOM��<s~���s��)�u?�>��[�����_��8����}���_�^�������������u��oJ����~�~w�}���f}$},�d���s���yR��?�+���&�?w@K����)���`ATg�A�V�!�;u��>���tp`'VP��
@4
�xO����������SY��l�}����t�o��L����[�Tu���"�3����eXIfMM*V^(�if������J�tASCIIScreenshot��3�	pHYs%%IR$�TiTXtXML:com.adobe.xmp<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
            xmlns:exif="http://ns.adobe.com/exif/1.0/">
         <tiff:Compression>1</tiff:Compression>
         <tiff:ResolutionUnit>2</tiff:ResolutionUnit>
         <tiff:XResolution>144</tiff:XResolution>
         <tiff:YResolution>144</tiff:YResolution>
         <tiff:PhotometricInterpretation>2</tiff:PhotometricInterpretation>
         <tiff:Orientation>1</tiff:Orientation>
         <exif:PixelXDimension>1866</exif:PixelXDimension>
         <exif:UserComment>Screenshot</exif:UserComment>
         <exif:PixelYDimension>884</exif:PixelYDimension>
      </rdf:Description>
   </rdf:RDF>
</x:xmpmeta>
ZI��@IDATx���E��������%@p'���.�5��[p��	�n��$w
v��p���K������zvgf?�z%3;�]��n�zJ��h����:1-�k�di/�0����@@@@@@@�H�f����H�B@@@@@@�A�@i3�%�@@@@@@
 PZ('�!� � � � � � �@3(m���:"� � � � � � �@�J�$3@@@@@@h����XG@@@@@@(T�@i��d� � � � � � �� @����� � � � � � ��
(-���@@@@@@@��6�^b@@@@@@@�P��r� � � � � � �4���f�K�# � � � � � �*@��PN2C@@@@@@�f P�{�uD@@@@@@�B��If � � � � � � �J�a/�� � � � � � � P���B9�@@@@@@�A�@i3�%�@@@@@@
 PZ('�!� � � � � � �@3(m���:"� � � � � � �@�J�$3@@@@@@h����XG@@@@@@(T�@i��d� � � � � � �� @����� � � � � � ��
(-���@@@@@@@��6�^b@@@@@@@�P��r� � � � � � �4���f�K�# � � � � � �*@��PN2C@@@@@@�f P�{�uD@@@@@@�B��If � � � � � � �J�a/�� � � � � � � P���B9�@@@@@@�A�@i3�%�@@@@@@
 PZ('�!� � � � � � �@3(m���:"� � � � � � �@�J�$3@@@@@@h����XG@@@@@@(T�@i��d� � � � � � �� @����� � � � � � ��
(-���@@@@@@@��6�^b@@@@@@@�P��r� � � � � � �4���f�K�# � � � � � �*@��PN2C@@@@@@�f P�{�uD@@@@@@�B��If � � � � � � �J�a/�� � � � � � � P���B9�@@@@@@�A�@i3�%�@@@@@@
 PZ('�!� � � � � � �@3(m���:"� � � � � � �@�c��!�
"0���f���;�����~r���?���JB@��-0��c�S��w+���<'�5�t��������0l= � � ��P�@i��}���M:��u_�����~�im��������K������v���_?�=M!0�Xc���9����Zn���vc�1F��~�����m������|���v�qG������&�()�����_��o��_�����+��A������M�����O�������������}���{����m�����X�
��M��:�������������?w����{��w�g�~����� �@�y�1�k�O�M6���#n��W�9���� � � �@7 P��;����Y)��5��n����\t��g��(S�Y`�y�u��~��`�	rWS�K�c���6��#���������o����+�����?�t7\�;��#�_�����fY���#s���X�C�t	�,vM����7�d�$0^����~��W7d�w��g�w�y':_ �� ���k���uV�3�>����j�#^@@@@�0Fi7��l"�,0�8��[o�-7�����m={�t�<��;��S��QkSY_{�
�{���w-,�z�>}���C�/����j��Z6
Z]`�e��n��3���/@@@@�5���~e��6�p��`�	������j���u�;c��j�;���%XZ�~�N7�TS�S�+w�����l������j%MB����(������w|� � � ��)@��5�+[�@�P`$/��������r�X��������e��b�)� i���n��$�f�@7�T������+��F����i��j�f�q�dlX�������6��A��n�^(4sVz��)%!� � � �@� P���7[�@�	LiA�X:��\�Ygu�^�-��B���O�M�R��s�9�X��l��������\xa9�2M7P@�ak����I �@�|�������r����������=��v�� � � ����X�������{��d�IJ��^{���H'��?��s�w����~�� �*c�=v��������K.����?�f�i��+����/����oA��������?���t�Eqt�[z�����j��z�v�?�Xt�h|�7l��GS-��s�fp�O?}�O���J��?�x��n�M6)5)�#�
#p�UW�{������j��?�p�������� � � ��O�@i���(k�������?��a�?����q��>���@V��/�����^��g�nr��*W�t���d]O��������*U�����]��f�4�g
��v���+�nu=�P�v�>��u��
�o�����o���4����~�n����_OV@@@�+���b}�G�M,�n�����.�Sg�u���>�O>��t�I��*�Gg,CNj�8��cF�������j^���"��,�R:�&�p�R�E��s�9��=����A��w�qGn7�s�5Wz����l��2+������z]�t��8���R����4������[e����%z&8���6���u�5�u��������i[����L�����j�����[���}��T�}B���Y�3�g���o�}�kW<3h}�w��� � � ��������n$�{�����z���. ��g?������������~����T����z�2�S��a�5u����qw�I'������eT:�&�n��ZN]O�k�x�u�vk[�~�)w�y���^{������[n9�u��n)k����p�@�Q+��6�����=�����W/��v����_>��9k2}���[����&��8�����.�&�xbv����������<��aQ]�����N-K�-��*1������]w�~����,����o���}���_��}��'���.��'�t�gt��Y�<�8�p�1���{nw���'�Z��}�u�m��fW���K��r�Z�b�	���SL1E�l�Yw]��.�t����~s[l���������9�����O0A��\?��3w�M7�K.��C�@��HSnv;�&����c�_v�h^���^�T���u�����8w�q���r�
W�2*hx�]w�k���}��w����w�y�-b��>��s.k�B���ueY*m~��W�)Gw<���������mb��O�W�fz�B��^����6����imgx.k^����f;o�����Q�{��%�n�G�w�N:^u��{�=��.����s��vs�[ka�sZ�?�t^�����C��(/�>=p� 7IF��[n��
80�Z���:�g����������~�h��3�f�����#�p�:���7�xm�h}������s�>��y��#�<�-����z��s8��V[o���g�i����� [����d�[*��>��Z�������%u�.s�h��5��>H��>.U�"��g�w���n��N;�t�sSx���������+�HzGH���g���8v�O'��{��7�q���xf(�^�k��;���S��?���{����
���q�I � � � PZ`��&���'+n��z-�d�������sz�eY-�T:�"V�T��?����{��~�!w�����^2k\�������n+9��`#H{�qe����QPr�-�L���"_U(��;�����O?u}-Wn S��,`6��XIz��g�n�*'8���K-�5W�-�?����_��p��uS��t�����&k�
�K�w�:[l�R�e~��Y�:� ��������
�/�����_����r+��J��8�:�O�@rV:���*��$�j����������>Zvv
���6����K���aR0x���?j{?�/�,�����|�n���7
�on������>;	
�����?��w��UBx����������
�����>�5O��cm>��J
���zVz���;[w�����d���3�~FT�>��$��5������M�7�|����rEl?�9����:����]sY���v,��,XU�%�t��`��Vv�W����Y�J+8�����Z^i�*.k�m*Y'�v��3Z�r�����.s+��J��5�����r�o�}��7��3���nK��cA*�����������i��w
����s��!n{3Q��4���V������L�R_c?U*�Ta�`�9�A=�������k�S��\���e�}���Yg���u��T���s�u��:f�M�����&����>
>��ou�������}�������P�^:���T�}�k{��b�-��M � � ��h��]����h��U(����eI�y*�=�
���o����Q�^��)c�k��
��VN�*�������J��C����-�4������ ����/X+����#w1+����Z�J���tUk����/�(T��ak�ZI�T�P��������*���ZM���J�"T��}���]|�juY�6�o-WuN�~�����9�$f�f����t��T�k[a������g�y�qOY����l��
7���Br��7X�>������HF�)l��5O�*�}����{�_3�W���J1-�Pn���b���#,P�����$�/u]�=N��"�r�,��
����.�^~�U�@b�����v��9_��T��	��{���
?���cO<Q2H��n:w}M��Z��n]��$��+���l��V��/���s9AR�?�����Z�ogA�jR5���x�R3��>�i��u����w~�[�V�b�5�,���������������K�9�g�r�[ji����s�h?�C@@@��6�^��uRW��4�����R����j
�,An��P�L+")��V#*��6���
UHK�M������}��
����|�
K-_ArZ�Z/�yU(��j�#���=�f]�6BRK�=����UY��T���������)gE�����d����n�
fEg����O9����h����)�OcAV���VeK1��E-i:���!k���R�'����Z�e�f1�y�@}�c���J������������*������k�v�]f���WiZt�Es�/+?���3����n�I����V�%���b��Ws�������4Uz���g-�6PN%��mU���Fz#�q���d�6�^p���_��
/*u�3�E�-p�����3*��(3�A@@@��6�^��u���U�H�\mR���u���&-�r�T��/\��0�&(����i]�f�.Q�Q���T�}�9�df��I�(S�*V���F*g��s�[�j%��I���TI�6�x�4U\-�����/w�[��u�{�u{X�rCnF|Y���������J�����"O�&��jI��SR �����nrV��f2W7��M6Yt�*�B�DW�O��np�����*b�����>��9+���@jQ�$ui�#'(���\'t_��Km��f�����������]-�d_��^Q�>��g-��Z��J/�TR+��T�J�#�R�eh��+�"R#<3���4���8�1�I � � � �-@�4��OGh<��-��q�JH)�6}�xz
F����T�jl��Y_3����w|�AtzT^Rc���>6�p.\��Y��]j5�5��,bI��3��:��r�?��cw��C�U���*5~��\�VE3�xUY�	��#F�pk[W�3���x�����������*�J7XW�y�����;�Z�jj_>����cF^�l\�j�]66�pk����X���_����?`�r�&�|�$P�W�k�K�'y���[,��1V��*d���7O�wZ��)��s�e������m�����*4U��#�J����NZHhc�-����duy�m��q��}n�:�t��I�Tc��������7�p��������:����9�s���w>�r~�Vy}�Zz�RV���6��S��q���u�tz}���
��Uh;�t^���{�Q�K� k�_���x[F�����P�B�[vNh]��JD�4��W�)��*N��(�sO�`-F5ph�^o���DZ����[��_:���G����Wc��;�&6&r-���?w/Z��zn�3�2$�Ux�d�������N��"+��Z����?f��	z�������{��sR:�U��9���{���Z����!��V��JzFZ|�%��*��F}f���o��D�����������W@@@Z]`�V�@��:�����N��"��s��gvwX�*o,�~{���Hu��B��B��m��a6��O�
�.������I�.;W�q�����������c�;.:�
��
�l7�
~O<��h2u8�������m�-��S�8�Zd�r��m�����=�ZZ��OVPR��UV]5	��5�i,��P�1L/Xw�sR�fu?�Bg�5��>�����GN��s�����o���-�5��Za*�x�}_I��Z.����F}�I�>��SIo��F�?]�����=6��^v,���_��Ip��3����g�V�=�����sgZ���_��W��ZW��?��Y���u>����v���K.�T�O�8U�?j�(����{�u���E$���f�%�"|~*�~t�����5���c��w���:U�x����:�4QV�@/�/+)����0���N�����U��YI�%��K+Y���g���^tS��k`�:�|�]vi[��Uw�Y�\ky�@_,�d�f]w5�*����w[%���v���4�%���-�'��U=�Yg��|:f7�`���/&��N�V�{Zw��������s���-G�v4F�l����j�����\�U���V[���x�[Y%�R���oOsa�R���.o��\��J���o�����3�>��O�hv��Y�>�Y�Z��Y'���k/w�
������pv�'��J�uy&��I��X�IU`[�W�vi=s�^�@`��N�#|��W��F{f��Z�g�$U�I�m�����t�{������� � � ��hQ����L@��Z�l$��<Y�
�~������6^c:)�K[+�X��
���/+)`�.��I
��{�N� ����������z(�X�%�X��*4LI�|j0 ���<���'M^��_�eG,�b��X�t��MK
����~Zm�&��j�a,�r?_u����<���m����
����4����*T��:r�+������� �_�%6��#�<����U��Yf�%O�AkI=�ZJi��H�d��l[k���a��FA�0�~��jM��r���Z�\-���U��0��N�R���e���2�wjA�V{����;6O3�I�r��i�)�l�i�d�R�{xc��5#+�����^�|�-�!E���1v�&�d�N:H��P�c���JYI-��B���w�!w9��K�rt�Z����g��^w������:�6�^
��j������KC���u�k�A�
w�����F�}�{���u;�j:�v�uWw���g��|�@��9�EY3�s���g��S�M�����AR��^�?��J�[M�3��Y�%��rLz���ekY��PkI�����\zN
���6Un��{��4Y����� � � �@w������X5'p���O<����2,�R����kYI)�X�K��E��y��C]���b��	���Z����+�N�v���c-��GjU��:}R��t�F��{��~��W6�j���4�u5&����BN-t����Ta[���j]��i�H�(u�k���W��
�����^��c�>�"_�j������������V�j�+x��Ze���~yY�}���6ff�>l���7j��V�:�b��r�T��+����is��R��J�s�==�*K-X��J
~�Z�*X�n/��{U^j=��i���=i�C�?|��Y����Mk, :�����c,�f����>���D��0L�d~�u[�k@:����.v$���Yc��zSH/#����K�Y%��.������vWZ�������mi�7ci�5�8������b-������I���+�}��y�����8�������m=b�b�2��B��lm����y��cm��u���Xw�[�����I��':�Y��He9]W��n����k�z4}�U�/����������@yV+�RA��<��A�Q�"W,����U��d%����x����@@@hu����+�>�������B-=c]�j��0��>p�}�����Bu�5���},8�a���.5Y�L����ZF<b���4�>��M�b-b�i��������������b&+��B9�`��*��N��<�y��
�U���-'
�n����w�9��3���+�f�������r�s�qrN$���������b-`��#o���R�}�8����u��7���	�[���.�����?�3���W0-(-U���oD��y��>���U,���U%������/����~k��L��Fj��j��Y��j����������y~^�*��������3���?���4yn�}@�f��z:�"�
V��y��~���v���y�������Z�i��t+�p~]�c)+����|����y~�-��
V�rS9���|�R���^8s�o��&w�W�����6R`����T�1�}������X��:O��)Uh��"S#=36������X�4v�-�) � � � ������h6/.���^��
&e�!�����W������7�������:O2����k�l;ZW��&u�7���9��S'��K���*��-O]"�
5�
6�����>`�R�m���4�sD'[~���m�-�����X��vj��Yi:s��p\��4�<�����I�W�:3o����6�i�{�K��m\\���c�����g��^{�������y)����6^o^����-b���w
��?��I+Tu���I'��Q���Oc���<��{u_K�|�m���7����y�KP]�'�cCAA���D���2*�t��
>��Y�(]�����b�Zr����K-���#��c�F;+P��f�u�����S^Y?�o��:���%*2(`�w�L���w�-���� �� k���':�YK-C��{IV�=Bc���Z7�dAt4�E�j4k���wl<j]����?W[�+�SU�Q��@���#5�3����R��������@@@hu����+���q����z#+PN��S���������R@���8l������Vk��X����v�������
�+m���^*�U04/i�jM��vX����o��0j|LR�����%���j)�>��Y��3Sz�p��[�k9I����bVK�q���,�i�^���N�������d�@�Zk����Z������r+���J���T&\����l�)oLd�Yn��Y��8��o�Ar+x\�9���V�,��n�m_Y��y�Y�um���W]����9��gg����]���**��J,����|�zz��1�5M�H����z���?�?*��r�y]��� �P�d��4�q�lVk�X���':�YKm=��@^���
D�N?������/��n��V����ms,�p��n~z!/���O�M���b^��3�e��x�x�������5�3CV+Z���U���@@@@�2�x3���a�(���Yn��ZH�3���G��g���
��������r�XW����e*\,: ����w�����(0�`�A6���V�}���cVy��D-�o��O;�t��R��y�d9����V�W��6�m���Y�����j�����0����5l�Bu��V@����U�{�����WZi%�����������S�~��dgmKW,g���.6VH�L���M�D����^����� �p�
3���1��J������
�3"��Gg_��T�z�Q����{�����D�}-+�J?K?ki��r���TIM-��+��y?�����A�b�T���yu�Uk�ml|��������r�
�8N�������g��&!� � � �@�J�7%�@��Vj�,�;2vf,��V=
��U��O=U���������%-xQm�s
�����#;�/��C��"^�Ju�\D�yy�e����?��J���f�_�R%���<:�s�;��C�b�z9u-+�����v���M����;�k, �W��T�+�Pd|B���`M�f2h����Rv����w^���jhy�^��nl�4��~:������Z��J�k����x��ZNg?k�~���+��+�����Z�ng��[w�Y�4��[l������U����W����*���c�Jf�0-�H�@@@�� P�r���6���~���S���dM[iW}jk_�/��W@�mc3V4��V���d}k��.Qo��fWn��p9*����������6��/Y��������y�|�,����*��2+i��n�����^~�e]^K�6�i������1��l��P���yA�m,�05��I'��V�������������u/j9���(�s��������p�	�^��}"�Rv��	��YK-�����u_�����]����1�c����rs�9�;�����UG��v��n��W�-���<3�$b@@@�^�1J�~6�|���IWh��T0��u�Vtaaz9Y��G�8�
*����/��QG��g,)��������/:u������[3S����b��~��'�����/�� �Fo��V�������Y-�N��?YNV1�;���{(��5����|�f���ZziWNw��X�������q���
`�5O�5�
��-�h����q\�~�i7�ZV+���U���h�
[n��;����f���}]��X]�t�	S�����>�%��^z�%7|�0��+�8u1����UK�A�^�z[��R�uh�f(j1��w����{F�)����1Qn��:�
VY�^�U*)��T���������YK����o����!4���k��X`�d�r�4����U%��1��/�����^�S���Zw	���Y�rYp�����5o�?3dm3�!� � � ���v�=����B�����^��3�Y�|E|�����5�)xY*)����w��(�x�����?�������Eg��B�<��',n���m�2�����>(e��XR�q�J����1�}S*-����IT��t����uOVZ��8����<^{�����������w���*A��}Tl��v�=��N5�O9���;5`,��j�L��m�Y�"�����V���y�)��2�R��y-�+3u3`M5�T��A��^v��I���L4�D����#�5~�=$/�F�nikm_N�t�e��n���"S#<k����z��?���1I��r�$x{���+Z��R��>p�sL�d�B}��6rY�VC����
o�����5a>@@@ZM��w[m�6���N,uU���~l��:���.��b+d�8v�^tQ�d�Bf��I�����GR!�	���Cy���G1�s�}7"'���"��M�,o^��������ZN���k�[N��L��w�Eg?��B���y��C;��)�o-�b�^��
������������q�A�n����O6��<����}��'7H���k����~�>]��r�������}J]����_���.G�q�*����V_}��6g�R^�-6O�������}��g��{��t�����F7a������}����Xm�d������8��N;m����[��!w��@@@�n$@����������*�4k�;Z���]����q����O;�{����+�;�XR�q�N:i���s��Z����o��W9��Z�_9I]�}��Gn�uw��w���fq�u#��u�q�m��3����4+�G9�Mj|��}�uO�v�ZN��
����1��rk��V��&����b��4����R^���XP;o��:�k�m���Xz���c_5�����Z���G��A��6��v�t�����I��&j���F3�k�9�t�E��#FD��+Yk�X���3��r��!�)��1K��rz-x��������|�)����#oT����{�n�����sZ�\�E,�rF������M�g
��z���$
�p�D��r�)�������x������z��/��r��+�e�����b��� � � �t7��HNwS`{�&������7���a{��G�j����mS��jA����?�f������w���l�4���g���+-�K>�@�W��3+�9���X�Y���������9�|��NI�����_��o�W�>�-`�KM?���UW_���{n�Q�W�/����O���3�3���}��7��T���"��t�<8P��N�~����O=
k����y���K}��u*���.<���j���A�p�4�^,�k���"/H���?�8���{�{���;���yI��n��z�+�5�s,�U�������K�G]�sO��W��ZW����y���������pVR�JA%��?�<�����sEN��M6�4�ev�]wu��>8��#���
3
��o�������������z������~��z����>^��J3)_��|��_�\�|����c�Vd�M��y+?3dm/�!� � � ��v�����|Q�;�p�j���O86�I-6����[���gZK�j��>�u���v#�{V|�H�s�>o�����(p��6��5n���k\T�P�T����$��JX-��_�=:d���0���J8�Mo,��<����d��pAr����ell��m>�wv��vx���N�G-�}�Y���v����jn�g�����<��3�P�6�U�
����ko��n��w�	'$c���=���
��r��u�xr���cC�����;u��L��W^���ZF������P-�7��
O��1�Q}�q#_|���_~��g�U���~�����V�����i�*�t����M����}������������a]�}R7��[���<�]G�����<���/������0x�V�
�^~���T�"L�8�����+����g��t_�����Yg��G��~AN�7\F����T���v/W�]Cum�������8�������/u[t��g-+��f�7�n�t�L�\07�����|z�Z���*%������q��k��{c-������@@@��cu��f�;Q���ov��������J@Q�JR��	&��)X��4��=�7I�w{[PhE+4����>Ww���a^�?�)'���&�#���r�/��3����a�il8�)[�2���������]~��^����.?5�����d3Ya�t�O�[��.�����y��l�}�m�;|����O=��x�I�6�o���\�[��]����n��6�t��uQ�C��b-��t���e��1������{��^y����F�r�o�^z����;��5I�gGYk�fK�Z�A�b�fk�����l�HT@,vmh�������E����:��RW��n�~,������{�*�����uI�����v�;��#3g�|���2�O?���8������n��{�Z���j�{�������4����am����{R�r�8��|y�q���n���k��z�>{R���u��G=Rg=k�W�u�[/s�q�_����f��Z��:���Z&����s�����.�����.��0����^,�T7�zf�����y�C���ZR+?3���� � � �����5
h��a;V`+��1�
���
����T�T��i��*������`9)Vp���������#w�1���;�"N2�$����y���Uf��\�^��j�����&��Z@iL��,p�N*�������[�j{�
,��Y�A������R�zE%O
�wuR0�`k����z$��n-��1]a-f���uR[=H�gP�w:�T9d�~�rg�ls=�`f^��F��)l\D}We]����o�GuV�T��|"a����%�y��*�������~�-���������,��.}������q_���i��T��gg<kh-���uj����U�D��� ���]w��.�� ��\�K���K$�PW�y�z��:��Z���V�G@@@����^l�mP���B�;�r7A]�mf�a~��G����9�bt�������O������^:���;��������[t��e�_w�u�4fMS�g���[+g��u�^�d�����~�i���r�-jI��q\��4,gY:&U���Q�M6���C��@?k[t<�c]m6kR�k���&9�+BT�G+M�sg����W���|p����~;ke^M�	���y��p�����n�\�J>TUx��S���"�s�c��~l1M��/�s@��O?��m���u���x��9���&-�k����WF�c�>�=iC��$�����K����j�a~@@@�]�@i���&Z�Wl���]��@���\�Z��C]��f�]�j�c����Z�Zi�SVRa_V���i�g�^��$u��/�h����\��c��K�������{���~���xj��I��z�U�r����F���������E�%�cB-�t<7{�I%�R�f���3�i�����j�����g�G�����M�Z�U��U�����~�k�*����o�.����]n�{tg����[*z�!nO�U�T�n����uk_�a5���yt�}���(ja�g�j��nkg<ki,`U���ZbW�dw����U�����u���g?�^�����|.�C+?3�v�G@@@��	(m�=�W�������t�n�����r����v�nJ_����)���/���Mjo�S�E'u98��i�.�uh��m>lXR���� ��G��[Z��R+u���R���X����}��
.��V�������4��Q�������Yy�?���n��>bc���������_�������[c���Tp����	@IDATV�2P��cR���7:~�R�9�w=���+X}�wL��r��_��}�����'���~������2_��c�0����X*u~�-���c@�[�����o��-69����7_��.o_��Y�)k}�>S~��y��s��O�^�$u��uZ��r�,���n4��.����|��|�M7M~y�8k]����>�6����Z����kh��\�>�Q{�0`@Y�j����k������6O�I]�����/.�%�,���5x`���L��}��k��#�E?�^c����L8o�����[%�R���y�f~��W��W}���g-�+�,��;��������^i���R�m�\J%OK,��{��']9�I����~����-���JD��5m�\��h����h������^�*m��qO��= � � ��U`��&��>�ED��x��K/�L���I@�.n��cj�YgM��S��Z��1"i��B��L�N;������9���:i}T�6j�(���@���Uw����V��Yf�%�V��=�|��4�P�Io-��n�&-[|���PSA�!C���M�4c�9�[�
=��sN7�l��q�7����_w/��^���c�rW��/5��K.��}Lj\B����Y��q�R��%�Z����*n���vSL9�������:������/�X�
|����Y����2��G���Y�O��o�Q�y��>���m�k�|�����:�56���9=h����b�4���.�f�a�$8�k�3�<S2@��U��9�\���g����)�� ��������t^�f���4�LI]AX��� ��`�����1S��Yck*�9���i�	'tK[o������������a�E���g�q����at~{�����V�j���;\�%�;��n9�4�sz*���2�6&�v���������[���e��m�g��v�9 � � ��@�����{�� � ��Z��@i��b�@@@@h�@)]�r#� � � � � � �@� P��v9� � � � � � �J9@@@@@@@��	(�v��
F@@@@@@� � � � � � � ���v�]�#� � � � � � ��X � � ���	��m77�\su�����:|� � � � ��-@����k� � P��������@@@@Z_��w[�� � � � � � � � P��O@@@@@@h}�����B@@@@@@H	(M��' � � � � � ��������l! � � � � � ����@�@@@@@@Z_�@i��c�@@@@@@RJS �� � � � � � ��/@����1[� � � � � � �)�)�D@@@@@@�� P����-D@@@@@@����"� � � � � � �@�(m�}�"� � � � � � �@J�@i
�?@@@@@@@�����>f@@@@@@@ %@�4�� � � � � � � ��c��&6���s��\1�
@@@@@@�,��{�����1hQ�����B@@@@@@�:
�����yYS� O��@@@@@@��-J��K� � � � � � � ��Jp��J � � � � � � P_���%w@@@@@@h@�
�SX%@@@@@@�������; � � � � � �4����)� � � � � � ��W�@i}}�@@@@@@P�@i�V	@@@@@@�+@������ � � � � � �
(@��w
�� � � � � � �� PZ__rG@@@@@@� P��;�UB@@@@@@��
(��/�#� � � � � � �@
(m���*!� � � � � � �@}�����@@@@@@@��6�Na�@@@@@@@��J��K� � � � � � � ��Jp��J � � � � � � P_���%w@@@@@@h@�
�SX%@@@@@@�������; � � � � � �4����)� � � � � � ��W�@i}}�@@@@@@P�@i�V	@@@@@@�+@������ � � � � � �
(@��w
�� � � � � � �� PZ__rG@@@@@@� P��;�UB@@@@@@��
(��/�#� � � � � � �@
(m���*!� � � � � � �@}�����@@@@@@@��6�Na�@@@@@@@��J��K� � � � � � � ��Jp��J � � � � � � P_���%w@@@@@@h@�
�SX%@@@@@@�������; � � � � � �4����)� � � � � � ��W�@i}}�@@@@@@P�@i�V	@@@@@@�+@������ � � � � � �
(@��w
�� � � � � � �� PZ__r/S`���rN8a�S3t���s��z�����eyt[�����������xT��Amxw~��b�)���t��&i�F�����:��-A@��&�l2��b�5�j�~M"@�[���V]M������[ou�������o�nj�o��O?�F}�I�o�]w������������C���K.���
{h��������)/f�N�kCunE�u�������xc����o>�����k������n��fj�}����
��c�yU�l���@�<������|�si-I��{��/��>��S��K/�G�r�m�}�m)��Z��o^�5j7$*(um�4�F���g�I���O3�4����r���}s�=�l{~9����t��3�8n��7v�z��>9��S��o�������uPy�^��v�v���c����o;����v����@�=�p/������;�������y�tM3sS	(�����v�%7�O>����l�_�~�]��s��������U�[n�T�Ci�1�p{��w�lZ�o��O��D���I&�����hN�I'��-���m+��Zk�1����J�p\V*V�����M��Q�>]��O�Y�T���@{���A~�0������,2�J����h���	�W>N0�U�(��x�����n�)�H�m}f����	������\�v��@���m���(����L3���+�z�-��_4��u�������p�����#GVm�JQ�,O�
���y
4(YV^�
�_x�Ew�M7��8��9%+]>`����?��r�-�p3�8c�du�L=��{�}I%O��k�$`���[;�������|�m�����u=:;s�V�s�^����[|��d����C�}�96����������_���[�M7�[u�U����s��L����v�.�h����_
sK��7�x�C��FZ���'�������Z8L�~�M�'����������n��{R����O��/�����?���������W\�������-��)��������?��}��h������9o�]vqj��
/��������{�j;+u7o��^��|�n���-V���m
���p�����]v��X<r�RUJT���^��zj��2�T�N*��ey�`"���N������
���j*����&�%�[K��ap~��ww���TR����lz�E��g��/��\h��`EV���v��g��/� �k>kUBSkU���U��j���_�V�#�d&��P���(-G����w�/k�
�>e]`j��fO����f������v}��;�A[�4�s3���k���n��{R��������tz9����fEXj�	,���m��*5����n��z�)�%���	*9���Zp+w��h�������9/,X��s!�n������j��g��F��f�w|��w�x����`��/���������iJO>����
l�?��x	�T1D������������}VR����?�9���\�~���d6.W[}��,
�L-���������������O*��<�o�0���n������I�Xg��N�	��*�w�����h�n~jC�����n�����w�M�q���?���i���T�2�u��N��1�[�
Y��TAR%=(=n���������,M����������)p�W8�#!�@����n����@+t�������m��jc��\�J��$k��ZS��m�BSC��t!-�<EK����Z"���Y�Gd}���
�W��|�Ic^�e��2@�2=����
�-���
~�a���&�����{���:LS�Z����E�*xK���g��*L�X���W�k��\�m�q�'�= P��*�/�������]-6�w������
U�]>)h��@H � � ���*:�kW��#��,�u��m]����������������R�:j�e3�eg������gR��l���O��:�������o�[]�����������Oji�n�>�����np�Y�ak�v����+ �
#@�����WD-G��l�d�R_k��u�������p4���j�i����g�u�j����c���>���q��f�7�Yw�+����>{��{����Z!����JK/���|�)��4�����K�[�c~\�Q�o�+/��L��=PN2���{*iL�XR�:������������/���n����[��|j���kJo�������>�=��;�~���b�:����\������?��a���xJYD3�|���b]t�E��A�	�v��G���d����.O����w�=�$�����u�,��"n)��.�������SO=���[�2���i��������YK���_?y������t:���}l�����n���{��'�e�}0�M��G�y��W�b�7��2��������U�-7�����{[7�����{tt�3�1��&��%�\��j�P�������&�f��7�������F�'t�g�Z�9�B=G_^�5��������k��$]G��k�K���_?N��6C�u�����'��4�L��o��v�c���%�{L�b��4���_o���m��*�h�F�����������+���+����d�I��v=�q���^��6�w�}�������/t��}����i��Lg�.�u��?��m_i����~����M��lv}�u��g�u��5F�������g��������{.9���@�ua��KVA��K[%��5�\3SgL�N��u�]����Y���Z��_�wo7�Uh����o�������r�K�/����.��s(/�]f��i[�����V�\��rR���X��Oz�X�����c���w�Q�9!�_���w�~��(s�������k����s@yT����{�d���~�|
�^�����c���<]�7�h��9oR+����#������=����D,�r>���/��c^�����6�<��4�~�����R%&mF���\�ZT5����
W�,������+�gC=}���Y���Y��2��@�)����na������=*N���Y+9_5}�H��gC[o�=��O���R�I�^�c����V�&��z�a������d�+��>���k������c�%�5���
��z_�>��_A�M7�����t�O�F���!�{�i���������Us~���Z��"����Y����{f��:�����-�L������}����M^
�1�������!-3�;|��3�zj��{Xe
������>��D��p�z��h���vf�������~G]t��mc�*�f���3�W�lf���j�o��;����^����o+��A����7��	:�����j�b����"u�9�L���~�;+L����N���#��{m�r���`���������U��cv����pY]�^��M�,X�����2 ���`��V�*�^����~O����x����������r�J�W����Qn�����27���<�+�g�u_��w���}�;H��YI�[��p]�b����y+;S���uOZ����)�Zj��}x����M2��t]�r��k������^Q�L���>^O`��&�*��:��B������������v���n���z7�EO�4f�tCZ�~d�4jtJog�Yl���nU+���'��4��O*�=�����e���c�}�M�s��/Z��f���V���^���[�>��|����y�{��&A�t>z���������U;M�����B�;����W�d67���t�j�YOIyfu�BzM�B���q��H%{Y����s���x�J�]�������M�W,���5��(��Xd������?��{8��6~��'�@���Y�O���>H~�m�%��c�=�]^
�+(u�g���|�1�$5,������j�A�y����_?����)kL�%�~�����c��=|�|N?t.`�zy���?N�:SF�Q];�Kwa]�9��U��x'Jz ��jV��uN��j�{���}���1I������5u��v���5,��u�)��I���`z]�w���F��0�<�������@&+i�e���}���9y��+U��J���zh�����/��6���c���������RV������N=5����?t�2�^�����D��t��7����Z��v�rb��?���*�Gq:�����PI?�V�`�����+���4�[�����*�I�����_`=>�f�t�9�n�v�i���t������5�^��'����~�W�3�
��t�>�����~�g%����N/�������j����a[�6\:`@R�'k���ni��*D��
=��t�?U�J��mc�[��JS��@��(�����
��*`���-\�H���{K�,W��^�s���W�s������������N�!v���:�B8�����l��������=���Td��.�l}+l�Zy&�p.��]�k����=�������5S�O��~�"�5|�y��YA��W���5����[0-�Ur�*�����[�>����'=��m��>��T��?}����|+�O[�|=��Sz6�3��gU	N���i��n��VK�f�����������������U�����OS��*}L�z�y�:>��m���{f����O����,H����}�$����g�j�m�?�{QI�~�Zp)/]o��
j+�`Z�I�Tm���H��������6���0���I����bVE�r���<����Y��k����������__���-,u���L�o��l*����W�
�\f��>�m�����������O�H����������H�k����GO���,x��]c�r?�X2���T�ng{����~{s�q���~Z]��^�k�*[�h���3��gi���@�q���>���{=o����}�����/�U���TeyL��i��������M�h���]�����T���e�<��:u\�����y��^��j�M�2�-s��w�1\�=��������	_U���OZ%��J<j]~�������Vf�J6�t���i+)H��0%����WR��R��9n����+�F�:�>�8�b E6
HV��k����Z`t�M��Z����#����t�4=���m�Y�"$��*������
�}w[�
������%�r�ARM3 x����?W��'���������p)X��cA�p�Z������.ku�*o�S!G-]�\me���Z�o��v����%�]�Qx���`�	�V+&�!Q�*����������*���t������,�C�X{���+������~=D������_����H-�n�������������BE?�
rT�U�Va����Q�t����ZO��~�~�d=d��������2��d�#�\�?���0�J��sL���|��/���W�7+�V��V�5$�<��*���FO:���V�yARm�
D�[m�X�c��T P���k�����b�b�yI���v����iu�R+���v��*dI5�����C���w�[F�w�V��cAR�+���>:�x�yT��]�W�� �TC�Y�����W����s,����WMk]C�����Q���*��='���[����L�z��6j����Z)�z��u�WW���D�o����s�HW��s����*�8�*e�"�5��Ov��*��w��i@���t�y��}�U�
�����xN*����&YAR���u�U�U�%=��
�4���]Sm�u������k�3f�~�����o��T��Y�yS�9\����~V�=��}�S�{�R�1��z��JE<�rn�w�9m����[��}�z�D0��*������0�/������� ��R�7:��2%}^tRp,����~��\��k$���Y�E&�V��(�$�wO�3�Or������}MWN=���Jf���s��Q�� izF]���J�YARM��*��+��*	��lc�����'���u����'��^V��_=�lc��������Y�z���d%����j��'���������\^������!o:�����W��{��+��J�a%�$���V��~��������CXz�"��o
u;��;[�k��I�%0VkmN�����I5�c�_H���n��JF���"RI�*O�o��&�2A'{/���[�j
�����}��s���������jf���"��j���E�
�ZA��}R-Iu5����SPN]R��@m�M�*qA{������Q��jEe���l����~V���K]����9����&y���2��&uG��|���IA��V����(��.X����|�\v����T�f���Jz���Z{.a�aRa��|R��7�f�j}�m�
�@���T4�n,��
�y�����	�8T�K���n���X?��4�Rg��da����jm�$z�V72�R�*8�n��?"|^�o>�����Xkp�����FI��t��~��U?�t���~�M?�m�c�=��j������s�u7.T�V5�5�\�P���l]K������t�Q+'��^��-U����~��vm���T�e����IM���%p�#L���M�Z(.i�I���uA���,���GV�����a�k��)]�Z~�e�5�
�K���y�:��]�Ux�����]oe|
�.��U����O?uj=�cy%k��.��=D� ��Z�T�t���!�tl�o]��U�Z�+�&7o���
ve�XT�M7��*�q����'��K��/����F�D�6S�&�F�^�n�T�q�J2��y��/���/�����vh[��cr�����������G������p�'�T��=�����z�PMK]���C�����y�^(�z�W�U	�tl���
������AA�GzX�>�1Sn����g�J�+}��sZ���~��k����J�<�u��x+h]�m������L}E	]s�#l)[�u"� �3��^ft�}���=J��:'}*�z�����hW��t�j=#�7��YT��>i�
�������g
����NV)24���k�������W���JO:vUP��NJ�������-S-������OE<'}��u^I����g#ua�W���=I�+�3u������}\��z.Sw���Z��_�Z���T��cUC���H��}G]��C������Y>}~����R�yS�9\����^��g����/�3�|���X���g�Z�m�:���J����/�3zx�=#�����!o�_�9fT����5����|��H
��gX��3�*�ne���L'�tR���e����4fh,le7>�w���,*������<5�D,���#G,uM�o_n����U6�s��;/�{yo���gU���uO���X�����X�L������;u
�����D�=W��C��=��#�l���������~����v�U�����������:9`z�����a��O�.u5��'m�~���Z���s�M���������GU~����0���0�R��(7���-k;��������{��+�U�]������R8���W��)'��aR?D����>�R5�m����J*���He�*#�=�_;t~�c��^���u�6���C�LA��B�5����*4V�^��!��k�0�f��hZu����T;D�>��@5�c�R�|�����_����s�6�����V>X'���z��:):�f]}�f��a8���a�m���u�J���X���m�����V�-U�B�����#�h+���������=4�����`���Ax?���@+��=��y���=�i���4��'l7���U��bU������u��?�y��3�G�<�^�D������$
������@��&����O�*���(	�j���?OTX�cC}�w�9����E�
�.��I�Ij5��������I?����*H�I5����������}��/�5��T��>��4�����
N�����x��\;���k�}:���0hx�]�U���]���2u�Wk�/��W���Z��e�Jr�:���
A}������m�CY��#,H�k��
m�mN>,�����I��&�r:=n�lo��em^��u)�CV�����0�z=�~`�cK?pTwy������o��Y�
�uR��T�8]��S�{�����k=���j���
�.���������n�����0�\��T��^j�]���e���f��a��W��a�G�E����)����7�Z�9�Y�(��W��������OS�_��g��z���B��OY���������8��V$��u��S�WU����4~������}�<c�w������U�quG�wY�#�{�t���
Z5�OE>k�<�^��x=�j���|��Z����
��:�������u8���Y����T�����Tq,��L��z6V��G*�VE���N��X���!���k�g�_=����[����=�m���$y^+���������9�����./u��u_��c����aO�{4�j}&.����I=�)���^g=����6|���|���~�3*{����H�w����w_��*d* �{@8��^U0+��#��z����
��[��	?Se?��0�ef�]���/���
�e�y�^eszF��%�r���E���������4����U�[���mh�r�/�2�h;���
a�S�����+��D����������,}��������"t���U�P�-L���#| X�R�/��k}�L��+
��b��u{������@�*��
����<�O����Mk-s��g=��"�Z��^Q�����{�n;Q��A�HG����+M���;(E��"M����X��H�Uz������D������_'7�5{�}����9'��d2y33��5k�������09��*���l�e<�K���Q0Vj$�����w�Kq�	sX�{zP�K���
���Os�
�{������Ca�W�9!��e����+I	{�����8.X����e��A=<[*>���7���p����ua��a����[�q~���)��b������Xd�kV�����m���������� �$�8���b�A:�E�G�u���}>�����M�r�xp�����5�q�76��F��Z
��`�pZ�����#���nXE�9���
���-�E�}�N`t��l����p��
��|����eKr��JR�c��]�k�6��D��4�R��j�&���Nab�ku[/O�,8�|~������C�Gp�D]���u	��������(�6�p�{��,u�Ux��-B1�,e�2!����b��
k{����>�+I��g�D�\����nu����k�Q�bX���+I9��/�����5Z���v���������+I9�s�n.�]�v�?`m��n��x��+I���b=���YaP��qlmq`������Iq��M��6�?+m:���~<�v��$�n�����=?���R&7���������s��m/�B7��^�+Ia�B�v��+��w�m
���6�����J�x
_�I|��O�����m��F���#���Tu���3�m����oSlR/g����(�~��`{���;��h�����>��f��3�4%��*����n��z���;������n��x>����R$U���(��;����,��O,.���{U�C�o������/��Q��L��	I~�nlO���%����o1*�����)��y��-�s<$�?��c��+Ig9��eY#�P-�/~���(\��+��dS\��	�e��e^�h�{[��{��6b��k0���3��6'*I9���7�����p<�y��@��W���=�}L��6&�����+����O����y{u�*�M�s+���<\�7��oW�s�cx#����m�}<�������|�]�Y�0���x�B�4�$�xM�cy�sB�iP|CC@�C����2���dXFE��27i�7�����x{��t������slj��}6k� b����ef-�����
���
2�����.���V��d1:�(��Bh<����T�`n��Y�/��(3�)����P\����~�Nir�jl{�����8�1�L��g�1h����������w���y�	1�3�f��0����}���Lx:*e���hx�\�x�v�-���I���0������.��)T	��bfk��3���\7�(�~���r�����wh�u��5��<���=7����"�u\	�[�N�^��B�g�q��Y=\&(6�_WJ�����p1�5�~�^�9�%�.��Xo`�}��:��A(�~i��v��������{����+;���'���M>,�6���>���5������s3���	��|k��V�f����?��
�Q��&47���<dJU���v�������N<.}7��
V�.��[r�i�9����?p��%�j�KUK?�Q��1�`�����m��Dl2�W4	���V^�Lx��{�c��Q���gK��$,����n��M��_/�B7����6�p��	�q2g�m�x�z�x��I�����b~r�
}�*�:���"���Tu����������;<���Q6��p���t������.��G��i?��@/�o���#\���v�
i��������T����]�Kc�9�8�w��U$����*�LJ������l2��7�8(�X�18��>�5��.�]�������Q�!�������~~L�I�E�=|[�q��7fe�W�$�����*��q�V�-=�`���O�P�z�-�Vv]��ks��d��E
l�D����!��o�<���U~�=�7�������e�es�26E���5&-GC�r��F���q��5U�����s�y8�?����	�u������~U��V5���}��<\�7��o�?k�-��Y�H��c�jY�s�0)�I":����k1 ����������*���������sX�������C�����Wr3iX��%^���2��2���8������(��\~��i��9����b ����/5��f%?�������\���|���������c'���
��
�_U����#=�>�l�d����)�#��w��"�}�2>��|�it3�1���u��u%i�5�X��_/�����e�@?�B�5U�����2��v��f������!"kw���N�>\��2:��_��k�:6e�Z��q�_K{���+&��nt�^�����gb[��t�Y����<�+V|�:f �]�DYdC��e[[�	)n���I<.V�����G}�u�TV�0��Z�J�|H�cW���G��5?S�p
7*�|\Q���]q�\O]S����/6v3�i�N�n�A���+J��<)����������:\��A��f�������6��?_������\�w+��\�����?K��2���z"������,t���4��4�-����@>��lkx���wY���]&-7��o�<���o�3��Q����ck}����l'uZ���.�hi[o��_m ������2�g�i0a
%h~�	i�F�l���������e���U~3;y���f���M��I����M\e�f2�43�#/Q)���3��dU��{��g�s�,�8��Y��qw{���-��
J~���y��xr�(FZ�����Q�����z����#�t3�-q�K�2������a/�|��1�qu�{��*��[O\)��%J��o���v�����/��)[�����5�L���x�`n��Jb���#h��RNz����_|�v�M�s���n��*��|���\��z��y�Y��K�z�a��O��������l��^dU��lb6�x���G����������oR����������$e
�2k+��=[��L\v��g��&�sE��������+J���w���������JT20�^6���A�d�X���/X���
Q�5�������c��D�
���>����@5ko��� `�������Yv�����^��|Z�y'�a�V�e�g�j=���2WO1mXj��mg2����n����=��4+�zB��K
_�#�u�B~lSs'�_3B�z��C�(�zy`�S��^��>���������:����i�}�u��YG�^�h������MV��,]��&��QW����&,�4i6��������f���#���)���1�E��8�����(��A����Z�;����j&�*��w���H|��
�N�8��)M,��fA��
Ei?�Q��X���]\��>���cq���>�@��{���}��TUOt��n_;e�*����/����<�*����~�	��Ln�E�%�������o�-j��7�,�G�����T&U�������]��t�x'�,��w��t�|�[nb��*���g�_�~���N�uQ���^e�6�M�t�&���N�@(����MN���eqL�'����yH�r|�95�g�:g�ym)�|����-n�]Q:��Y��������i��
Yu@[���c/�I�nq��	'�T����g�R��g�c���Xp��^�@���#�8����m�Q�W�f�M����y���,����l�qVu}�7��������~<n{����� Fm��V9�Yw3W������������,��=@IDAT����hD����+��b���f8F]�{a�^���I�h�Z����{������{(��c&����������Q!�r��X�����"EE�k�|�Zf�{�e��\����~��(��l'
��5bbX���S�,�n��{��p1R4����5>�}�f�G��f�������k���y���gvgL_7��j��~����������������X�b|�p��9+_Q�	���2�Ek�DiFiSV��q���.�G�~l<��wl|���B�u_*��@���,�2GY�n�b������m �52�����I��Q�49��I2����4K��l�M\�������������|>�����H�$����8���v�����&��r�/��\����]�\e�k�9f����$��~�%*�q'�Z+��[y=�h�D�Ru=Q��P��eY���:8_G��f��������[f���_�w|�$���^�/*S<���U������~�v�����3�LP,Jc/�g��������*�����<�F��q����M\e������Rt���K9y�x�����x3��\K_C��/�����a���cX
�����y�c��I�����Y�����c[��((]>��<TB^dMQ�����'N6�n����t��~��'���=����o��:y��=����8���{����� F-Z;�Xwu:��qv{[�7��oW�����
/w����e�����]��C���\�]���k+��Ei;��t

���r���
:O���K>
f�2���5�\�6X�E)������5w�����l3�f�q��M[/�-��F��%�X�zk|\�\��D�Q��������/Pd����[����[:�r�0����\=�,����X���z�n>C7�G>���3O���{=��f~�P������_Y\zE��D�F���o��F3��������7��<�;,�Rw6�8�,Z��mJ�O8!�m�:�A&m
��L[f�`.���dV`�I9��7���m�u���h���������Y����[�U������o������a}l?Wo�Gsw�zc�O��;�����^������[����,��k��Q�y��5s�W�2��jeKG�g7�����Q�������1\�Gy�������"F���G��c�%v��|�!k���x�U���D��qM�f�To&�������(���'Z@����,��5_w��h��1��k�3�}}[znkz��q1)�N�4�m�������6���vR7������o���fF�|����x"�e���-���2L���f����X�����������,l�m������X���X����v�e(��U��C��z[�V&�FAY��R����D_�����B����^������{���?�%��5c�W������;/[��K�ob�]4����!\=+h��R�����d`���/��W�9�<<�����;���LBe|�:��;�'9��m����i��J��z���W����7�z��y��oF���V��!�X�d��}	��X�Aa��{W]m�l�}��^�U66-�n������4�����I;�2���9���k�Rf���dM!Wp�������y��M�����))�v��&*�";>]k9��m�]��PN���fw/D��,��c����'Y��g6���x���|����g6�����t���e����^�������uA���4w)�<�s��Xg�(8�+�&v�	���D�n����zb��Ny=E)n�|&3���;I���I�6������U��&�_x`-jf���qt���-_�(����+�#]�A�w��9���?n����w�=S����)���������J�m�	���k��� zx������.�5�|��qo�Hb���
��>b`2F�~��w�n�3e�hTO�]G����L<���Rv�V�w���J�=�]Q��wQH�`/e��D9�����h�Qo�'�4z�n���9��{U��k��������#1���A�X���%&�,a�����vo�����I���&~���^�y�a
�v��*�����2Lz���l�]7�.�y�&�R��f�xZmWU�=�#G���w��H��625c���~��C��z�����/Z��
��:����������9����WYh�0��$m��.��ub�%�4���K.����{wi�B�������=y�MlE��H��%�\QJ�������FqTq~)s��%�*f�Q��%�P)J�7�H�Oz���zm�^?o��W��i�cn������2S��Y���V����\Q���K��L!��Y���~>c\L�s�S���6�<nmE�Y�g���
7,<:0c��afWt���2���[�cU�C����{����������s��3S�%�b�.I�|��;������~�b�,2��mQk��"��9��R��n��c��Y��(k�{c�����V��A.S�������w�z�\�R/L�\U�#/k��:�����c�l�Z����z����N��w���t��2��N��2�.����ul�2!}���qV�W����F��n�x����Y]�����=q��������!/����;N�h�Z�LAA�B9����X��{) yc+�.R�r��f�^>$����/��HP�6*�Qi�BE�5{�5�\8�e�Y�g��g����7%6.�<�t�n'�����	��1o�	jVi���Q/��T�Fi�y�����d�3C7��}�&{�)n$�����Q���'V��i���A�UO�K�P��UY�'�U�5�xw��$_�f�G��j<�~3�5M�wU�����l�������3&���L�����d~�4�u��\zU>�~���T�Ig�����u=fE��w&m�#���U���������ss��������{�~G^���_��-�8��d���s>�����+.��v[&���'��W���4��IL�Q��Q@=w��js���������m3�+{�f�j7c5qr����.R�?FC%�{��E��?J[�H��������t�~�{U1nZ��[>�����oFQ:}���);v�UWe^�8��k�0��S��)k��F���1�z��c����m��:7���t�{�������{�2�Sp�}���m@�?f%��g�];L�1��-��X\�����2'��2%&��m����������e�p��~���p�Y�~,nqat�)}�2[�m�Y<U�g�7~������Ht��e�9�i(��mm�u�g;�n4���/���O��U�g����}��}x|������?mQ]�s���SN��y�W]}u�%s>����~�)��z ERu�O?�T�V(��.Q�be�����r����K�2���1�N�,��*����=�P����U�����sU;1�s�u�=]����4y��������``'�Uq`�Nx�7pu����;��K����E�
�"���?���7}� �Fq�z�k�~��8���N�����%������;��j+�e9����������;oH�+��C����&-������)�(�������V�Q'����f���/��rk�=�����,o�^z���\�7S�l�����w3��_�������������[y�Y[��[�k�#~�-K��,����y��;��>��j=���_~��,��Xw��h�.���}��,�|�I��Q�=7H���`<\;���-�V�N�V�_��^����&!�7�|��EiG�D����8��W��(]�c�V���fV�������������6qe;�������M��nw���������P���q�������c:���C�
�����&!��t���9=L�[�O��/�=����E�X�����/`�%��3B<��>K���ku�{��cE�hH��G���k����iA�����w{?����3w�E�;�?�
b�*���9�NE�u��_|������������BU�����cU|3��v5�\�e�xE��p`i~����(,=wP?������f�m;����K@����n�>����F��03���O�����M��e�g���AaU4��P���K�0df���hmG��u
]���fG��`?��@�\v����r
��8�q����*=�������-3��?
�����_����5�\ZY��k�=��A��?�$�����O~�->����������l����Z0�Qd���i�vl����������G}^z�>���������\�����l{w�U��%O�5��k��u.��.��s�}u~� �X�����=l�t��v���nE~��&NR^ht�)�<����7��5�/�FR����<��g�0�����"�p'����J��l�(�wL8����%�
4����B /�e��	�sqk5������ m(����
���Q��@}y�����#-�<|?���BG��(Q��G�*��y�&�P.Z��[����x�O���E�`@�\R/�`���I,~M������#�^��sV�Lj�
E&�EG��v:�W��z���+����g�#�VRO
4��k�Dk���Z�|~����� �!�[�f�qV�Fi6}��+��b��f����K�F�m�x���O���k��}������j�|s���@�W��B�UO��g;�*�����F�W�����G�q�&���g�W�q��gQ�)�����*�I�������cU��C�}FE�6!�6v&e]|�����!�t�U������[&�U|3�x�������)[
��6qe;����K3��.	sdP����z|(�_��T�p�����%�����LI]g7������q��A��[fq2���-����)�w�qsU��G�v�\��V��3�>;Z�Gx�&�n��2��U��B��w�9���1A��Yf��v��'��Nty������l�=}r&L���.'k���R�}�"p����&��e�F��T�������iW�
U���S��b��cne���x��U��s�3�|��������X	�D{��z8�_t�nV������pc�bS��������"�Hh��9�	�I;nf����p���w��3k��`���Yy�f�����5
|`�|4Z��b��FW~3�O;��A��������)SB�����f@$�����c�!��e�x���|������g[��=:��%����A����8��Y������U�s���Z��w�����>��bA	�E�f���W��w��"`�UW��r�)������(c��^��Zv`
{\m����x�N�R+����Y'��Yf��p�K�z�[�(
:�~��C�/S�v�����],�2��\`
N�t�QXx��<��s��Yb7���W\1;L\�1��tb��s������0 \�K5��7��Od
A�������:���]S�x��s��2K?�d�D��	&~���w@qK�nG�����qI��ez>?�����;�Mm"���N��\f���:�6�Z������h�L]�3����,���b����z�����6�6��g-����e;_����s^~)�|���:�|�	KD~,����[����;���h�;���}���$3����,���69!����u��m��6�C�������?���8�T�����w_�o��Tbc����=�]�W\�S�P��4S���6J�il6]1>���Q���s����f��y��X4Pg ���[���d#/��9k�yy�F=�,������o\�lk4zo����b�6m/,s�.�?���s�&�����<sP����A�������� �==T�;C��Z�����������K�_�O�����h�D��^�O��m��a�Z�7��w]�[��>!�v"m
��Q�hWQ�c��G�����A�$z��_S��P>����l���O�3�}��������?��z������o~���&g�����v��R�>T^h�=;|���~mTx���^+�}��Z��>��6��E�����c�E<Ld�i���[6VAn�	'����n�g�]���{����o������(��O���8�OE�J4���L�����y/�FZ��O���q[��r��n5����f�L�B��x��UU���P�_��i�cnEi��X��*�]�<�9�M��5JQ����V1����f��i6������f�g�S����
~��J�#��}^I�����:,E�g����"'>�{eS��5g1��E� 9.@������V5��h�Bc�������$���`~�������<�Q�RoDP�]��
�����
�H�$b����;f�H��jA!����-�pI��N���'k�V�Jn0�F��0
��f�=;����p�Y3z��=�>�JR�����mE�����Mlvm����l����G�u����m7���6���$2�@y�J����n���|"��j�D�/��E���7��jkG�X��*�W6�-�!
�F]I�JR:Y+�\�s-y�������?�^��?���+��lm�+sn��7V����p�)P;������|�=f*�lm���a��`(4��������OZ���1���kI�+�]y��$��f�wCZ\x�#�QXo�����(&]��y�s��N��~�28��)�����=���y�s��_�l����u;h���?����HQ=��u��3k�V���m=�(��3��W��^�!xE��5�o�OUm�Fi�|+�����lp.�2���w;��0�0�0����{�hm���3�V]O����,��*��^�nf��
R�P�1i��1QaF�\n�e}������(m����Q�7Js/�W��X[���|O���w���5fYs���^���}�~�[&�U|3�x�E���1��]V�~G�T�&��l��=J����Dl?������O�	�w�%b&L��`������50��H����o�-���"�
����g��?���iA3J���=�+-���������p,�q���1���k���gD���3>}���(��K]3��4�v�11�5
]1�p%)��I�C),_�_���>q�	Ee~����*�7N��{PV���A�*��q�m�7���[Yz�<^�7��oW�����|]sNX�/���a���%ov��|�l\
7�	HQ:����^.U4�� <�-��YK�kWfN�k�ut�^�>���}�c���W~�e�>3F\�
!�����5����o��u��C��
^0h^$�JY��O�y�����2����1m'��|�l����1p����a�n��V��oZ���b&`�}8�"�nL{���za������t��x
����$�_x�1>���Q����s�
���q�WD�z�>f�Rf��,�od�@�\C`u����mQ.n
f[�����l�>8h$���x������/����������C���YF��'�G�d0������4\lk�c�=��of�j����)	Q��:.����m��|�%�)f�������"��]�nn�)wEB����6�S��	�(n�vQ�k
�(����a��A�0*9�-����w@���2�!��zV��Uj�3�������K[�N���W�u�]S�]�����%-p*z����R����x��)�/����o
u�f��;+��#�����cf%����K��_O��(��RE���>�,��q�/e�t�f��E�(^��z�(���\V��#�,=P�k[�o�gUm�f��l�b|���e�r�`[��i^������S	u#udQ���-J�|��z"�+��YF��0&2��������|��q��e���P��������#��N������q�m��E����(Or�a�U�;P��i������V�N�����.����S�6��}?��G���7�V��v�~��<������$��e^2:-���;)7�.���i�e�YV����]{�m�$��m,�H�hWQ�c�hw�Q���O���F���P���/i��-I�G��v7�
�"y���O����#�7zV?�\���pqP�q)&)I����+:V��%s�+��+E���y��%����|�s�g��[�����|���'pe�j1[37Z��5�Y�|�V�����6���gQ���C1�V�_�����'L���c?���1����Vd����"�u�z����E�Y�����A�0�|^&`�e���d2��?{�.U������t����^v:���<L�y����U��"-�J�r�~+3,9;�@�/[&���A�L�����������?���xN�c�q&�r����z���X8��#�[�� �Ef��9�\�~*���"W'���t|����E%�*t��fd�c��U$�"����t��B��,��MY@��S8�� ]��y,*p�5�\���>((`�����If.k�r���������Q�����kg�������l@���x�X�b����]��cAS������������b/[�m�q2����w�}�[�l9��FL����0Ck�i���,�?���u]F�{�V�����.���p{�j���3�
::j�Z��^-�-�[V�r��g������fyI�c�IY���|R7�/[Q���Y���_3�I�y�����.���a�����
e�E����6��4�,� ���89�|J�����X�J(wX��e�p��V�����Yg��l3+R�+���v8aA��~��������5���o�e���U�Q�I[���}��
��w�����Yv����|S)O�q�3b��,��x;�D�~L��UY�'�U�5�s��-�������}6Q����(�f�k����u�N�F�_/��:W�;���F�=�2��V���*��p��2�����Y����mQ�k�����*������gi�X���X
iq���u%�:�C���czx<��q�Y�c��$�F�b���)�J����k���(�
����_x�Mrn���4���v<4�o�P���J|~��X��o��>k�cqU=/��i���;��e����Y��W1n��f��cnE�_�7��oWQ���X�����<��eL��IQ:��8�WD����=O�n("�2�z��V#��<j5.��^�u��j��3x6�M6k4�Jy�WoG��g�?g��T�`��3���+mc�M��U�	f���/����������(���^��OD@D@D@D@z@`
[�}w[w���FJR������������]���-%"�~"0���<�0!n{[6I"" " "�:)J[g�+D@D@D@D@D�/	��1ss��+����.���v�e��L�%" " �J���eV��.gK�HD�_a�i{[��A�t��/��t�����@/HQ���������������4����SNY4������v�%����������F`�v�]p��G���#CI�u~�Xs�,	����{erto�	��)�c�#(�" "�]=�`�b�)�����k���b�����Q=��C�=��;�'��O����T"�H���3S����K������_N���o��sM'Wy�iT
(" "0��������SkSN7�t��7��\���2�����%��#�H����s��6�W�L6����2qc����d�{�������������������������@���~r���9K��)J��V���������������������������+)J���(]" " " " " " " " " " " " " ]# Ei��*b�~% Ei���KD@D@D@D@D@D@D@D@D@D@D@D@D�k�(�ZE," " " " " " " " " " " " "���(��7�t��������������������������t���]C��E@D@D@D@D@D@D@D@D@D@D@D@D@������f�.�����kh��������������������������@����_���%" " " " " " " " " " " " "�5R�v
�"�WR����Q�D@D@D@D@D@D@D@D@D@D@D@D@D@�F@����U�" " " " " " " " " " " " " �J@��~}3J��������������������������@�HQ�5��XD@D@D@D@D@D@D@D@D@D@D@D@D�_	HQ��o��t�:�li��M_=�����'������m*B($�j9.�DE@D@D@D@D@D@D@D@D@D@D@����.��;�����W^{-��{�y%����O��zky�]���Ot.�Xf�e����������3��'�z*=���1Hm������#"�6�f�q�7��" " " " " " " " " " " �����E`�	'L��3N���'����)��2-��R�����Zw�q���~Z;��r�-�N?�����
�����g�V���~���E��r�V��HD@D@D@D@D@D@D@D@D@D@D`���c�N���B��>H����+<������$�Y�;��AJ���z+�����W_ye�Gm%�h�EN2�������;��t�Y�JD O������_a����7���<��|��l9�Bh���-#��P���_�=��+S�����GK����4�$���_t��i�=����;�������+��b���G_x���(3A���*��o~��YH&4)J�)�M�JAD@D@D@D@D@D@D@D@D@D@D@�  Ei��� ��zj��L�Zz�A��<��A���V����/"��F����u������������������������P��vX�q�k������^}��;��J����-" " " " " " " " " " " " �N@�}���[n���RK��g�9������L�\}u�va���9��i�Yg��=����_��#]m���8��\Z`�jQ�;��i�����~��>��S���OX�E��:��o}�[�+_�j������Q��W\Q��_?�<��#Fd?~�����^����q �/~������J_|�_R�N<��i��6Js�1G�n����o���}��t����?��.������I'�4;��nH(��E]4�����K�N�x����7�}������k_�Z�����[������7��]{��jj[�.���,dn}����Fz��G��]T�m���~Y{�Y����?�n��T�����K2����E�e<o���lZ�����O�,�H��G�]w�9�.n�I�/�[�,���b������;G�������b��~�=-a;L0A�`�
��s���K���/�[n�%�u�����|x���L=��������w�}7;_T�9��b����x�����v�z��+��c�?/���u	i�9�>�,��g�I��sO������^�" " " " " " " " " " "�/��l�i?�eb�[`��v�<t/o���B�p��GgJ�|bQ����5H_����8���Y���M��B��:S������f��6K3�4S���}���^H�n�I�a�1%f�]v�5����E���O��7�4Sd����������K/�ix`�,��(����z�3��9���*��:(�@w�u��w�!)�^P�~��7O��r��G���������,���{����>;-kJ�F2�l��U�����k�	'L�vXB����sy���z��2���-��"y�oK?�����>�i�_rIv���>K����S
~��_vS��o�)�L�]~y���fEB~���~�)������=}���,�������E���	������{��F��^�����~��x�����>;]T�9��):]���)#W\~�����O����'�<�G�������wZ~<��O<1�����s���\���s�������������������������� 0�����nx��1�)�c
OoXN	�����>y�QE����I'����w�R%)	�J�N�0c[Of6E��?�y����CiH\����H�I��
���iJ�2�����)zv3EaY\(�x��	`/�Z�)II�����q?��e��-�Hg�sN�����1�9���61ep#����y%i�f��J�=�@�����g���(��q�_���~�����-?�)II7V������cT��Yf��>���������fa�JR��r�)� U���m�4�)I�!��<�P]n��iGD@D@D@D@D@D@D@D@D@D@zL��4����o7�4�d�~n%
�O>�$s���Ya~�\��n��(T[��1E6���;o�5,����������������Da���!�s�5+V�Y�1����Dp[z������Yy&*��{������f9y�m�q:�s-�q
U,���)���Z3}�+�dSaZ&��YW����E��#��+���o���a=��
7�~�y��
s��ll�����7W��
���?2������lS��ws�)}Q~�Bw�I&��Cu�����\4G��7����/ki������\,
Xp���"�?���Nw�qG����q��0��3V�����_��Wvk�Q1���uf�������VZ)���k`�Nq\$�����U$n�]������X����g��v��=s����s��!���l������-�UW^���/������QG���z��(�bS����Hv�q��a,���4�TQ~N4>e���f���(y���-�x-o�n�]v���=��" " " " " " " " " " "��zw��nMqo����������O���[6�z7���	kM���kU�0��LX���d.=])��lsM��Q�`�p�R�����K���W�3E��DQ��u�����@;��Z�(>]�~���mS��1�t�Y��R����uq������(��7�WT��|X�z<���)���K/���p�����&����t���ejL;��dk����?����>Hs�a,;g�a����X���:����]w�-���>�c�S���+�~G��~��:�o�bM����0��5p�������gP�F%v�=?��%7�fAs��������J[n�U�P��G�������"����o5���4���J��/��v�5x�H[U��%S�;d��Zk�1((�4KR&� �e�=�DD@D@D@D@D@D@D@D@D@D`x�����>{�4�z]��K.���?����~�-V����� %^<?T��l�mMY�Bo�-�MIJ��kk������d�g�@ng�HC�R$X���eES�E%)�P���*�E���i��|w��?���L9��B����2�y_���w/��8����w�������0��.(������OMIJ�����5�\��e���Eh������f��"�#�V�$������`��������.�(d���~��e&y��	y%)	������4�@���s�b��y�����nw���(?��������5lc:� ^�,h����@����������������������@���C�FV6e^T8�����)�<yXi^z����/�(o]^{��t�-���A[�&���(Y3*rb`\����HPT�N�w�X�	
���������X������Y������?��w������W�;��-�M7�8��s���#u�~�|7S������~�w~,Sq����{�|8~�n
�2y�,X�v���gmw����������c�g�k���Pi���3+h�����~���y��,���
��y��?�������|���wU������������cK��K���]ci-�~#��~K����ym=�(w�����Q��s���x�Ka��O�-���/���f3��y)�$%��g�1���k�k
�f�Y���xF�����g�)WhOj���\��f�u�J�����.s��;������\�z�l�3ea��^���X�sY�������f9z��W���-���C��_57�(#]�1o=A1�m� �Xwq[��)������K�\Qe�y��Mc���1�~y�]xa�G(7�wR���~��kwG�����~��S�������s��gNg����w�#F*Jq�[&_�@�l����_Y�x�uE��Q�=����6G;���"��6��rn����N����TSO���s��nb��Eb;ppk�x<����.�����)'�d��:������_Ng�yf:��3�5����o��v[|�!�?��#k�Rw���(/��b�Z��1��.�����`�|��7'w�L�Zr�%�?�����61���j����������������������@?���!|+�M>y��E��N��u>����w��k���M6Y���i����k������y��^h���'��M4�D
�-������b���Kfq��_��������({��7� d���~��p$a������
�\�/S�J]vk�>?jT���������zy�~��'�����l�}��tdP�zZ��~  ��!|���v��y�L�D�qW�����b!���R�������I�m�+xn����a�N����:|���������(r'�x���Y�b	��G��}�?�<��k���n:g6�\����/~���O���'���
����y���a�=�Z��Mkk��`�ebi�5�x��W����8"�?^pA�x���������(���az����
7�(��R,t�WZ)����SO���Su�����k��&�`����[�UV]5a��q����0?�������" E�P���������8�j���;��S;��A��O��`�b|�������eW����/��~[���/���7��
m�f���.N��|4�W���C���wSKD������~q���#-�P�g�}�"�.Z;��n��2E�P?��9��%�X�o�G~xMQ�z��3���O�`b����[��N����I'���q�,�}���w�_�6�b��W���\_s�DD@D@D@D@D@D@D@D@D@D@���\���{����Aw_{�u���X~����������^��7����~�v�_\#s�e������w�q�m�������7����bsb��uy���|7=��#�}�.�2]��[�y����f]��������3���;C��(z?���Z��kP��7����0<��wQ������9���\������=��3�K�^����{$��.�q��������������������������@_��t_�#��\�����/�����Zi���tl�\r���$L7�ti�=�������}��G���LQ�\;�HP�d�o�4������y��P���}�Y-�����*�:��c�)���U9���?��Z����]\���+j��;X2�s�}5V�����i��t��d��������z�������:�|�e�n�vp?{���5�\3���i��I��dw������}����6�d�Anw�4��ERE������+��:�?��E�JW_u����w���@�����������������������P��t(��{^X(�2��W���"ek<�)����3N?}����fE������mzrX��������z���b��p�l������d��\�.`kGFA�x�������=���_c�J����Z|���B�r-�u�[/�����,B'5������fnw�l�iF��X�=?jT-]X;�f��]%�	,�o��Dg4�NX��C������O3w����`�
���j^`��f���������~�C~������^��'�<]|����{9��$���L,Xu��F������[,�����ERE�a=b�1��K.�l�[a��em�� ����?	s�)�q�A1��E@D@D@D@D@D@D@D@D@D@D�'�3��I��������iuS�L4�D�Oc�T�������3�������x����C)��_��?L���O:<X��H�2��7�L�3�a��@IDATL?��)oI������G����x��i�W��@�|��Wf
[Q(����Vn�O���?m���.|��W�
�=�r���=S��@]�������4���<��27�{����/i<{^�W�h����?���L!I^�<��J+��l-���z+�n)�SO3����?���Hy9����W��kJq��s���������J��Yf�5����\z��G�����?n��Y�/B�Q�ck��dk�����z�����d^��\e�Uk�O6E'�����{����s������[��� �W�,9c\U���,{�!n��/���u�y�(P��w�Ae�[O8�����M�uql���i���cW"" " " " " " " " " " =#�?Z��=r�����]gt�K
'�d�4b���JR��T��mjA�t�a�
J�9���+IdJ���~P�Vl��V	�O8�k��g��AI��*��w�y'�t���`e��(��Voz�Y���-
:�������M!��O�=V��n�q-�r�e^Iz������+(`I�[9�x��N���n�z�����w��m:��Jo�/��z������'���X�.[�"�g��3
.{Q��6���p���{�ha������U����2���|>��s�������B
*�(V7X}�m���A�=�:�" " " " " " " " " " "�eR�vp3�?��3i��Kw�uW�"�o�[��
^p�����,��!�{�ecTf���m%,���7�(�E�Rw�K/�d:�\���Ss��R�n?�[��8 �el��x.�5T��{��t����������\��Q�=��c.r���9c�~��-�t�\b�����Z$��h�
���_t:;��"q����������Y�n��v���X�0Z�������/���-z�{�,pW7��GuTY4��~x~O��fs%��o��A[��Xr.����,'c ��
�V���^n~bDd���������G}�?3�X��*��}���u��m.*<��(�73k��s�t��5����r��������������������������K`������v/n���X8�����o��a
kH�;�|i��fJ����,'�}��1��� �k���W����,y�\�����2����Zq�/})sy���&��C!X~����:?0��V��+f��r��|�t��7g��+�\0[��0.W[��%ms���c�92���^�D�+�����Y��(	q��K�z2&<�TSM���g��:�(%������he�����)M)7�1��\��5G���s&O=���M%��������HD@D@D@D@D@D@D@D@D@D`�	��z?)J�<)"����bx����?����ND@D@D@D@D@D@D@D@D@D`���r�;�r��GD@D@D@D@D@D@D@D@D@D@D@D@D�!)J"R��F@����F�<" " " " " " " " " " " " " 
	|�a���|0M1���^}�����_n4�?���CD@D@D@D@D@D@D@D@D@D@�R��
oY�8�Xo�u���v#�c��w���(& ���\tTD@D@D@D@D@D@D@D@D@D@D@D@D`��t�\=��������������������������@1)J������������������������������0& E�0~�z4�bR�s�Q�aL@��a�r�h" " " " " " " " " " " " " ��(-���" " " " " " " " " " " " " ���������D@D@D@D@D@D@D@D@D@D@D@D@D@�	HQZ�EGE@D@D@D@D@D@D@D@D@D@D@D@D@�1)J���������������������������������������������������������������cR����G(& Ei1��(�/W�&" " " " " " " " " " " " "PL@��b.:*" " " " " " " " " " " " "0�	HQ:�_�MD@D@D@D@D@D@D@D@D@D@D@D@D�����\tTD@D@D@D@D@D@D@D@D@D@D@D@D`��t�\=��������������������������@1)J������������������������������0& E�0~�z4�bR�s�Q�aL@��a�r�h" " " " " " " " " " " " " ��(-���" " " " " " " " " " " " " ���������D@D@D@D@D@D@D@D@D@D@D@D@D@�	HQZ�EGE@D@D@D@D@D@D@D@D@D@D@D@D@�1)J���������������������������������������������������������������cR����G(& Ei1��(�/W�&" " " " " " " " " " " " "PL@��b.:*" " " " " " " " " " " " "0�	HQ:�_�MD@D@D@D@D@D@D@D@D@D@D@D@D�����\tTD@D@D@D@D@D@D@D@D@D@D@D@D`��t�\=��������������������������@1)J������������������������������0& E�0~�z4�bR�s�Q�aL@��a�r�h" " " " " " " " " " " " " ��(-���" " " " " " " " " " " " " ���������D@D@D@D@D@D@D@D@D@D@D@D@D@�	HQZ�EGE@D@D@D@D@D@D@D@D@D@D@D@D@�1)J���������������������������������������������������������������cR����G(& Ei1��(�/W�&" " " " " " " " " " " " "PL@��b.:*" " " " " " " " " " " " "0�	HQ:�_�MD@D@D@D@D@D@D@D@D@D@D@D@D�����\tTD@D@:&0�4�������D@D@D��	�:�li��M'q�I'M�O>yS����z��n6��������B
jz�^Xd�E��V�KD`�(y�WD@�������ZJ����J���Z������t���@�w��k�����?����.GuT������]������>�N<�������W�����#���^y%m���]�����Z������vE,"0��k��'��TA�����}k��g�AQ�w����[oM�)��e�����[o�==�����g�IO<�Tz��'�7
��=#���^��^r��N;�G{,]v�����1U�ND�W��0��v���o��������K��O?�6�l����/��]8�*��8�y�<��oGi���7#�o�z>�����}���f^�%��3�T;��l�SLQ;���!P�1c���$�L�?��0%�<�g�r�)+�YQ��� __s�u�����h	�`�	F;V��z���h*{�k��=����p�	�8����x��k���[r��j�W[m�4����O?��v����[.�~������,���(l���{��We��\v��jyb�fH+��b������/c5�n�1y=M�_�B[|�����q�o������>�����/��]8�*��8�y�<��oGi���7#�o�z>)J��}Mj�0��d��"P	���{Z~���n���t�GT�"�H�=��+;��������.���m--QI����7�b3��������bL���Jh�/9{��=4�f�CC-����f�������@/	|��������?�MIJz�;��A����z+�����W�;��Fa��g>�~�]U�.U���������r���s�y���{�)�u�i��Q������~��t���`�	+�N	0���s��&�|��gi�M6I���^���z�HQ����~b�
.V^e�����2)J������Xx���|���%j������-�����0h<��&N
��Rd����������~LS-hG������Ko��� s�$&�u�����)zJ�����{�1Z84��{F���jC7���v�^��>���J��K/M��{nH����i���1����/]jnwW[}��W�$���{�)A%k�P�ov�<�TSM������{8�'�L;"���(�����*���6�z������m����w���:!" "0�����m
5j���(" " �'p���&��d���t��#��;�h6l�{�8���^ry��%]������YE��	���w�-�'��"�<8T�u_3���,���*�" " "��&	k����e��R�D@D@�*3�8c�y?��������~�wZ	��V�E@D@D@D@D@D@D��(m��x
�������k_�Z������W]��|����d!s����
8���f�<����V[��U���O�p�������'�����?�������/�7���B�������o������d����m��V�&�Y����m�����n���]�z��,�F�&6��"�,�]l�4�qy����v�{�/�3�B�y/e���v��������_}�v�y���1���&�:B��z��#����s���������L7�tY]�f�?�������NO>�dQ��c��
7�(�1������>�l�������7��v�;��������(^u����k���ik|�s�=��+����s����y���7�xc���:.��������f�e�4�4�dlX����/N�/U&��6[���O?�T���	u�7��A�3�k�>�L-(�_p�����Gy$;����KK,�D�����~����nY^(�������v�5�c>k����%;L0A�`�
�Y��}[�r}���y���[~��8!���.��i���k�~_�o����t���SV.n�����*��-�|�%�L����K_J�s���}4�������=���y�"i�����:M�P�X`�4��3g��o��
n\E�]�������<{�}7���?�?���?R�m�e����1"m���e�>��������Q(ok��fVv�S4����^.ZO����-�{�x�����K���o���,��M�S��g��9����I�����:�[��V��W��>x��D}w�W���v��������o;��������?��Sw7�I��i3-mm�)��"=j��� �����.o�|��o�i��k��v�f��"���OJW\~�������-�$?��m�Yn���K-�������fm1��l$�M����O<�};�f�fu�8����P�������;���1�
�����3;Q�/��[��|�_�:����u����f���[��Md�>�����;������ �U���1�q�F;jjkS.��������_�eRu�XE���@�����Y;m�����o���_>�om
����{o66Q���e�������[�x��5��8��f}����n�{I��L1���#��.��t�]���v�������6;mu��W�xYY�����X�=����]�%�;J��r��q�����N�3G����	��f�i�����1���%zU�N����b�����:���N�$������]]TfH;��[��v�M���B{c
������>��6~��C���-sey���20���X���%����l����F����+��_=;T�-����{5���xR��*���q&�r��z���X8��#�����q�j���c6x0�@������>�>��L!�����3�e
�z����,�?��lp==������l���|����~�)��8�?�����
�2�e��_lk] ,v=�+_I7�	eY^��)�h�2`s��'g
�|8�7�{�P.>B�7�b4*���k���H��G���oj�0p��C�~�l��
�I'�%�lSp�aE���<��j����-J�;������}�����N��`�`���l�n0��d6������?M��sN�T�7)��*������4J�w����N��+�'������WZ)ml���3�8��l���+m��v���)�m��e�������h���t�g�SfPZ������H��M�����-�/C�8���g\~my|�M7�~��[��F�Z�vS���4�t���)@^h ��������}:�'�rJZ~�
�aI�.;�4� �U�\S[35���_�E�N��k����l�|B��;X^�a��z��Nv���a�T����\�f�:�H�~�Yg����'�N��:�D�����:-/���l7MD���{f�X�H�.������B����c���~x��)������CI'�tR���X�e8_��5��M�(�oo������[g���6�,���'�c^�+����|����|�,�~�����������w�e����?�O������:�1=��9g�}�����k���������z�����A�2����^������e�dRJ��M���~�M�):���v�E�=�N��@�~����r��|d��h��������v�s��6���G�;aP7/�Y\�����s�[��w�����.��0�y6�:���S*4�^x!mj��f�.3��(�g�^p<���+�@SQ=Dx�����:��d�}���d2��}��fVG���s�|>[���5������~��e\����������z��:���������+���V��b�-�XZo����&_�
�w���|��m�cL��X����=����'L�[���E�^�s(��m]k&4����������w���|l"��|����#mB�+JQH2�<�#�`�����I�������?���
���+�����>�_��������]��b�.���������N�'�N�g�lW����6x�q��V�2~w�����qh��o�o�ADQ}��>�pe�i���a�����O���NW�f������������+j�v�|e�*:��x��U���m:���Z�]2���~�s�+���,<�����M2�$�:��W6p��-�Hg���#e����s��)A���;�����+l�]���k��`�t+��'��~z�R��,q`%�@&2E����/LXk����|��)I�k�{���rUK���}^IZu���������\��8���dy�"��i
��L);%1�9�uf��c�����;V�(�{M4Q:�V��h��2%)���&)�U>�XC�aLE�<,�g�y���:(j�����4��f��2�Y��o3Q\��!�A������H���5=�*�'��|y�M��	34GZ�����Z������h�v*U��N���_p�Ei�=��[�]��)�9�X����v�o�}?���D:�x�����?S����2L����K�����`N�o(J�_Y���1}(Sn������8v2���EB<�1��	�.�ovYz���l�����y,��f����w������k��?`�����L���.)S�r-�0�	�&���%�H�o	��w�@m2&~K����j���c
O��"%)����gGu?�(�L��Y������o�hI�%�����cf[���L�����9����A���^����?��P� �uH�m��K������7.��������V�����\kcaeJR��� &����'3��m�a��d7����~y���m_������v���]����y�xM��`U����g�hW;o�'������i)/;�&��M� ,ce�^v��I7��6eJR�����Z����+Kw�������"���W�����u�#X�0+�@��u������o����?�� ����
��h�.`$w�J�Yw��~�5�<��eV�~fm�������c&�	\��n��%ah�1��H8�@/�LF��������2t��,%>��,v��Me�	�B��o�R��(�vX�����i�>j%\�b�����3��u$������b���0�p2S@2��`�����Q�����!.3\p�Q$���Yc��w�9K�R63��:�����O����4>���R\��g�#���X�������K|X
n6`�X���cU�wW�bv�)xF��z�"���b��f�]>����5f��sp�w�) �u��e�cf��;�u��a��Bs4�)�?��S��+[��@�{��1�����k&1��*�0+o���,�R�����f�|C��D
���*t0�������/g�a}�/��'f"S�b�q���@q�Xf�)�������(�0�&wD��w~�S�2��c�XO~au��%����������_7w��t�t3X��������9y��7���4��q!���������v��B����O��������(n���7G������`mK~��J:����b�$bt�{��}=fu$.�p�M�����}"�)�}�\�R6)�^����{����`/�#!�S����9��T�xw�Y��b�sWY$q����6�O.LaB���{��vO���1%L��w��1X������('X��]\���F�:�;�5,3�]P6E%-�v���N]��c�6~����Pm�^�{m�)1�N���x���#^�<����X��=x>��@]�����V���oi3��Wm�V�5�������Y�9�B�U���cmj�d�:�rw���\�k[	��5��g���=�z����'��|� e�I���7����i��4v�U/������H�21�����Y�hlc�i����c�
�O�'D�~dR^�\:mKx<��EI�`M������V�����0�f�0�����3�PK���+����g�~�B���$N����[�6��t��,����������q/Oz���y&.�l���G�x|[u��2���:O{��^��*�'���u�]���{�oIQ��!>��#���"9g�$9+ A$��� Q� Y$)9KFr�%� IOL(�1���x����=sf��{�������3�������b�e��z���V��X�!�
����$D6[�(��:�6?i��>Gy�A��D{S���Wdh(O�>��D%8O����2^��kM�Z�g��"���QY���l��yyR���v�W����X?�K�������F�-��	��������v�u�d�}�I/��p���,C;HH>[�����{A ����`T�[�HX�ZY<+!��K���������Yt�(I�X��
�L�lA�J�����E�F8>�
�P���w�0ie��k���d0,jU������B}C��#K��Zb��zcoN%�a`�%��Kse��|���@@���&Y�1j^���w1�]��g$j;�V���q�8[�4l�2i�g��
�C�9+��Hb����(� ��yd����V`�?V�!������C����wEa� 
	��y��8���h;K�1�X�[��J��J?�I'�D�����[�A��'��3\�i�����~_������������-L���&�V���F���R�}������qBd����_S�E��X�N����J����a(w<��Z��o;�hyG�BK��w*!v-aI�W��S�������0
�5e/�)�"^�:����[_���*�X�@�����}���A��5�S($�4������t�rHr�����(���������Z�R����}%�Q2�)�8Wb�m�W������P.AD�PE)��}�{������7<	�a�"�/Dx�^w��#����H����e�*������f����C%�2�������\�����m��t��{�;���1��}��vQy
����|�����L�n���U����1����i���i}34�Ho4���L������oz\,;�9/�_k�y�p�����P~�|	����[K\#�e �o�q�z�z���
/�f> ����T�G��EyQ�F
;���",1�=*��*C`�@������;o4dh���X��S�
����E�:,��W�E����S���6��5%�����6�`�I��Y8��'�%�
�	�;�)m�ncP�C��b�J�o�&�@�l%�d�(m��!��v��\p����6Q�(Ju��\�*J��q+���m���=V�'i>~t����^�� |b��P(3���JE�B�kXI���������?���e>%�d`*�
%����zd��}E!�gx�d{QZ%)����`��2�Y%)��x��)J(T ����P<��=�b����^7���y{l���Dc/����d��PIJ]P�Y�I����(����*Jz�<q!����+�(��!����c�bN���:�vC%)�����%[�P*+����{�����^?g������p�:.$�v�0�
���{;�<�?T�P�0��Jx���A��u�^BZ1.+*�R�$�x���G��e�������e��x4�_����=B�Y<:������s��	;M��+!4�3��wD0o���PI��D#�]Bg+1w��Vj��&NVIJyw����v��U�r���o�}��y��)<���zY�P���y�xE�2���J������q@�$��/����c�BJ����P���g���	��O�:K<P����D��P���������V�v$~��3RW����y�$%
�����iJ`������e��:�����x��5v�?�����K?�}1�+1.��j��>��Ti����w�m\t^������yk�:xB����]��z�#���A��Y#b�U�%�L��S���V�0���
�&�8R���������h��nk�U���*jl�M�9��JR�>@-?�rM�(��e�7��5)��z����j�u�'M�gM���[���3V��}��v����������X��^����s��C%)��D�,A���Z����/V���I�:�5G�M\Q�&�-���X�����������a{Q�(1�e��#��M�@�o��>Z�h�x�YA�����������C�#����Z��i��p\�4����:.�V�f�A�l�Z���e���.��	Q�������/L�=B,*��6��>h?/,�b�p���J�y��n�&�{�r�P�V�K�����Y�gF�g�����J�sL���6�I�F�'V)f��1��a������X�P�wm�"z�.�%�g��kX"�C��g���x������[g���hej=��g��&�K^yE����DX�����!Jxz�!��SbaS<�}��=DY�g.d�5���O�x{�DSP"�1�=)u3��R��[�>��q�J()�;C���� ��"�8J��"U�sD�i�!�5T>��<�.����C����)�1N((K��p���9'�����o�I��I�<#!��{�*�)�E(���Y�Fd��v}�����&o���5�?Z��bO?�?
1�����
�#fl��������2��~�
�~���wT^����Y��(�W�������=*C��`�����kUy���~���Y���s�4��Ha��{���a�����TA;ovn�	
���i�\s"��8.�:qK�V�������y��4m��������&��{�Zk o����TVqj��,��Jl���d��*bL����D���N�gz96�~���-O����9m"��w�D����<D�������W��g���e���[L�={������D���E��"���w������eF����#���(�N�ay�2gF���=I�BUL5M]�����"@)�m�rS��e�<�����fh9~�<�,����!���4&�����~���4Qj�?�e��=�jL�������7�B����wy���&�@!��o�-UR�w��Y�~y�k@�r�8F��aG�0�.ci�^�{ev�kXd�wP�cQ<�,(P�G�*C{ZW�{����%�g�k�;o�n��,��SVYo

��%|�:\.{e��T���o��T�}�=	5G��������@�<���
��	Y�4��y8~F,���)FE���[�)��_�=o�����3�E���]zi�<Z<k��1v��=81@�}�^A>Y��|F��W��},-}T�N.�
�x6�2�|�����&o3�8�G�X��g������8��-��y��5|�K���#�o�����v��P��5k]�{<��������/�����\�c��=�
/�y��1� �z���+���i�o��"4�YC�X��~���K�+c���(�
�����\s���G���_9_��M�vm����/�z�m�.�����I�:������K��Vi�������>wc$*����B���z��c��W��m������9u ���:P��<���(������*a�~^�7���c�����5,��,#��aA'���Sb)����E�G(8����s�x`�i��x2���&���m���.`���x�n��$�M�t_M���[$�!����B�:���E�R��>e�
��gY�i�(��q��N�4um��X�Va�_�5��Y�������h�E��P�pR�%��2�G���%��n������=��4i��u���h������@�2��*J�VX%7{5�BM�����d���O����1K)�����b�_�gA�������*�M�����~8�AD-���sJ���4^	��vZGA����C�����d�����������z�Q*[:[���<� ��y��M������nso��\����.���#�6�2���O���+�h�ev;�������V������vc�|��+��eL9N�"�3V�%��2�����e��]Wf��]o;���3��������Pt�[Sp��l�H�����'HWt��!^7�yYyR�sM�v��6duy���^�m�.���wu�gM��!����0}��M�9�pV;����JU��e�6�~����<�[}��#�z�)d8�i���������{�����	Q�*����BG�=z�`�Q4�({��0vlRU�W�*�`�eTyV����
��K�T^�����E���>f��6<����"q�.�(\(Qja��TV���9Z�n��#�lap���x���I���m?+�����_��i��m��]�"���FK���e�`���%�~/d�o���W>+�p��gb�4�5C�~R���(��������E����,j���_���Y��y�o��l}��_i$�a����o�f}�"�9�������[%�id��q��Ex+��i3E�;���L
��-�i-/�!���vc�$�����v���?5��2�����xl;������i>a���z&�����H��~-*��]�m��M����;�5=��
��&�\?���7��~���0�"������|s���W���yffZn�v�{�u���!���_�������'�%�J�s�-�$_Ze��%��k�H(=���X�!���������e_XB�M#�]�~������?�<�J��T��$t/���ei�9��������r����(��"?�6�L��`<HD��4�|���aFZ��F����qW�>��j��
��S��m-�(a	�g!TQ���(m����_��=�c)��
sn��,������]���>�W��a����Y�1�
rl�n�����o��_U��<��f/��s	����>N���F���w�%�D��4�����'x��0n\�������S�>~�%��l�<�ng�~z�{�-�:�2���o�D?��&y��l?��y�
�����/~1���bp����n��C���5Y���h��"����4�A��z'�� �Z���7���N�������K,��0�����?)����2���R8O�{Y�M�9��s�G��_-�@������I�5���O����A:6�������myR?`�u��pE�����o���g�m����#?<�������u�]��:�'�~������E������{��D��y~��F���(�y�����c�	E�H~��^r�/�{<����>pMTk��{�w�����eX�'��,��G�x�����;H����^����z��o���E0B9�����z�y������_�s���e/R�[
	%i��~�/x�'m<Q+a\Q��kc$�+dC[�
�k�,��d���S�261F)�yFk���~_��������g���Q&!�v�}���",U��#�	��C9�sy1�����K-�������
E�/��q���������D?��&y����
�z>���k����.[YQ:����6_{N�O�|Fz\�s�������������AZ}C��vY���l���;w�uW�����K��&��6�#
�t�M�<KYZz��~���pn�s�	y���&:���wLt������3�����6X��|��>L�d����m����'��^�I��s���q��7<i6����l�A�U�S���\}u���K�\�+P����B���.�*I�r�M6)�s����]��V�E����m��{���IM�H���.��^g��,V[}�a������o�Q�i����^�Byyv�g��tK���x%�6��
+�I�����*u|��a���A�V���)�G�e�#��>f���$F&1%)�b�R������/Yuz�(r�4��|c����/��r,Y���L�@�6��sf���w<09Mr�GV��:�pn�
�\y��sK`�K�>P�^���/��;o^����BBwb���N���<��2�t~��<��#�W���9<��~�y��q/�_��7y��m��+�����3��M�&�na������#�����R��������K���b���}{��<��������m���Q6�^����<�z�J���6v�cO86����M�D�"�Zk�9�Z���i��K)c��i
��^4���4��6,KM�9_|����������M�7�~hq�4����6X���v�s�}�m<�0F�Zv����<I����@�!���~�"}P�3M�]�W������#�a�OP�V�����-����/���'ak��r���(L_�pzJ���5]�G:��q1B��fMP�����h�j4ts/�}�n\OX$�5g�A���m�*Q������e�JZp!�������_���m��N�&����W�J��������ud��������0�������y��Wvn��"�&7Ej�N���t���������O���>4;���%r�����^�k���H�.1�MQ������&:^(a��@��s��(M��0T�U�\3,?��n�����n]G��NTc�:��-��s�����3��x�A�*a�������;������{v~�'lu.�����av�E��v���o6����e��mJ�?+�l�@IDAT�(S�%�vL����3��.�/z�7��G�����O<q��F���	}B��ma���=R��-���F�S�[�/cD?�fL{n$�Yc�\0�7g~���K�qx���N���	��D%�v����q��C[^�*�C��9.���/��q�x�u.���{:l!bD����oW�%l9�GD��T�N��`v�DN�Z'3�Y����{�L���6��$
����7�,���v�e����xA�SM<Zy;}����6X��|�j�5���~�H�wKYF�m���oZ'�C�5�E�I�?w�W�����za-e7����k��j����dC�RV��'�l���q�Q�"��BH0��7^U��	z�0���'-��C��4~�������a���m/y�=s��Gwn#��Ta�A���p�m�M�(���%C���w��d���E�����n�������s�UWu�Hr��@!w�wh�T�IC��
�����??�l<���GI;�U�w�iCT��]s
c�k$�����_;���tu�Y�]h�]��$�����>\xa�`���.&v?^����W�%�B!������f_��e�h�
���R�N?>��t?D��n����N�����{���
�����&7J�)C�����2`����FEV�����`���2G��6��d�Gi���Ca�z�#�l+��]<?�)c~�����G�����k?��P/��yC���qfI�,j�y�J�����1C���KE����lfQ�w�~Q8�	�^�(��
�@�D_���0q���!��g�~�o��CH��u�/��-����FGyd����`1�C�`�y#(;/%k%"0�����"P��_� �"��T�6y�:���w�c\����+2�9/`k�|`x��^K���|�>����AQ;B�kH�t�*/A����u�7�(%�v�G=��]/ 1��N�v���g���-��5�N;��n�b�^�r�\v�Jt2����h������6X��|��W�_o��5���|-�)�1g����~Q�<����;�����Z1���"�w'^��p�*���$
JBq�o��?������
����|oHY���{'<)�����%���1c:� �<HB�]'�&T�:JQ�/��b2��>-�����n���w�V��}����F���:+U&�=��	�����PV�z�������f�B�����m�������v�j��w/UaI��x��rM�x�������������}���0J��i��6��q�s/���/�aB�q�I�?
����b�d��6KAr�<�	��y�~�z����RE��!��-!4�F����?�B�$���s��:�p���N�~������$��O���C'�K�tA��|g<���n�9����
�!���\�oE:uE@9V�����R�N0��M=��S:����X`�n��3��n8~o�����eN�.���V\@�����X^��5K�w��Y[�i�?�y����W�����@�0�:x#�c��^/z|����}��g���(E������,c^X��8�S�������"s��ea��B�X`�|��F����yy���E�m����0�2�9�\	�z���g�����}��ko�:/�/Je�%1|F3(?!������>6�~�f�)�7����T�\Z���m��>2��%�C�g|����1^�C�2 o�)�~m�}������u;����%t�k��`����g���b��{3��#��
������;zy�6y�:���w�c\����+2�9/`k�|�x� 	9����)@�X���>��c���6v0^���������_�c2�7�u�H�>(�-���u�m27��}�'��C����S��ce���/����
�����6����h���@h�
����=�ea�����sM�3}����D:x���Sr��6�4�B��W�<�i�<G����GaX�l!����WD���/��A�����G�w;��|((C�Fx<�nVI��e��7OP���-J�{�%a1��@!���N=u�{�um���R�3N?}"+E�$u����x����@�h	����PI������W���^���v51n�M�=���PI����&������6�[L���o��;�0�&J�6���������v����������2�z�����P�
�����b�i:���O�h����3�4SGY�=�a8P���o}+��	��=���je����n�P����%�
���/E�D}��%��?c6x�����%����
�����Diz3����JR�:\�.������a�m�����w:Ei�
��`���y���O.O��?~Xj�/x|�����|�M���G��D��7v��6J�y;� L��+_����a������b�w���h�cE���i)�.��^�h��5��s�������4`�H?�JR5V)�UC��6��D?l��;��/��n�Q:��6��������P�����Y1�W�WE e�=i���D0�scPj������zo����bV���X���q^ D������*\X)���s1Zn�������D�j��*�eb�m�*d�������}?x�9����=�%�8#��>[����s��v��mh��k�*I���x�,�V�.���`�I��Y^�~��>��;�����y!1������)O���sG�_pEi�~�/���5z-��O���K�--��-�ljYi6/x�lo�9������V_�Zj���*$8�g[D<<B�^mJ�g��}W�e��l>6����������+^-a8
��l]�od�t<�T�����w��{=R�UD�
�
�l9v�g�c�Z<�e�����l"��C>x"e�����%���b��X�����ms�:e�MgC���-����j	Q�c�l�����?U�����?K����������m�4���Mha}�o[o���7��0�@����%�����s�is1
9@<C��>GB���(b�}�Y�<���FI���&z0���e�n!Qo�K���|�A�������%%
��Xc����0���w&/�T6���(��p��q���P�:i>x�}Y�^Y�����x��)�����|��S�o���(�|�x�y�������=��=�^m;+f���6����y�����X��5F.�-�Lt\.Zo��#!�����)��V�P�i��e�2O�/��C��<cJ�-��#�OgI�e�}�� �G\���%a�p�I�m��G���|�^�h��8��7Vn��|��d���D���H�M!�+3v��;���6]�{jz�/�;��6���I���m�,���b�������}�+u!}@���\�m��kz�
�����X4�-����v�5�
�x�h������,�T�C��#_x[<@l�4
�H�;z��'Mg�V��q����-��>Se�����Uu\�|���y�������,����/`��X��u�����������6b��L�6^R���Lb�*/O��ce�p��^zn�l�:K�f���������8a������GY��;L����-������}�.Yc�����5��_���1��-Q���"����Z��+��-��G�`����c�lY#���'�[���:�C���z_�y|��>���Sx/%{���h��y��:��mc��i�h���e�(P���|�h�<�e���wcJ��u��4����oi�5��6�l�f��Uo%/���i)OLh����W�$������z�1��*���y��a������{>V"��[����K��:�k�e�q��&h���w)�nX4N9��04\D���hz,����Q�h"h������]�������^��a�C������B��(�!|&�{A�d#�}{��L!B��&����G�����R�2�����<m�pz����t�Y5�^�b�!l.�@����:����Nx�	���Q�@�=�z!������=��,
��|��/E�����a��:����$�
��������e���G�PF�������yu�r����_qE���$4��=K�k��$b1����z���g�6������oNa�*�L��]b>��Y�c������[nI��R�m���|��_/S\f�2�"3�n0�|Q�Rp$l!�����+�z#���m��h�
�U�7�1C��^�~I�@4<A�K	o]Vo���`��_��!�6y������*;.���u�����@��_m�<���73���Hx��@>�^y	�)��&�������^��{�|"�1�G0�n%��E���-N���+��5�������E$����O�!u�h���0j�
����?��m�mc�0���sm�3k�E>���5oNh����'����12����#�n�TG�pG�pF
y��Q��
��S���p��8
�Jl�h��pG���j��3i�<EiCEz���#�8��$���+J=��$�\�eG�pG�p�
�gp�w{�����8��#�8��#�8��#�8��hB����k��8��#�8��#��J�|�����k2��@��uG�pG�pG�pG�\Q��^�#�8��#�8��#C`I��P���o�S?:��#�8��#�8��#�8��#�����-��E8��#�8��#��b�~�����^J��/��7����W���&���N��\P��#�8��#�����3}4Y|����'L��s>��#�8��#��N&�~�?l��}S�6���G�pG�pG�pG�pG�pG�p�~E`��~z�_[���pG�pG�pG�pG�pG�pG�pC���A�;��#�8��#�8��#�8��#�8��#�8��#�8�@�"���~�2^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8���+J���x�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��+�(��/��rG�pG�pG�pG�pG�pG�p��pEic�z���#�8��#�8��#�8��#�8��#�8��#�8��#�����_����pG�pG�pG�pG�pG�pG�pC���A�;��#�8��#�8��#�8��#�8��#�8��#�8�@�"���~�2^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8���+J���x�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��+�(��/��rG�pG�pG�pG�pG�pG�p��pEic�z���#�8��#�8��#�8��#�8��#�8��#�8��#�����_�������'>��d�����o�6��{�d��1����}G�|�hT��/�r�)���M&���2}�AZ����-�������M�|�c�=_��pG�p��a�������N�wG�0�t������� 0n��'�z*���k��ydP��x=O<������J�.�����k���.�8���;�{��/9��?�����^����O?�s>��`!��Kt��/�4X����F����t���#�0��;o����&\xa�����:�l
�8�����N�>J�����~0>p��m��1v��;��+��������K�~��ai��#P'�=����v���uf��y������+��������<����ko��<��3�K����6vrG��(����w��o��L6�di�f�i�d�UW��Z�_%<
�.����D��r�/�y�5�\3�b�):���`��8M;��e����t�����U��U�s�(P�'qZE`���O�(���j�d���n��~-l���u��x��|����_?i���m�M@WZi����8�s�9�������cW���85"�x�c�h[���cz:~������&���o2��3w� `��I[G�(����e�����#0�����O�`�u��r����N���t�����o�?���a/����{��^���o'�o������p&
�������7%r��7�|����Q�@��L��1�����;.�S�p89#������*���w~������M�����Q{Di��x�Q��~�l��������Q��>�����4��kY������w�aX���u������;�v�����{��*��������������|G`pE���E:U�k�=��rJ���W^���a4�B����$=L�)����#�����/�x�������g����G��4(2^LH�[�F�>��d���K���|�����`4���S�@�w��2��S�O���4���Q;l���.�4�S�nN����'��Z����m�Rk;���O�;�����5��*C
K�z��6K���M�j����-D�W�6����4��+J���KpjG���t)�o�i�B����N�srG�>^tC��*��Yk�A����G���8�u�_a�ae�x���~�G�pG�����[�������q%i�0?q�Q���Q:j>���#�8��#�8��#�8��#����2K�������[����8��#�8M!@�]��^|QO��8�@a���0T�$�����d��6J��s�d	�����M��?��S�+�&�o�I��/|!���]�������x����/��������{��~����}6�������,���Bl�X(+�g��4s�s�+/���$�M�0!�#�����n���-7�����|���5�\3YL�e}���M���/�'�x"����&�������Gk��N��/})�b�)��K�������e���J�|��W�;dO��z��{��/~1Yi����^8����<����=w����R*�<�RK-�f��_�*��>��LYd�SB2�����<��6���_R�^w�����8c��3���{����G+���M�\r�d���N���B��K��R{s�A����^;����x���vHY!lg�m���Q��W�s���f\������/�p���S���<�gw�+���K(B%��:���?���{.�m���)^:��@���{�=������cm�����A�]A���%�"u��U_�|]����&���&�n��7����3�����d����j�$V��c�l����'�c�t�O��'�=�X��k�)�9����(!g��g�~���\Y�����;!s�?������N�k�����@��?����4��B�g�}6����s�*���1c�b�1�=S ��
6�0����yU�)s'{�0/�JY�E,��sP,�:��c��_����3��|��o�=���{���E��s�#��1_�E��f����k'����6��$���[o�5�]�(��e���[,Ya��d�+�������?��1���������'���~�����K.��R���x\�O*�8��#<c\��_����= O?
����l�,��������&�exf� ����{��x�7N9�T)�Eu)\���
�7���������j����q����Zg���S�x_�a�N���O�|���n�[�GB��m��v����,���M��|���G��9d�`}���\e��e9�c
�K[�������sK�yA09������J_g_��k.%���C��&��NPV���#}h�_�k������Y��~�x��	�2����o���%���C�$k]K�~����od��22�C��Y���K?'���'��\���Xv
Su�-[^X�^~���2�����������"�����e�������|^����doV��|*k
��D���w �hx�nT�;��sz����wx�9�H��9���-���Y#��	���N�|��Zm��~5�k�}����[B��>.�����g�C�%7A����		���k��^�y|WV����k��En���m��?�if��{s^�)3��e�~��u=��P����L����T�k�,b�|�����w��]*�f��>5����2m�U�L�#���z���L6�3~���-���iqOLx��b��,&�s�;��<�0�����f�L�7��uV�����L��;�U������+��~�a2F�D7�������3�U�#�eR?��?�L6-������)���g��[}�k�|�#����0q�� �X�Y�P�ym����� �t�a�%g�qF���$OQ1��0����[n��������CIc���`!��}��%���}��7�y�]�"����u�c�=�����S�����$�sLf8�W������I��Rp��_*��IhYe:l�z���O'���:&|��O~����JiR�_cH���rD�v�UW�B0{]�Yl�7.�M������������ss�`�nQh�Z���|�(ru����-�L��5�����#}pA0��w\zN��[7��$��`����C���k{���f�!�Jk,XcD�����K.�8v��kU�j�}k��wO�&���P��'�p<��?No3��>��a����zk���=��e\�����5���K.��c��C;�����8 ������"i�9�D�����o�j4�UEv������7h(~w�uW}d��Y\���*�_�b���]��S�&�=d���BY�����9��Q�9�SO?=YM�l��a��(�c�)M_������'�M6�����d���K�G�)g��G���.:,��8IB.w����s��)��8��!���h�����7����0�7�s5���D��TG��:��'�%B
�Y��^��_�w��	c�����;�0#���F�����:�=�,Fw�I>��F��.�O���pn�Cc�|���QS�6$�B�R���/��sysJ�ky��}������=1����1� ���'G�0YW��1�6��3���?��:R�,�T����������:�PU�z�7�.���Eo��w���Ba�&��,�S= ��n����&c��
���=x���G��}����~TE&@Yu����ov\�zgx�/Dd+Y��:|<Y!OBx��/a���=��f�!�Aza�_�k�^�Z���<[�n��*c��C>Wds��1i��1�B�e���d����|�LP����2�L/�nc+d1zU�f����8Cx�n��w"��sz�����#��pBGl�q��=����g������;i��:�6�����tq^�Vd����������9G��VCn�.��Mf��������Xbk��e��Sg�[�a���<���,� ������3����2��;�w�����~,���(�a��9���l_��q8��:A����vk�����n��Z��3R�>-���@7]�7y����#������PH�D�],��!�.�m:�e+&�X��M�9
�+��:�����g�{n&s���)V�(-��&��I�b���
'��z8S��YlD$�������C}}������+KI���.�����,���s�I�5��hZ��c�D��m������%���������N�|��C��E@������"��j_��[D�k����>�O9yJR��!��x�)�������B���E��]�Sx��>w�*�J��z�y�
Qg-Y�#�+���3+�q.KI
&|c������j��u�(b��k��B:|o��SN=5YY�+uR���G��,%)�a�b|?-c��Q��#��{�������@���v�����5�����k[�"8F(0h�)I4=�o�/9}�����x\,�hz���E8���z]������d�`L_K����S��P����9�2�61�$��E1�Xt�N�����yS
�b�������$�01%)��)w�uWf{!�Y���d�����_V�����K��_R#<���|#�'���WB`�1�
nb���d\�n��c��^�M3_���<_?�������)����}�����L��� �mz�`��_�F�������HT���!M�c
Ug[�-_#kQ�$�{�6�r^�Y���Gk��y�����^U&P�Xu~�z�^��'�)���o�x��G�^��h#yT�&�,��]eV����[�������w��/�k%g���T�����U���D����k��4Iu��yu��A�������tP����w����p��!�ml�U$��9"EIz����9����1_7Yy�(�oYs5���E�;x���v���T��C����������nk�7��x��^������?�LT�G�e��R"L�}"\�y@H�B��H���:
��
o����c���Pp��VHxd��$
�D��{EH=�(�i�Q&o���aU��,a�>)�I����1�M�eUm#^Jxr��0(XP(��'E��^����:���'�L�Gf'����;��x� -"^!>�4G	s�,Nl��o��]C���?#C�G<l	
���-�������;�SIdK����>�q��)��a�x�xsfOk|�Ux�1�0'L+����W%�sYB�m�g���+�����Y}�5:'�����mV��k�3���a.y��d	�H��}�^���>�����[�q�%�����{)�����.!�UC���/E���cD{P!?������~��,��><G�#�_V��6��`��P���	$���Y�'�}0k��aQ�Z�m�J�"��>������2xH2h�gaW'uk�R����u��;kIi�X�X��0���m�P������E:�\�K����������Q��Yp�X�j9����8�y
���3GRi��
�B������V+��e^���X�Y,����h�(�E<��	�|���;���r���Yed]oz�*7v�{kl��!���d\���	��������Rs�
&�z�<������^~&���M��i�Kx��JX9�yL�Y����9p	�j
����b��D=�"�H&�<�_%r�]�		��/���u���e2�5�_��j�����m0T"D�Q��6\���p�J�������t�q:���/ot��%��i-�7�>�����(�m�Mt�D��6�LT�����/e��z��
!� ��W��W���	�;<�t
�<v�DXB����1��=VY�\��(��J2o��������x���N�c
Uw[�c$���(k�v������Zp2)t~�)t;�}U��V���Gk�6�����U�	T��O����'�����Z#7x8d
���t�x��WZRy	|#�?�X�w3r����e��R�}�2�U�t��X���6(����OIT+�����u|���w1�&l�5����.�(i���>��*��(�9^���u�	%x��CQ���^�SszX/�
��g���cb�C�0�t�c��Y�)����L��w�|�Q����$�.�2�9��^���WaB}�� | �����^{������":����r�J������wGq�����-���"s�l��D�D|P�d->�6�)�C�^P���RU��qU��;�}�*�{����Df��EDkS�2U��+����EI�_���c[������9���+JG��`�l��p��0N��U���*p��o��J��P ������t��Y^�py��o;��Rb"�K&j��Q�0]�#���/��d�<(��N�sd�^[&�p�9,�\+�|�)JI���lL��{+�CZ�.B6Z�A���%,a��G~#\o
��0g����b��N����#L��zG	\�����p�U�7���7��,B����-��u�g���?+��u,+u0��PX�0s\#��
%���/!9Uh�s(��E�%�*4���y�}����e��k�4,�\s�IY���g�s�O��|��������J��T�mj��
Q��:^�+d�B�����WBO�Nj�����0B�������_?l���T9�k	m��.�@�N�*K,�Q|�\r��HG��cF*���XS��s_
-XP[E�������q��3�vd��0'#l�����QG��&��%�x<�U1����[(���y�'�2X��*`4D^(���&��2u���Z�c�|���U������9/��6���X�m����>���^�u�w��%�1vj��O<��
�?Z��<E��FCPayu����q}2|/����
?E���p�/�w�YT{��f"���|E�F���F������������J�o}�4�U(�,!�#:������7����-?<2��}E�SX�@R1'!�iK����=`�1�=0�w�&���V
C5^)FI�<*�f��CDG�|�>[�X����~��|g�4E��������(�t��@�X��������GS�{�oK���E&P�Xe~����a�GqD��?�����Ty	~gac�!������X[���;�����6��S�3F<����( ��������8��������m���<
�+�H��R������3r5K������/A�{O[Ei��d���(?�E����(Qd���������'�A6����L��V�e�����^X�����u c/�#�n�d���YsX������bdl���s��e�x�g��m{X�*U�A���Z~��Y1d�yKU�b8�������0�{���z��7��\�%�^�iz��K��G`��|�+?��U���"�	��&�����>��N���R���Kl�p�%1��s����Y�4���^�G6-�JR��\:3�'��U��e�a�M	�E^Y���,pC%)�a���eI^(_p���`V�-��ef�$���"�zQ,(a�f�6�
��*I5=G�i��g������R��5����oVv���PI�s����
�a���<��0�bU`�},5�&����g�H{#�b�/����4(a�ggz=�h=5(��KC%)�~C�w�T�QS}����;�W~��h����2Xha)��M9������%���^�aeOd���
MS��D[��o�5e����������Z7����e-W��	���� ��=d�<%����>=,|�{��Gl�G�_�r���"dRR;��G���W<F����[O<���y`��.��vJ��JR�2�����:��9�L=���x_�CVQ�����~9�4�~Ss��x���N"JX�/
���g_g�
Q��
�PIJz�o;�2�[�by�"�e(��tu����q�>��;�����G�7f�$�����C
c������:����JR�{D1��.&l��a�*I�AJ�$�:�png��p-���\�9%�G]����c�.���{j�+��1�og��(FC��]W)��hKg��F�T������}��*��M��"�g��{���M��Y���z�2�:x���[�w+{�`]?��0��Qnk���i�<�K�1}�y}���Z��1�����)�LI�+��K!��J��r���C{,�����M�����2�����^������v��+���a�T�w���q7q��|���DS��^�VI�=�����Q	Yk�N1x��m�Hd�"��TEm,����(B�?�.�F��C%)y~Y�D������ �6��0�kM����#��(����YU�NjqD����66QrA
,��C�a=l2���$�Ea�m2�b�y�1�
D��e��.&[R��^�s�G�m����X�����J��H�\��Y��IV8�|�x�"��6+�&���d��X+��&V�Z���,�{
�U���*���C0���^%��
,,Ae��t�xY��O���r����O'�u�,�>�b��,B��EM��s��'��&�_h<���	��4����&�B�Z"i���m��=����Zm���e�A�G��,b|e�����Z< �
�2�V�"�v|��y��{��v���Z�5���.�5
G�-�����8��3����G�s%������8\d�}����U�Q#�Z�c�sP�z1�~��n����O���_y k�����%p$T��	#x�x���A�-��9�~$^RVx��E(k�<W���U���}2����T%�<~�cTd
�x�ap���zG����\�#�XZ{��w�X�HH�,�U<JQ8dQS}.oN��K����1�'D�����5p�kzlr�`����+�Fl�:�M|4m)f8��z9������3/�.CM��2��i�����G���}���Udu��U����Y4��r?�E��Y��&�,��]����1�n�A^{I��,��r%��5�E&�6[`Y"��*��D������&���o�i�:6�^����c����3��yI���C����O�*T�w
�G}�d���>�#bE������^��+�@.��$
s>Q?�h3�N��1o� 
�X%x�%b��������u3T���q5���z��u��b��vzE`�^���#�P�L��!�����f7������s��Xh����/c�y���[
�|����H&n��h����r���&���:Y���O�E��+LnLn�����2�>alUp��������1w���&W��!wU}>/o�)�W7zS����	�	��@<�E$�H�U	\����_{�^V�Y&����.�C!k��>�d�������s�h8���mBP�GM����~�%�=�PN�	��]�^��������'�����;���^�X�^z�>*{'o5\�}��#� ��^�E�b�"{�f�����^B�&��C�0�9�,����2��n�aO
AN������V.�<���@��Y���9�h]f�P��?�V��}L�����������P��&���B�=�C�[���MG{!<d=&m���SW��:W���;��M�W�9�A�-�s��U����{=������'e�;�kg��^�|����z�#�/�e���sysJ�u]{"�	�e�W��ptY������a��x�{F����i�h����X�z]k�X��������x%���6}���u��M�=�����5�z�2�)�3V������{v���7�G�O�xU���k�"e�4m�����@����<
�0�a�����"������M6���%c7^����N���n���Fu��\"]5��mjN���9�9�K�]�����^�!����|��p���{e-�g����l3?`���D�[���W,�I��A��Z;p�OQ�]���f�/W�A�m\��F���-X�{8EpEiQ�Hg�,n�g_��t�M�'p�4���e����>G�'Y�M����\�f�f�nD�w��%OP*rm���_a�_��YG+d�Z%�����A����L�q�q��blQS���:r�
���.b�=/�U��	�c�a���T%�r�%J�UT���C��%��?V��)�M��\�����)���:�N����H[.Bm��^>���:�����������8���1K����������la|�F��y�h������q-�=�^�:���i�kX���@����>�GxL���
'BM�GxJ���,��c�\v�yKK��C>���q�N2�����������9(|�������g�������,�"T��g�lYe�^n����j�KK(�,��_V�������zS���@��o�h��E��%���G���}6/�����s�U�o�!��r�-����sysJ�"5]D��GE�����H�&["�H� ���v�H�����~��R�{��W�J�����5#������a��~��G�k��5����5��m~Y�u����������^�^��}�@\�����(�s
����.��1�j��gEz�we��F6r�Q��E�{U�.��R���[	{��������t��WN���&1
n����cu���"k�Xu~'��l��u�M�_I;T��,��n����p�e��l�����<H?��u}�~���u_?����h���*�����b��h��"VGJy�1M�����*���S!AV����L3M������MV;6
�f�+�K�N�@IDAT��.{���R�y�'vTf�{�
��m��� ��"� 7�����)J��.t�����|$Z�h�s�������a���"BM$"{��,��Y�U�x�&�j���q���Yyg��-{�*+�>;�TSu���!���r���B�O�}�N�'{��W�)&<F��s,��0�������_��O~�^�������k8wSg�;�s�}��w#;��w����_W�$���q�>i�)<������.�U����F�JR0��x)������I���?�.�t��_�L�����}e1�dO�������<)a�Oei�d�V����M���u�������jy1���3V���lSe��n�D�k�������������:!/O�9���J�e�����M���%��L�q�T�[S\([�4Ie�X�9�*�p�����N��_�3v���������f���n�oV���6*NU������m��\��6l�}�<��4�����a�$T��_�������7_�_$��Hv�a��c���On����R?k�b"�E/�sj�����g���
�l��
��`�;N�� �����|��k������K,��0�F��5���{�P�h��%�J��d�4}~(�,�<����^BF��'\a3�R�_���$g_N��9,k������7�j��7�����x�Y�k���~p�%���jM��2������`�m��Ae�q���EcL�U6�?E<���R"
���6b���7^4�����	����Z�������u�$-c�����|I��?�9@��"8�t�����HM�AE���`���7�Z.��q��/�
����\)�$�{y�����Qu<��Oj������[d�~�c�o�����u������f{��kl�������e�]6+�Q��2_��o�|���K
0���7���X#��@�
KD��Z��q��i]dy�*k���z]��O�LJk�*2�:y���[]��,��7����i�������Bc�X��[c�_��W�d����Hc�FP�aw��(aI���A	�o5.�[��uD�6VEv����,����e���;e��ed���J�v��E+i�/kh<����?���RU�_�U�N��<l3��cF3F�n�@W��Pi���F��`��he�P��h�4�#k�f��y�4}oA��F�-m��W���;�#��Yg�-MB��#?</y�{XH��\|C�;��K��	���������Y�y�)�j]�������R�
h}���WMHL�*������y�^�(J	3gC�|i��r�k���-v��h�s�^j�4?thJ�C��\r(��v�u���M�U�]�u<��c��E���g�^�E�;��c{�?m��P�������)��P�vS��3������$]W�������)�P�K��{��?��	���~�P����_����O�u�AE�kzK(fzQ���g��s�3w#;��j�TW��Zu<.�'����A��fY%��b���x�Bb�Gi/�E�q*��<<F14�"��,
}.���:���3�H��3k��w�=��m��FF�#$���XC����x�~�cRZ{�U-/feM��U���m��^�q���7��h{
�vyY��n#� ��6Ojh]��z�	�BO�(��
�Kz��Q�VB����B"'Y��� �4�9��w���Z�
���;,��d���S���h�J���5]x��#�:��Uy��WC��$4]Q�(��u���GG`�"�o����v}�^�� ��y��4Y�.^�m��V�u������!��p�������"������BO>�D�1B4jo�$�Zs�NY��+^�E���o�U���N���������h�d����p�EE��$�x�M:����G��\cW\1��2�
���e�����Y�����y���+�Aa���������t[-SO;2>f�9�yvg5]�p`�\Y�}�"b'Q^�������B�|��W'K/�����t!���e�3B�<Zl��;�����AE��E�U����byzYy �"�q����q:1����>'��*YC;�V�XG�,RN���H���� �W�e��������7�����&�
��M����O�y!���e�_/���3�b���N�T%�2�,��]���5Tm��{��������n�j�L�
����e~��(;�bd�{�����������0YcmS�e�_���A�Ul��^m���]�_��[�_"�)m(��j�&��5�D�����M��D�Q�������1�;�9�?dd���9������Ly�aQc���Sn����#������^��C��w���=�����2��\����=`���5��EX�yV:���j���*��a��6��<���������+JG�K�({������%pL��c�#����o7<G��0AO�0��9����_ye�v?�=���+���O�V���� ,���:+����PPt�X�eV\<�P��x��?�7������d_	e��|��B`
�R�����7�^����p%��_���"�;�\}���
�J����}Nz��=t��x�n����%���Oj7f�_���������c�w�A`Xu�kBY�F[
�)�w82Nf1��g��d�Y��	2/������N<��aJ���������3�x�A�U35�m�z�#������'�m�>Xl����V���	G�4��S��Da��?>����>
���!{T�5=�y_k���LYc<����^����7��A��B�������O9%�[4h�0�n�����Qu<��'��3+5��~��w�����
�������G���7��Fc��xy`(����t���,��Y��2(G��*t��A�w��0�����4���~���j��Wx��~�_�u���L�.����vOh~wS�&��|O�9U����?|D����5L�����b�\��'�����Jl�����s�����	[ah���(m����q�a�/YB���g��U�
�x�V�"���9=����_�|�lt{	���	j�U�w��o�g������g�9��0e���2�����;����������	�[�}Nx���L�*R���������V��Yt�8�t�M���'�L�e�����������hG��#������:5���J<[B�����R���*}�>��N�0��(�����V����n��biG�Zk��U�O.�`����C}��^y��Nr<T������X[�&� �"i���������o&�j��$���v�<�8��a�&>�������k�YGB��������N�p�!��(����w�Mn��H�s�=�)��pA���C��Y{u2�8aO��uT�.���C�����Q��(IN9���P�zjw:��>��i��G��0Z��s��Y��)$�maa�=�D(�Ke����/l���:��3�p���y��v��<�x���8�6��a	I��z�����
����?�z�%��(1��c-��q�"�g�Gh7x�!c@��(iX�~�x�Y�V�6)1/�8s#&,����	9�oT������}�\B���%���,c28Z�o���~���X>�s�}?=G(�+���wL��`M�F9K,�{����r���U�d��b�BJ���D(�|e����������-�N��&�.<�ko���@��7{[B��/������"pV�+�9b4���@�]��x1Fu��5��{9�c
�V[������~]{4�[/2��x���xX���{�b"OxN�,b��kR"����$���<�s
Sd����������F��||��F{-1�0�y����1��og���qP������S�T�?�H��te�E�S�s:�wQt*�D�i��
V���z�����.#qdmK^K�7�����-�l���8BX�nw�>�Mq������~t�9��8�3mUy�:����[4��F����*�5d��o���.��5������25�n�:�}������hC`���B��>�H"��2`X
bi�'��.��P����
:�J�^,����T/���tBd��y�	��"m�N�b"F�C8����'
G2���Z��zn+��*�G�E��*{��(c	�-��5d���J���1Xa����N;m�l#l	���;�|�0�:&��|�I�?���E�����o�W����T��i��zk=m�x�(J�@(<P���}���>�i���K%OLP�X�3��}���c�H!F���,�E���b�d��6K��U�h8&0���[�^;��#�/��`���J�L�%;���s��Z��}�������8�o��OYj����7e<$���r��'�,��.!��VL���qO�7y<H��( Y|@,�Ne<��(��.�l�X�� ��c���E���x�c��>-s�1�0�}&<�"���
�}��h��w�I����\�y	Io�:�u������t��83o0��_�����9����z4�&�u�A_����o@�B�L���0����a��>�O����0����Y�A��B���v��7�<����<`�W��u����q}2�=��"e\����^���}|�*cF=!����Y���0ckH�������>���d/��7F��������6�.^����c�{����_��)��k����F=�A�#��C�h�=����u�[�5T[m��w��<�u�Q7N:�"��g�:���6��*������O���m7�����u�Dea��6{��w����<^��5L��������>*���D����'d2G��c]��D���F��	��s���N��BqPC%�YGT�"���9��"����HD��e�������3�K�?�8�%=����e���:��"q�A��<�S���q�%��}L��������+�U��)a��b�5�{��i�a��>�M�T�A�W;�����"�i��;r�B�}Cx����G�����1�[��=���F'�%�!�m����Oc4�Yf��.'u������Q��{���']Muo����2
���U�����&M��IX
ke�E� C!#��������/t��6Q�%�xF��C�3��0	�<��^���"}Qiy�	Y�-����~f?4��*Z��K	A?B
�:-�0=+��.�4}��5��#{��3%��+�J����):[��{���1�������4����4���?v���
s�����Y�����[��~�s���;�6�0^d�E��1(�^�|'%����@�m'��c�9f���s���e���Z�n�>�l*���'��b�xG�o�V[u��������Y%)��U�B�k=�}�[�pTz�1la��X#$��(^���U���-��X�0O*��A���������Z�r����?\�c�p���,����9�%�w�^+z��c/K�����K����(;B�9/�?�2YL+i����{�`�{j�������x\G��{�A��/Z��o�@��o�E��B�7�IY3������r��0����
x���~�s��� ��+��r8|�A��A#�����:��U�"��Xq��:�Pm���������G��T�	��3V���P��u���R�,�(
�\K���E^~�>S���X[gy�=�>G6����fX1�]bFK���	"���h]�/�ZV��,\c���-��|'������z"3�����#X��b>?������c:�:�S�w��y/+oR�m���u%�p����F�'���ta�������n��
R��:�B-j��v�/(�����4�.o�C?���/A$3��A7��>�y��2;�8���+P�'qF�������Sq�e�I��������vQ���+�m.���s0d�&X������zHx�(�B�����R�>z��c���z$��r�.�<.�F1���v���`��:�E�����<<C���9�X�mo�����
+
Q,<c��t3.�=��E��,U}�2����r����Ob��4|�^S����~���|�����1�$��.��)�D�s�D����BuVa^���>���RK,���R�(�J4G��V��b�;�s��%J#+t���>E��@1�������M�y�F��im>6�1F2V>;��\C���;��*�1��l����=�9/sRW[�t�,&��EH�Y!���K/�����l�e�_�w�7��,&}�H�o���M�^B��������/���}s��z��|�z���q|��}-�O��(������Nht��IB��e�Z*�C�:��B:����JE#�����lZ�g�~������\�9�<l(�*!��k�>{H8m��1��\'�D����js��m,�'��e�%�=�����
W^qE�'���`���q�8}d���~�a	�~T��dSu<��'��=u�W�-c��>j�*���]��+_	�������x*���5[���>��}�������E@����9d~�S<W���!^
���>G]��B��$�<Fc����b������������.m�^�<
��5�A�<h7�~8���"E���}-��m��k�:���[��~�n�-��e����~{�c�z����Z{X��l�1�cV�����]�"��g�:��~���g�5�������B��X�s��C�@�:��>����..����6yk�,�2���W��c�tKK;bMB40bD�+"U!����K7�ls��x+�f��~����"�I��cN����7���_���k�C��g�qF4�:��mys���)�(�,@�����;/5��'�1��r9��b��h�m�Z���t���~	�[�W�A�W�G7������s��0l�w��i���3r%�-c�����`cp�2�|�(�~�X}�w�c��������d��0co�Z��,�����OL��~�=f3�#� �Q������+Kx����?�|���A�l�u�C���d�`��g?K���^T��@�9B �m��Z��<���C������k����ub��Y$�4DP��3�����U���W�#����_L���K]_��?��p"��#}d�eD��u�V%<2>"a4 ���E
i��K-��e��G�������b;�|s������,z���eA_C��Q�*���J�
U���Z�N���|x��o�?vqW%�:����271&��PK�f^9<������?��y�3�a=;��(�/P�:�)c6�<i8-��'�0�F����hK7��A�GHK%�-��TM����5����a�12�O�
�����<��Tof�V��F�"<8����MR�~I������T��a~�� �W<�G<+�1���_��y���#����0����I�<��>G�4���������Y��9�	����Z�m�c
�D[o��*���`��L�.��������"�a_Su<GF�?�������{Y����):��U^/m��3�j�Qk�Ia��KT�*u���������������G�q���<�����iI1J�\s���f�%=��/�i�����H����;U8aL���G�x���_��� U��2u-�����D-��lYG��g��������E0��1��3��:���v;�c����?i#0�z?W�N��wD�>kQ4����G-��vZ���9��\H�r��L:I���2S�p��S������!@��W�3bq��L?)Q�����f��~�����R���y��r9yjG�;	1j8����'����b�^U�?�`�X/<�G�h�	4����8��@������uqG�*��(���U[�?�8���5����7�~��4��Eyz����JRBq���4�,${Z*���������/�J�J({'G��o���nBim�S�R��t	����x�&�P�c��e����8��#�8��#�8��#���o<����p��z��p\n�+�=Hw��^�C��^�qB�|]�B;
>V!n����f��@�|NBd)���#��?��q���U~����%��F���0������G�pG�pG�p����{�������8�`�s�>��Y�7Uh��9�B�O\B�{��'�e?P���(�.��B=��#�����A�z�(^�G��xS�^_u�U�p�amC%)dt���s�g��#�8��#�8��#�8��#0Zp����%�=&<�h���<����i��#��.���d�]wM��{�d��fJ�.����������3O?�
���b/S��������0���_��|t����#0B0V�Xaw�
<��.�P��~��L�)G�$/��B�������,+6���|�S�J�������W^I�0!�R<I_~����{rG�p�#�2������#0X�vR��/��7X���:��#0	!0��3��a��;��������8��#�8��#�8��#�8��#�8��#�8��#��+����������^��#�8��#�8��#�8��#�8��#�8��#�8��#��(mZ��pG�pG�pG�pG�pG�pG�p�W�����z9��#�8��#�8��#�8��#�8��#�8��#�8�@c���1h=cG�pG�pG�pG�pG�pG�pG��W\Q��_���8��#�8��#�8��#�8��#�8��#�8��#�8�!�������G�pG�pG�pG�pG�pG�pG�_pEi�~��#�8��#�8��#�8��#�8��#�8��#�8��#�4��+J��3vG�pG�pG�pG�pG�pG�p�~E����e�^��#�8��#�8��#�8��#�8��#�8��#�8��#��(mZ��pG�pG�pG�pG�pG�pG�p�W�����z9��#�8��#�8��#�8��#�8��#�8��#�8�@c���1h=cG�pG�pG�pG�pG�pG�pG��W\Q��_���8��#�8��#�8��#�8��#�8��#�8��#�8�!�������G�pG�pG�pG�pG�pG�pG�_pEi�~��#�8��#�8��#�8��#�8��#�8��#�8��#�4��+J��3vG�pG�pG�pG�pG�pG�p�~E����e�^��#�����K-�L7�t-�V��O|��b�-V-�I�����:��'��~������#P�A�K��'w�SN9��g�,�m��?>��"H	�F��.��(�� !���������RP)����w����f���s�}��>�{g������f���v�����?��F���B@! �����$���������;�����<�,�����H���y'm�'�h���d�V���g�{����LQ��N�_qE��K/%C�m�����r�����}6���k�G{����'�`���nH^z������J�1�������
�g�z���1j�=�h�I%�8��ckm��K.�_���l�`K/����#�k��L3������^��������M��a<P;7�l�~����9�Y��^! ��B@R��-���� �|E�3�c�������7�0]����	�L��{��T���$SO3����x,�i����|��4OK����c����m�����n�m�3��2J����6�x��n���i=)x��'N�Zj�����DIz��a�/~����+mX4�!������{�{A�Q��79�c�q�i9����6�������j�6�N�>�|��n����u4�R��5
��M6��v���^K��H�z�����:A��:j��.�����y����UR?��B@! ������*�R�
v��a�d�%�L_��;�H�9������+{E��Q ���o~�lmj�_w�?��#���bE����d�z�%���;RV��V]���k�=��=^��>����2����~�iAo�W��?'�rJ?�W_}������7�|�p.� �1��E[,�i������f�nZ���;��O���+�f�����F�c������;�Q'�G��SO;-��g?K!8��#�{�����w���;f��7���G�]'d~����1��5%u$�$�B@! ��#7R����?���^f���D���AQ�}�YBQ	A�I'��L:���I'�oUz>�����Y����(��NU��X��Y}���J�����'}�Q�k�����7���;��w�	���W^������g2�?=��5E�B.���?���Z��i4�#<��s'��6[�`�f(NX���O����l�`[/[������j9��o�0���/������E�p'cB�����>������L������[����Nf�y��1��g�I�?�����!��B@! ������#�w����)&�|�w��z���K��s�d�)����e�]�3N?=����]��!0� ��v�%�
z��%�o�yCU��D���/�\J��*T����`�{	��������u��_|Q;�I2���N~���5xX��:���hF�"! ��B@! ��#�
�	GxX�z�-��/�<������k����e��N��������x����B@t����Q�7�x�6�o��g���A��k4_�B@! ��B@���Gi{�U����/�,h��)��"a���<����hi�H����w��g�=�o���Yg�5��_�������w�5�53���G?J�go��������J�����U��w�ly����{��6K�x��Z�1\%�=�t���_z�����_�������?Of�U���}����$s�5W���&J~��g�?����N>�}� ����U�`�	�Z(!��N�<m�t�=�$�Z�<��NU��'���,�����	{[�y���R��\��b����z����fw�Q�����_#�!k��c��=r�x����?��'�{d����������?M���?%���Jr��'���K,����������%O��q��7��*�G�� ����B�~�g�����X3�,�$C�I�=em���=���j�d��VK����O<���q��>����|��d>�
����?&x�:��p��W��6��e���}��{�����$��:�$����c�����>��j������o��<����y��Fm�d���N��~�t�������7�Ln�������{��?��8��q����'���>�p�Z�[�f��F���C�*�����*�<8�E �!{^{��	!��h~�/����|��v���?1�|<�bZ��F�y��p����WX!���0���[��G�C����>O�����&�<�p�W�+����1�S�&�^�\K�K��o���Q3�v�}i���-��l{a�������,�L����c�|Z,��f�UWM�6��_z)m�/��BLRz����7��B���?����_��
�%���+������K���'�k��&����e�`����8�<����{����������<\H��\���~�\M>���	��8x�������?�����_��g-��S����e�Yd�E�f�1�_��y���6\W�������}�2^���y����G���o���c��(=o��&������M�gai��1�v��Vx�X~+��2���a�����������1b�R���"�k�S���y�����r�%�����[�*���:T�&%�"~�����z^����J�3>�(|t^[m?Eu�����yu�5! ��B@!P�Q�g��7s��T�g�c��������6�aE�r����0K,��:���������5��5�X#�S��l�n���I\��<Q��`�(dEp�G�d�����or�g���Jy�2�����n��9�W^��l1�=w�=9���kI����t���M����k��eO.2�B>������rz����q�P�g�7����~��#�K.�$�t��r�#|9�����,�p����J~��)���"&��|�-RET���:�X_��=E
�4�u��	�<�����l�����?�~�k�o L����vX��Zk��1��n�q�b����?�2�.��
�[L1�����*W(�`���p�^&���SNI�}�b[��o�$�]v�9U�z�L���RW[[����j�=G�"v4�o0�P����>N_��f�3P�7a�/��V!��d�L�\f�]����0����-UH�c}������=���['���z�3���LQ�
!0<�B�/����m�<1��f���)����)��&-�F��l��5�|�~w���F�+������*
b��7%8�U��s�=����s��g�{�v�H�7���F�y�)Q��0���m,j�Pr0� ��#���Q��\x���}sa|��I6d����kS.nkm9�[�����������4Aoa8���7e���8�����z�C�sy��\�w��E>�<�k�1z�x�Mln����R�O�G�����+�
Q�sWv.`o�}����������s��}�����<�#��lg���y����`�UD�O��N;�.�����������s����u?2G��n��
+!��x<��#Sc/������,(g��7�h8%T�}O���O��E�#}��Q�����v��MP��a�$7��g��9�W2������������l>��o��B�5�3�}+����>�	>r.SZ5C�������/��������{��y��Z����y8[��������Z�>�x`�q��	%��f��4i��5���r���z����9��i�IkR����v7>g3�#hY��b�m�)5�k�����]�=��-��B@!�=�����_���J� ��)<Yl�)II�"dS��<��c3O�����E�v��(R����{L���h;�����k�Y,����:S�Z����L�S�$�=x�
~���]��{�$]g�u�}�X_<��0/h��y��
7L�7�n���g�7�^��g��*�S�Z����A��3���o�w�����:��7Ee��E�9&<�S�R.��V��h�"���kxG_l�`W�f�S��O=�����������+:�r�i�JR�Y�<���l>g���R'b�&
�L�%��5���,xQ���aW^}u��z��[dC��X��w;��;B��~����A��Nx&��U�e�#%�V5o='�y�U��d`>/R��7c�m��]�F��#�:*A�_�$%���]J���h���n6�d��Z�>,R����7�7�oMY�G�����z��L�T�$���rH�.�.U�]�/����1�C�jEJR�����1>��3?h^�eJR��/x�����jw����>;<*J^]3P�=l��e��=���r���h��71��r���JR���DbA�����B�U�$%�<y_7���S���+|�H=��HN��~����=X���?���U(8Q��*U9G�;d�=���1�k,��Q'�O�<`^=uM! ��B@4�@{��FKQ�B%�7\t����?����R���B��p"0pBP��y� ��7X���6BQ�3�Y����^��!�(�����D������<��8"$�������U����&4��=$(e��c����,p�������1^oZ�W�/%_��I2t����l�uj7?W:��3��@{E1��+��X�W����|$�{�7,b�W�QP��1���0��A|��b,
1�.:GH��
B����Z~���l���
%w�~��g'3��p�������
/[�><�f�0��N;m�����phF�K��^yQ����{	=�X@=�B���B^_Bv����E#�2e!��������������Pn�Zh��LP�������7�9��"!t_��x:���G�K���L�I�b�dE��.]r�e�B���m16���	��J.�~�y6��=$�	��L<����G-AI�������,�3�4[���nx�``��w#\���#V������H�Z�������4d�����~y��{�d��(�'v�D�����&�3�L�MM��cI^xv���|<\6���]��'������m��p�����h7��>����������-���[���0��;�9"��^�3C��y,|�+���Ef��Dh���>-��u�
��2�;������X�x�I��
�Y������	�0����
�g2�%����2~��y
w���xW�h�e��v������{�0��*U��JmmXy^�����9���<Sl068��x��*?k�H2f9���88����~��?������fT�FE��!M^�>�r��7�!�+��F#8x���9&�j�-�e�W�����Y�P���&r�+G'�-*P���1xi��kU~�aZ��-�n����V�����z�������x�����f����]���:z���;�	�0���m�fL��%�:�m���>V5G�{�VYB�[D������HG�#��o��u���CY�c�����V�/��B@! ZG@��������m��
!2D�����	A�����k����6�m�|v���,�,B%����0��oV�"��P.���B��Ny!��Oe�|�1����e�����=X��������DE�AfE���B�3��c�=��G~x�7'��4�yX���TE~�0{��n��\@�>jY����#��I��H�n�]������HKh���,�m���h�X[�B.���n��R#�(��]�w����K2���(���?b^n���j�w��T���$�]$�'�1�"�!�P:1~]e{���:}�=3�^P���U�������"����������c���l���3F
���o�PdG��sZ�
V�� �D�i��)����6@���RBp6������X���rl����c{�w[��F|�h����G��67B<��x�A�����!�2��VO	!b$�����@��o1��=�	���g7r�M������0�N���s#����'V��"[�3��n6�P�^_�e)Y�5���>
E�+1�h/���m��#��*��"������e�a�Z��/�#�3>�
��u�n���Z����Z�}'���y+���g�t����4�b#1^���>��u�������f��?��z��	�
����>��������[�#O�5��M������x��cJ7�d�>���=�7K�����s'�vs��M(`!�	8�B�F���p/e��f(�[����������C8���c��5�SYj��&}?t�cp�W������@��F�\'�)FOa���f��*x�f��3G/l�d�x��x�'r�a�c9�8�r��D�����>�����d�i�l���V! ��B@!�����y��>�q�5@`���������J=&��X��cY%)�jPSN+e��~��c��k�N�/��G�Q,�Q���! 
�V<���n_0��/(A�JR��UT��	��]���P����<�"����
�F�+<��x����{(�n4���6���_o�H�Y�����#���X2���
8����������,�o��l_J'�1d(
���������r�z�z>���zE%)y����q�-x
��x�*��g����=&��e���\���'"mnJ�����(���\|���M7���,W�19�!����q�4�y�f����8�X��w���� ��FfTg����!�NQ��G�;f4��j�'�k���V9I��Hx;������m����������6�+�����[����X�����2��a�V�h���=v��1�''�F��~���.��e������������6�d����n�+�G4�����'KK�Gf���t�;����4���;��iJ(���L�����|\!�Yb�X�x�,�����]ju�D����m�a��s���c�N+�����8��<���4�h��e��<C��,<��;[68���Z������}�0��������X����C���9��h�8��F�Y%)�����b[�K��^�*���|���4�B�\�	c7H�Z��O�<��SG! ��B@!�R���^��"�����J/�E��&�,#�f�Z�<��<�8�gO����Z�g�a��@IDATs�P%tk�-����u��GK�B�9�g������Aa���5�%�D?A����y9���g����p���G�b��7p��O��h�� ���k�z]���!�v���y���SO>Y�E:�5�#���\�6���tQD������r���C�f��=xw��!���XU>�_�������3xJe��!u�}����,�=��#�H�t�	�9��He����v�j�i��aL���5*��:G�q�dc����L����y�DZ'��F�������������������uQ	x����J�Z�vB��
�x��/��>g������7,t���A	��=v��D���\y���?J�����7���q+�Xg��6��V5w��8'�b�����v�����K(/c�W���M��E�X4.��9z�zv�9�7+m������-[�#�����;�3��������m���}��h{CK������-1,>�@��?��<����]J����yu�����=����Y?�
z�������+D�t�������_B�l?���^O��B@! �@;(�n;���,�gE��,���!�{)z�����-�R��7���#�����^�W/�f�w�����?Q�x�R��X(HX��g{/.`�Fr��^���e�b�����K�-f{����H��=���4�}?���G����kE�im��v��O+���{�M�	7g���(:~f���G��1�M�������B<��jE�8^������k��4f�������f�U�O6����-q�'�ST:s�CVs�|������-�L34i�_��P2������q�f(K^��n�5�~�	����[�7��xf�������#t&4E�����yk��.0��-�;���ql�s�}{������)3�I�W-��/h���7��U��c��NQ'��u��n4�!L+!p�����r��8SU{�f���/�9^�f�SF����{U�]^����h�B�1l��F�t��oo[�XW"�'��������]zir�������n������v�5���XF�*���^�1}U��O=c��g!�D��<>GX�0>3���y����o\T�n]h�#�>����~�N&�}6}7wc���� W(��\��0�d�i�,{�B@! ���" Ei�Hu �J'-�"���#���{�h{��k�?���z��r���5RG,������/��L>�)H!�z���Is�	Z��@\_���6�x�to��G�@���2_��y�e�B"�
#�wB�xNfoR��w��.�~�kOO���Xg�z�����g�_���FKn�L\X�s�gY�����>�������n��W�OYex�"o]���$ X;=ZGqI:B���(�>����G%������N/V���cM;��
`x}�73&����R���DaN[��o�����������=w��x����^���=�Q�6C��"&�0�K������,�����,o�6�6���=���N��l&�.��916��?�����{%�y�V�^���p��(���������l�_|�E�R�7
�Hg�����4��^�
7�0��xD���?x���9s
�s�O������3����uc����d�Mow�<�;/*��B/��vC���qz�=�Lf5EN+����8�} ������E^'�������N����w^��xN��%���[�t��������	! ��B@�f���f�T�����9��'���CO��x��<�7%�j���L��)�o�����[_�E{����
������e�)���	���x.���zv�>������y�{=�����i�q�IO��j��[G�d��0[�A��e��j����q��cV����o�G�|�q-I��n-Q�'�m�i��g������O)���w����4V�=
!��WQT4����~��H�(���f^#�m�}���J�n��]v�99��Cs�S���((�_2��tl�l/���z�j�j�q���f�����o_%�b	��f�<����^��=c�1e�@���":P~���y�ql��������S��������,���-)I�w�|c����@��9\�"���^��f�W=G7[~����(+�%.���S%�K! ��B@D��u&^�yW���F�YA_�$����=
�6c�)�_���?<
�8mWX�����Vu~�1�$C��e���+�����{�-:���R� /���o���cX_���n �d����#��K�D��k.O;���W�a1����,#��(A�<5���������]�?�������
�^dED_F����%�e�	�}��}����:6�X�z^����~s�{����%�0|Kn�d{F�)WFn�O��/l�3��lc
�J:�1���o.}�8&����!�����2@i�]��n�>E(����o����]T��/H^Y�����2��m��uOX�����9�/�t���/��o��gt|�n��'��V��;�<���b������nK~����}��l/��G��l�lu�j����)'�\�8s.���F�J3������H9�������=�Hiu�C���f���z����E��5E�W�~�N�5���N����q�)*�1P�o���VzD���)V�_?�p��%�k'<w
�o\V�f�
4��[R���
^'�ee5s/�O����)���M�I�+�3��c����~�A���B@! �@HQ�hU=��Q\����"�0_y��F%��{���{�9yI��v�=)��>�����2�W�aJ��<��Qe�S��[�p_�1���,c{��oa�P��������L�&e�<0A�~�@)�[�}�"}`{XBo��f����r�K�Y��%!./���x��yT�����0{x��g�%7�_d���U��	���S�F�W��Z~�s�m�Aa���������	W]yer�!��s�TSM��DHG�0�Q����k��S
����X"M�?�������^� �}#���y;G%���,�����������E�?�|��
�a�m�M��p|%Sj�����/1~�b�-��v�1MSe{I3��e��V��f b��HG~x�{���=���z�)��"x�vH��(%��!�5�Q��h����!{YSdy�N(�y�;�G3E)|��:!�6FN+��?������)J=q/c�c��l��7�P^���\s��\X',},��I#����m��j�r�n�nU�I�+D~&[������~���o! ��B@!P
�[�-���{�G-��tQ��HI�`�%�~�U^��N�F���c���>�?��e����������a��|�����ik��)I����j�NEeT�_��g�~�v���gD�h9������������0q������/�?tPa�h�[�|5��M7�XKL�:���yD���O�@%/]��?��ql��b��O?}?���?\���[�kZ�U������;��(���6�s}���O<�D�������um�a��w
�f	���&���J	����R���y��)I���O3�7��J�AAu��W'��G�{��n�%�y������<�f�E|Z����j�*�?�z�?������5"��|?���j��=X`���n��w����Za�����Xz�e�]z�Bt���38�v��jU�}��0���.�U���Z��t���7�&OZ�������=�6��2b�
�Gy��<~�"#!O<�3�ie�*��F+��5i�\��e�>.t�i7�B@! ���" Ei��U�����g!���~W�~�Cx�2!z��.����o�z�����7�>Sey���
+����r:���K.���SA�HP@�-�a���J����(���o��w�=��]l�Gv��"oN�X��2��N^F��y�E�SC�]���.",�2!�S�q��S�7�;���3����{�~�i���8��E�������V��L�)z�"L������\8�B}�-�h��"�=��k!��(qZ���Gyd����|7��14��o��dw����.������xc�??���M���*��v����W\~y-9�;��S�w����O<�T?������{�4����w���+�4=^a�}�]��|nD��t�����_��@��V�M�C=���>���?�=�[D#��}~72�w��R���Jx��~!�c��#���N�������(��r;ql�O������F��A��N���\�F�2�m����Uavig�]w�
���7\}�"	��m�}a���I[d������z��O!<�@<�
f�T��?���8�]�u�jFcG�k7�N��}L���o�o�K��>�^od�����=�S����$m���}z�]w� ��`�G��h����kU����_��4���_�s�_������@��C�/�C"! ��B@!�
R���Z��\<��7A�d�O�����|�k�������SN��p<��c�����Ek]��.��p�TUB�W^{-9�_�r�����W�B��P����� h����k�`!~��'��=�z�a�R�9���v2~��5���#���w���=-�� ��ge]U������|������'�&�u��5�/��U�&T�d�I���/�=.�=]3G��E��/�_a�eQ����>��Ck����&�#����87�d����;z;��^{��D{����,l��v�yS�����i]�wZ�p�"�r{��V�6�<����_i���&�a3-�qhh$�_g��P�_c���(x�DQ�[RD���k�.�����������������g�U�[����IN��D�l8]�d�3��NQ���=y���2wg�|�B�b��`D�cm/�,�\P��+����v��vp���7�Ro�h���&$�S����01�l}���f!d/5^�?��V����i����\UsW^�E�.
��#s9^�����P�P��?��^�1}���i����Y�c���z��/6���6x�M7�3��^�r�I'�crD)�8D�v��k���JVYuU������vZ�������&�����w��mY����g�{�^K�~��<��XM����z�m�-B������y"�|���5(���^�N��Pv��2w�����[�k�l�����}�0���u�@�<��d����p@��g<f-�]#�=�kB@! ��B "���C��G`�]vI�7�G�� �+X��'����k!.(cm/2��.��Z��`�B�X7�O-a�	V�1\�i���
���.��#�hU�m��v�w�*(r��>9�j����	����%?p��oo3�*�`�1�L/�p{����}&����L���������oL�g�{.�z��W^I�5%L���qi/��j�	��1(��p�,�*�D9U�WVw�7t�����7]��keaL�*�-{�������� p���9�,Ai�E���_��ze���Pn�~���RK-�^���C�������Q���}l��,�e{�=`�;�;!��\#������s��m���������!�,��w���6&<����:���5u���F��a_;�6�7��	�t6\:B������T�X��:������d7��>t��W6xui�9&YO/��e���_��cJ�<�*.C7��_6��_��8���;�L�2OW�7;�&P������>����y���Q:������
�'i/����F��n��L�{H����(<�35�9z:�<d���k0F�����x�������Y���~��`�b��	5�}�8����i�G��Y���hE�^(���0���?�����k�������-�(�=f4��i������x��P���������?���@�6��l[/�B�5F>`���mw�Qx��gDu�2�����^8���b���B�g�-�������B�zS��8�7@)��g������#�oV�)�����X�t��~�.k;������{����z y��7� N?4����]W�n��6.���o��:s����O�g�<1<y��>\�M\�j�n��*���L���Ef��<�xf����X��������X��x���1�mc|�!fH%B@! ���" ��F��P:��+Yh��gBP�|QH�R�EM��9<����`��fK-�]0��/��_����)'���z�k(�X|N3���L�QU��@#R�:�o���Q���`����a��,g����K�G��x��<�,��<�
�X=�Z�A�/��4C[Z8U�H�&�n�4���6��UU'/���<��#J��M�m3xM5���)I����nE�5tAWT~�/�T�*IiW��
���s$�*�����������'�|����5���_x����Q�M0A�3�	����G�=����9;�2n!�^��y����o�d���6�U�"�Z��kP�t��k:Y�����`���{���H��$+�c<`��"b����C�{���.�oQt�Ck�!������m"*Io1�rE�B=�����H�MW���n����,j����nPDgb(�������yz���K��_�XU{���aSFEgx�9���v������Zk	8i�Ok ���T5w5S��fT���Y�?m�~�:���l3�~^�6�9E�����1<��5��(�0bHZ���|�e���F�aF+p�J�FN(���{D��{�r�LS��oJ�H�����c����On��T����h���)�i3��F�j�3P���o_��a���*I����+�u��4z���3ZX�wg��U5G7Z�*���	_�O�jP�/�W7��?�SD>���@����>�x�H����t.��B@!0r  Ei|�W�+i���Ox����1V+pd�!�{�&"ai��f������,p�3a��&@�O�3eK����EAV�����SO9%���(�B�l��~7j�
,���w_Z����F�Gx&���*������!V�Q���'y�fa�v6o\�R��S���i�9�ymD��y�����}e[�>��E������k%?��g�<�w�B.�~��������O7�q5Z&�����{��G'�ZHO�L�DP�������?�����)�=?�-��g�y�����Pp�i�OkB�!&��w����f�!��<)V1O���\hD>Y�b���J�x/����)��T>1�Xu��a��~�����s����2z2� ��$�6���&��������=�c�\v�W����&(e��c��������Y��)�����c�cMY��[��/�-��9/��~C��^�B�����*��3��<����b���{��!}��cl�����1d�{�lN�(c��b���cA�5�g���e62�w��P'��
��%K���#s�`�������"�����X����1�����#��>-���<����b���U�]�m������ec}5n=�u�v�A%��r,���te��^���nV�����5��|w/��7q#���K,���1m���Kx�8.�21�9�"&,jcC��nb�y����?g�0�����E�j�-�{������x�����h�/f��*�1k��-l?Fy�1\�;x������g�G�����5�����z��8�>��������IJ�
����S�z��z���9�Q��\�z��ON��Y/����?(���h?�/f�Q�N1m��1�����2�B@! ��h�Q�g�|�fri"�ls���~��G�xj�I��{g*��C�Ha?�f	��l���ZxW��xh6/,}�gak@)�-`��U!W���Jz�s	�K��'-�f��%��&0�G�x���x`A��y0"�i���E�u�D~�|/<u�7%!^�`����{���[Z6Ri�^%2��/�>�	R�������X�0�O<�x��"����/|#�����Ly�=8�4������H;�}�hG1�_6����~��AA�.��h��c|x���R�b�cx�������*��h��lL���0J��%�S�h>����x���p�v�QL+����-L;��O��O�sF7�
^�3�B����gr�C�}��������c�|�m����!�V[o�V��(���F�����|Z����V���ml������OY�n�;�Z�?G���q#���1�{�����<��/�)0���c���3
��&<5������0�c��5Jx%.`�sx���ku�l��GLf��7f���)�[��}��t����1�X���dM��Ec��we���o3P�0����@7��9��z��&%\��f�����	���M)���{fdI��3>��y��h?�z���
y�����-��B@!P=�]�'Ei�mB9
! :��#&"4���g=d
�I��!X��m**F`iu|F���P�����y��y'Y�r���;/�
��:
! ���+rQ�P�����(���B`�"P�(���z�^G�LQ��uW���B@! �]Q��������B@�'w\����~8��z���7J�'�2���<@�@Q9�Y���x�I�Qn���g��E����s�:�����:
! �!0���s���K�TG! ��B@! ��B@�F�{\��B@�&8�����,�#�� �������a��4K���mo��������������w]s���?.��{������3!��H!��F-c��w���D���B@! ��B@! ����P	)���!0�����C
�P���&����w}/��*7H.�y���[m���kVI�����rB�]�B ���G=����o��-�K�kB@! ��B@! ���E@���[�	! *A���?O�\c�d���O���r�d�	'L��~��W�;���<���	�d����J��2���k�;��3�s����f�)���v�����>J^~����GM.����������U�&}�1���������1����\�P������;}�Gm	!�y>�����W_M���/;_�JB@! ����Q�g��v���}S�nb����B@! ��B@! ��B@! �@�"0��~
���-K�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�B@! ��B@! ��B@! ��B�cHQ�1h��B@! ��B@! ��B@! ��������eT/! ��B@! ��B@! ��B@! :����V! ��B@! ��B@! ��B@!��HQ��_F�D�{��^����&����~���$s�5WQ���G�o���ve|���O����!��-+S�-�Pr�}�%o��Nr�5�$�M>y�^�g?�Yr�W�e
{��d���XY��s�6�h�DMT�c�U����L�2����B@!P�������g�q������uC!�H��_��:^-Iu(�A��
+�����{��3�=7���*#l��9"Hl�f�i���7�H.�����7�L&�l�V��g��j���g�M��<����e 
��@�6������kc��{��U�`�	��n�!y��W�7�z+a�e<���<\��I;���0� p���&SN9e������>�c�v��G&��7_Z��O>�ce)��!p�1�$�>�x���'��\a�$g���n��f-����/N������N9����){�
>�,���������}���_x(�(B`d��#�{W�n��(j��p}�m�M~��T�����'���z�"�-�9����v�%��V�Ymh����<�,���#ou%�q���F�W��7�w��J��'�t����k8��~T�ds�1+��w�C�����uJ�g�QGMv�e�d�m�������[n��2�(���'�8Yj����M�4����
[�V�|��*���h��g�}��<��-?��Q>����c����s��d��o���`3i�G<��j������1G�^t�y���7m���sfT#D�������j�^-�p���-�\���~7���ok��8q��>��z��y������=���O���@#k�����u{�@Q����{��O����W���z�YJs[{��?�AM�9�c�����
�wu�+D���>I~dB<_8
`5T�#�{�yw������]o����{�j��y�Ez��w��Aq����&Qy��y�F�6���v�����
[�)
�Zy~��Go���g�f����'�rJ?%�W_}�0v|��7	���I��ys�v�q��=t���'�w\��@��n�����?�����k��/����������_'/�%�H�$������������+W�
g! :����>�����<�������z�6^�E[,�i����~�A�����-GY��%�x��?���}�[��oK��������;�������a�d�%�L!���;�c:����i��-y�`�R��S���p����M�,��p���T�z����3���0��3�<�S��F[��;��w�	��W^ye�����{����+����{D�����`����jj��U]n��c_;�w�0a�L������~�f��{P?�B�y��<8��s7�|7��,���w�o�?�0��"t���m�����J&�d���/�H�����v��W�*���B�"����?��W��z	!����=�=� ��^Q��j�r �K|��f�--���h�B��u��Q�,���������&��{���Q�������^f��0��)J�j.u���.D�:����������$�[k��VJo����/f��(����@	����7�L�%~��%�/f���������|6�h�����O��z����7�x���W��!@����}�y.)��I+|G|.�=��C����C=��(e^�FY����B�8��3�DB@! �@wY������B�!"���58t"�@! yW}�U�;�W�
! ��0v�}����J�m�f(�E�#�~��'0O��]D��-�C�G<:�$��u��X����B@! ��hB�:���+~��B������>GG*#����:��.��r�s��L1�ix���~;y���*�C(��l��EY$�6"�������\�����L4�D�����L5�T�����OJ��������Y"��,�>����[n)�����D�<!�\2�w���;������a}����\�Q�S{����7��r���l����>�[-�7Xq�6���;�����/=�~G�/g�<��aT�7�Wx���~�����������^y����>���������'����|�������XS�5�)��/`!D�zX�%8C�~�I����y���{A��$��N�<��S���Z��f���7�<�=n��?�y�C���=�[�+����~�W�������2$=���}B �+�vF�����'�x"a/����>�����V[o]+�P����w��E'�G�����o����&���/a�R��,����O�,��
���������b�u��6�h�Zk��P.{\}f��o��fr��7�}.���������u�%�H>�>��^��m&m^Y���|�9�"3���~��4k�q�5�L��p�/�x����f��p7xa�1�H��w�d>��lo�bl��g�v�UWM�# ��H�Z??���M�<�w�u�����o��S���t��oF}��V7�h���g���b-"M:�d��V�l��9p<k��g����6T�nw�:La������\�k��W�"�.��#�
������z�����a������J�����D��G����C�%���Z�v2�8�$��a��6�Lh�={���.H�c�u�1�����~��	cj�TTwx����SN��M/���x?�����~�%���>�����v7^}�����������Zk%?��O��-����W��^z������'��Z�"Zl��_��c�{6�<���	��Z�C��hg�L;m2�x��E�?��������uy���Z�9?��/Z���I&������1g��2}_X����2"������o���{�Y�F������E�����s�;6�����tmQ�\��f���<�V�u�/|���G�G����j�K�<Sg�}�t��?���M�������lK�^��h��-�c�5V��Y<l��F�9��d���K�9m���
P��h$�������n*}�u���ot��-ZSV�F�l����W^9��~d�1�1]k{�7J�%��"�`�LQ?o4_�����!ptb�<U�N���W�m��"<t������ym]��}�	m
���o�<�p���8�c+|h|>��P�D��{9��;�����	r�2�o��������������V^e�t|��n�;��k��������k�#�30�b�Q���X�^1N3N1�����w�����EY�2w9�����BT��?��!��L��_�2���6~��g�M�n����~T�_��@^�].���������n�5��e�'o7��.���E�R���/���������d>Yje}K�K�'����h�2�����2��mM������}!����q���mm��_����/��vj������1�6��~y�{���a2(�YU�������/�,����|�����?��x�
	cT;�\H_�|
����0_���kxl�����X>��s!�,>���>�3�������y������J��i��_'���}�����]yW�p��<��5���f-g��{h=����,v���J��w�)�|�-��<b�.�Y���B(x�- ���&i��."�Fg�sN��KC�L�
7� w���:�$����y��chMclH:-c�*
�"���&�}��� 
��"`�i���������o�Rh�gm�e�:�	,���S�������"z{y��LH��1MY�#��6Y:M�Q�r��o������-�K/�$���u�v�����v��2��,}B�f1%���X�����2A.D8�ir�?��K��(��v}�y�%{��g������oHK�"�P����_�*������_}��0S�������/���tq�i�&;Z_�����$yO��b�	�Y&<f1��B�<������k�,2��~3�e�����E��o��n67�@��{_;�t�����OO�+Q���0c������WMpS4�x����}3i�Y������l���O��~m}�F���:1�Mo
�V��?���������p�{����KV2A�5C��q�%����pBaG;z����v�}
7�l���!��
��%����-��2w��D�k��>�y��K�L��
���k���&��x�7qb<_�����T��{��GV9��&���,�����{'�Y���I�2tM[���QHw��)?���;��}5z2N���D�����eq
������������*�v�m�Y�~��>���P���.E
���M�]�O��1�-f���|mF+?�3����������o�^<G���a��0"�����������%�X\CE�����z���j���|s�q��
���?>����W2�*�w;����}��B��Xbi3��k[��O^�yB�*�T�h��G�<���P!�9�{�f
E���x��y��iSTd���&��]�����a��Wwx�(3�������"/�1(f�M�Q�8���'�a�t�H)'hso���LX�Fz�_kx���=70{��H?���WD�v��0���'�o�i�VC������B}cAQ���y������Z�y���4[�*��;f\��9����o8������ql��<��q<,J����&�����-f���B���Xsmi������"~��
��uRY�~#J��Y�J��8��)o��}o����~�~��mm���2�����������[kC�W���)����v�zd)�;E:���ON5L�����#�������c��0����1�x|2��9���@IDAT���5k����3Pk�v�o���G��x�'��o)�����"-��4����3�(_{{}�11�7C���f���7�I� �x5F~e�A�2���?�_�Z���7�U�A��g���y�l�u����)�)���g���9��z#8����������8 y���k}��T�Fnq����N���K�m��>�Lr�=������@���<=cc�?F3g�#�<����3c}�t?E�N��^������b�#��`��}gd�P#�{"���(b�y�1�3�����1���G���HIJ>(1a��!�}���c�==2�S��XI5J����g�e��m���3*I��o
�"�������k�z��G������-K�;L�
�U�������A�Gah�3X�>f�E`�3u
%�0c������o�'
L[���$e�����uuc��������bS	�y�dB�u&�a��JR�G�^���d�M�k�'*s��fNp�mr��l>��))II�x�1�E�:�g�o�q���������(�����r��N]�r�Ehr�)���4[�n|k�!1��������V������Lp^�)�.�li
�zm����5)�{?�����:O��'6�K��i��-�[2����Y5Q�k���hn�:�a�����(�":����6�OM(E�~L�w����C��}�1���S���kH�R���#<�A=%)��5��f��xX5�0�`�7_Ec�J��/x�<a2���O8���o
�O*�E���\��7�`Y�a�#]|�l^������>����."��}��:o]������z���`s�Y�}c/����f��\Tw�/+�-���~S���V����b^U^�b��X�xT����bH���G�W��A�U�@?����{���uK.�����(������f�n��<������k�Khw���E����NK�q��@���s7��"%)�2�Nd�n�PD�dF~E�!s�[4��2��(��J�X^�����L�yc��\���ao��i���w�/�\�4ub}{�)M�J�V��}�z�i[
���Q*>i�'y��n��U�A���Qy���>�>�R4�{�F�U�2�<%)u��f<���S����5��2b~�c�I������S��di�K�SesU���N�y����G`x3���N#���wKuy�~��``�����
3�eq���?_td���6�.��0#������L����d�L9������0E���
E.��,�Df5�rY�P�/D�s&�0EDqQpY��x������&�P�(�0K:o@��9$�������x�x�	5K>Q�D�_���<."W�b%FX�-t�oB;���#��6!iQ~~��o���O��4
��7+*�G	�G�	}��"����]�D�G�bZ/���X{q�I�;��M�GHD�y��cf�ym���0����y��`�'��c��M2I�}�N��������
C���"���B^���u��QA5�����"�x���~�E)�U��c���AX!�+iv7�;7[�f,����xE��L@���q�6U�EEy,o�R�n.�����d��1��XB)���FK���N�a"�.�V����������������xC�|�4���pLy|��e�&�<��+G�#�]/��+��F)M >W$x�4��<em�9��yO��9�������d�	��GP���z��Sa�(�"m��H��o���^���w������0��#�A��<
�Jx)��#���E���b��x/��ksg� ��I����z]l������U�*�������p�(K������}��W���;E���qta,�����)�^m�'��_a������N/6��"�/c�;�D����q���eD�����h�����e�FYE�S�IS�����z��*�/�A�=B*�wb���&,������A�����K8��+�^��>�v���cVo������:��d����y���g�i-"���N_�Co���5
��Q#h3��x�k�3Q��/����6���X���d�:Ho���G���I�����k�DQ�����	-
���{��!h�^^�*����C�0�]Q��O�ul��HU��1o?���skWn���z�Y;��O��])���������!l��64��N��w��������D@�2�]�=5C����v�y�������)���6�4��?d-��L�$e�xj���o�
w����kO0���9�uw������]�������38����b�C��v�s)��I��q�;���X����n��<�������3�ujw}������>��<
�������uW7�u����=�4��/�9�������S��A�w�b�sc��B���c�!��oi;����SR�0�j��UQoD�mo�m99���yu2&��B�C��*����_"�A^�t�=���g�#��:2d�v�7��J��
�E��>��N�!�`l�u	����L��vc.���;v4+��ru|HQ:��Y�#����a8���.����'KN0���fb�c�-E�4����b"�A d�/��+.�	��1����#V�.�`�=���������hS�����AI���3|��=����	�
�	"!@tei	���	���M��\FE�A
8z����Rcu����x��Y�T�7n�,��tBi��+M����qt�g
�|�����c�f�l�7����=�b�>g������zg�&������
�po'[��"�'�s�#�>����W�*���(�v�#(>�/7� �&$Wm�	���	VX�:��bH�uU8��fc}!H��ovvD����~�.Q���a����e�ChLXi�q��o�>�+�A!����M���I�s�c�9��M�_;�:�SvNX �3�t��_1��F(��At3��Hw"<���\c��[OWvD���	��\;��e�����rA�D�G��:�����a��u:���&�g?j�k>GZ���(J=�V�b��%�E���v}S&A ~��3���/A �=�gm?�=<��0�_�/{� pt��4�1k�vZNT	(�l1?ods3�L�P���g.���7�!�+���(�!����d����	j�)!�*"�6/�U�!s���t�U|����U�z�C��QD�7!K4�
d�������Ek}�����;��n� �2��U���w���O��H(��>v^�����4z� p��!����m'���6��P�4��n�J(QP�g��= dd���B�;�7{�:!$�����6d����+��`�B �m�b����(���"�,�����y;�/�^�N���e�bB���=<m�G��{�!�ta1F�qn��[r�f�������.��P�$o�n�����P���m��:����v|����9$B������1�)��el��k�]m{��2�g�X36C��v�/6�q?}��5.[Y��_�ty��C�:�,i�������e<W'����Y�tn��_��*~ ��o�hJn����u�f�<�;������;����"�����+J1x���*��1�x�� �k���}����v~����y8{W(c��t���|��'i����������)/�/m�N����(�?_�4������&��>C�1�c��H�1��A�|=�o��� ���j����wmOh����a�9'�4�0������,�k�X{*�">h+s�������YEi7�b��b�hV������C�;����qD �
�KVIJ��l����8��<���x�D��,1X�gz����Z�/b�V)k�Y��5?"\�\�d&O�w��?��g�<B�����=�X��rbb�*I��@y��7z�T!'�������*I)
Af�6���^U�����`���3i]���{x%���=hG�����m%R��0���hVA����o���$�}�f�=i`l���.g92G%)�!�`?�����&����������/�>��3�����n����/�Vh���#���\ViI�x���f+��3��c�+��'��@��U}������S�R�n|k��������q��A"tog��y�rdc?(Y�����8��?�"Q�'�Ff��i�0y�S���g(����������q�!V��R+e!d�<G���
7L������)��cl��������2&����7�a(����yC�Mi�%'N��,�@����$�P�j
��>���S^�r��L����t�	2
���7md-{?W�r��6_!�����U{w;���%<=����w������.���K�/J�Y�!���*���);F\�k��l<�:#��w�i^���S��Aq�B�U�������t�O�V�>f3n/o���w�����R�5I7y%����v�%����"i���t�k���P���h_N[�A�@Pk��a�WfVI�{1��h{!:�nY>x�l�	�~���U��,�sL7��r�n�WQY(	0R���=��	�u/:��zyU�3d�����?�G�kJ�`�c�72��<�`
3���y4�l�����F��/�$%���07�G�z��=��AVI�u�.�c.�N�����RY=��W?�q�a�J��;�\cN�*I��>��G���S���-<d�f���}�����	�W��5��X_`�G�������������1��eN�����"K���M~���2Q[�������7�pc��:����Hv��i�>�,�N:���aw���2���u�� ��2�i��sq���2\to�" E���vi��`��������B��f�qcr#b3���������3�<����'�������"�j^��L<�.� �N���3�P�q����d���{����{�R1��p�+���[�W���������^f�!L8�����Y�lN;�[$z�^8�\{�?3���Hh/'�r��U����EK�O_���[�����!Y�u��G�����M������`��E�+|�Z�1*���������<C��6��>�w�y:E�A|��i�^��xd�U%�\k��"��\�K����1���w/�n|k���0v�o�o���Eu��u�Sq������������8���Y������i�=�)��Y����
B��f�[)A{�l���L�y�y�f	!�}���{����e)/��>�t�yd)z �H%�#�<#�����"�h0���V1C�l���B��m�>�����y�=���!��U���.��������~0"�;D����Mc!N����~����w~Ax��������3��t�y��	<�>�>x9�W�}�	>�������	�<-����,�:����a���e0����������0����X��h�GD�r��I�3'�x�<���Br���yi;y��5
�N���z���^����3
�=�4,��`F_�������O7������o��f;���:�g�������C��*������-���|��N��1��#����0����!��|E�[Dx:5cT�U������g3��v�(�x��:�?�5��aQ]��^?Pd��~'�����df�	�����:s��O����!��{Z��%�6�<��>�uk��b�x�C��~���z4���_����y(�[�*��!�'<�c?�����4c�nR���Q����!���&�,�����<�}��-�c\�m����$���;9v�^F'#���G��y^&Za[��5��X�k��s�G���?q�����K�b���"��(^����'\$8Ys���1%.a�������D
�!T��N.D����i���F�m6]���|�F����AE�Q��������o��VM�����y��~.�=��Zr�O?�(�	�<�1 ,<���O�%m�N��:!$+clH��Y"zH*,va$�����"z��<�����#���G
 ���A#$����
_��)�4���������A�Mj}*�=���E��8�����I^�v�ub����u.Z�P�n}�,6�)��	lB/3g{X�l�n��7�f���o$���D���L��J�l�V�� ��1X,���j�,���T6�#����-#���&OX�7��-�%
,Z�8�0fe�,d�+����H�g��<<���^"��\�����h���J�A�>!���(��|g�J�K��5�����/nP�W���XTc$�<%��QNA�A�>#�"z:\��a��1���sxH����ce�����~�*��������/�y�r���{a4�G5�6X2n���e�������4b�XK�.�nae������&��1�����<���<^�@�k��4��4���7/�G#�z�~�o�}2����n�:Q�f�,R��k�(�`Nr��yWud�.���'�Mh[39}kJ�F�9�`�So��yi�>��kc([;�
T+�����b���@��N��[�"m���q^?�gd���9���b�(�������m����Uq��/x}0�;7J���~�	|��&G�������5�W1�"l
@���)�/��L���*��]{�_'��C�>�z�X��x��7{���M��y�q�S�]���?�Byn���)��[5�=���\�����c��]�)J��t���`?�z����(me�����'�T��/#����%���``P��,^��|1=��(�k�:
F���P�P+�����Z�Q���C��!�������/JS�u6/"�E��(]��v�q^~y��$���N]0����1���j����!��?/�^���olh������E��{fQ�1��
:�H�w�T/��Pn#4?��j�c������z�o<f���N�_�x���g-\����������d�i�h��9_�	���_o��{�y�2A��!��[�gCnz����c�e��Q��5u@@���{&x���@Y���3
��%��{D/�N�`�5t��f��Z�Os�~,SV��jblj��I�y"����Z&���d/�H�SB���� ^NPx��O��b����=|���Eh��4���9��������P'��M�3x��a���<��2������wYZ��!�ZY{�g9v�O����b/�v
{6�km>>*������J�2�[!�|-������_#�N.3�m$�V���F�zM����I�������)�����-Ei����u������Q'���r�����g��y~a���(�����e ;iEQ��yz�O%!��ME�l������,k`S�N�g�X>\NO#.U���n^AU����*�O�<[E�,X����j�w*z�N�/��28�8�,�n���?�K�qG�~��1�[s�k��A.����3����E��*���������v\�{�x�
�G� �n��t��|0k�:7�1�H�}���{4�����\�}�������y�t���R�;��?��^����C�k"(���UA:�KI]�$%/�!)HJw�H\�R�p���4\:�?J(X(�������u���g�9s�Y��9g��������X{���D@1��,��
B�C�w#�W���+	��B�� /Hx�0��W�E����kIGk!>�x�!n����yWe1�s����-�� �*���������V�����cO�$Aa P8>.�A'J��m�4�G�
�	8PJRp�m0-��������B(I1�P"S�w��	1J��$�<�0�&���z��<��U���8l����c�}F�-���<����T��F��s�m4Oz��������[7�e��#G�VIj�L;���a��/!�z�$M�K��'��y�Fv_��4m�*7�)[��U�����
�n��y�X����Z�=~A�����>�VE����0��y�/��F����Q��j��4�J �tS������V�������9��nu1��������1P��)R�^v-���1���{(>������Y��l����f���q,Z7����Q����
/Y��^��mE�T/���a�r�xD�(�����N%)u��]	/�nd�F���<}w������n��������rIYg2&�IX��M�mQ�?�����o���;�Q� �d�n�FMW�XG�����G�STv�yW9k�����T��Q����4�h���%��h��*�i�g�>v�����(8�k)Y-��l�9��Z����&�J� �����h�m���Y[�d�X" �����7�P&�,lvb[B�!�k�O��������e�:2����o�E���rF-�����O�,�I\-���3��xo���E�L���VY%��o�ID�:<1�����V��xnc��-RN]i��$�w�xYcE��e"^1��<�Wh��3������#mQ����*���8�G2ka
L}���!���!��S���?'�m�����o����'�7U�^�ie�ccu��o�SE����o��J��l(�n��~���1�cl���p�
Yba�a��}	q�B�<��6�Y�ic�t�����yW��sB6�,c�p��1rq��3�O	#�,��x�(��e<L2l"MR�;
�iy���^��ch���9���r�6��o|$��?�<Q���k�G��������?I�M
3e�D��s�w�K]O�/d)
���1c��P�bY�!�Y"<k�"�G��X���^V�����+%z�������w���qZ� �ny�����Yz������/���6�F`���7sAV�iy��n����e�(� ��O;���"���[��(�������l���\���u�C.�R���������=8�|h���y����7vv[Y�|��%��
M��������C�&�L_�!����E3�P���JnVKO��@���M�����H�+��'�[��s�_�i1��GB�. [��{5���VvR~�wQ���WS�:�my�<2����U0`�d�3��'��w�)�6^V��V�������EV&��������c����� ����|�R5��H�2��=+#5Jv��kE�{�J���~��R�Q�A�(����?cORKv�j��������b�i�fb'���&���" 	�]a�u�@hp���I������0���O;-��yBc���������b%d���L��3��=�(i_:M��b��iVp9����]H(I�V��'��e�1hZQ�u��<^4]���z�
G����_��X��T%����h������%o��[�q����R��W���Q��)U��0��[�Pa�8���[y�o=����h�y��a�m�[�����J2n=��Sz�sd�n���9<T��"�������%�6�������CX��������^�w��B84,�zY�R��R�0�VQj�Mk�*��������	���v�i�;�~�����'��U$�7����@��.�l�o����lB��e���U�#hn���*�m�3}�<G�H��t�}���Iz��{�Y��))�:���a�k�7���!�����66K+;�:Q6��C��9Ut���� ��#�L��I��^��y��li�+H�@�w��
)��mj�S��mw6%���*I��A�Y%)d0�J�(:C,�
}��&?B�F�<}��6U�fiu�����6�o{������?��Md���l9�2b��Ee{+"��65���Vv�<?�c�?I��i���^G�m�<2o�eG�:�}�<Y������~����=P�v��$�^7�2I9�Wsq���4vt�����a
L��
���,,3��e<�L?}�Vr�s��	�����������J�>]�VZIo%U��7'a]��	�%���2����������w^]�E4�0m�n���z����L(�Yd����aj�n����6[b�(	�k���"-II����Wi�&�4�B|�%��}�o��;Y�gt�BKO.����g�����������(�����h��'����J�f0�(��F.���&W��n��n[�b���E �����4�cl�ne�u������"�
KR������>Z����3�dl�<O�AQ����e�)I��wb��7��x�I��G�B(��Fw&���S����#��\���0$c
FuI4z�-;����\=��7sX�e�F�#��h�����s���4���4��U�=��Q�)=��z��#
'ef����,�-N�S%�J�������(���mP��q������6	*t_Y�0���-;�:G�U�<���>'�^-��}��-�0R��5EV(F�RyY�9)���m�����y�s�v����ob�I[��\ �^�j�;e�$j�3��X��m��v����%J��������_���He�t��nelZ;;E������������/��C5]�����y�+�7��+��B�����j��c�Y��%�f�g�M-o��LZ���!��J�N����$����9g��Y�k�]vYF��������(=��z�j.n����OZ��+J[�9�W���/�<����O��O.��JVDh�"D�������h�<��
/�bOBe)�0�
�w���5i,XB�S���\����p�8�$Y��9a�����D���x����dQ�{o�l7aO���������E�]mGW�F��M�������Ky1��X��V	��D��4�z��s�2A������zO�y�����a(�kcm�az��6�����E��W���t�M���Yg���8�s#�	!rh�����P�I��$d�R�y���Mx��"V��1������<T�����;O`�HX�$��k�(��P����$�z��]����0�^���D�E�1���2��;>
�F��Z%k�y�r.����tL
c��j�EOI?gO�1�o==aB?%�`z��\�B>IHq�UW�3��=�@V��(��v�oo���9eV7�Y���7���h���xc4��G;���M�4������}\UOi�`�[l�E�J�^zi���'�@f���J��3.���h%1v5jTT����{��b\!�0���I��l)�7Pg"�� �
n������C[	��7�#k)�PBYe�����i���/8����B���
�����0��4��F/|�J�w\?CG��G������EI=w�M8��e��$b�V����pR���lM�R��\@���^u�-����|��
��&V}������7�.����4bn�v�|Y�~y�ab<d��S+�p�0v���Z	ao=G�.p�����u4���Zv�c�~�-��p����^��/�9*G����S
����pV^�����1�-��q$�\�q�'!Q"�7�c0�<}��zc��g����]�u��^��m�������n����_�����3�aw���y���i:��k���g�yf'�^��m;:`�I�pEi�>G��������lXM�}m+��J��/<��~~��s�0�E�!i�ca�d1�70
�p��Dx�#~��A���u����+�)wNMY��3Y��DP���D	����_x�s��sD�
����Em���,�$1!11�0������3�-'O�:�q�rP:i;[�uH��T�d��z�y^L^Z���%�����|i����.i������Q��7�!��4!������>��������f	-�pZ�4a�� ���"�D���P(Cx���`o��Lh�|�C;,��y��Ze�]��6�7;����e��>���(��)�ej~.
eK���<=��P=Vx���e�mu��Y/�X�]������e8V����9�v���
���{��Axo����'�^vY�8*b\b��<Pl1��-�����?$�y���>6��w��������:�X�����
H5]���Q��t�D�@0�0���CMx����a���=B��Tb��pL�M�C?���OH�j���yV�����!�(�n�8BH��E�E���Z���V��8�	�u�N���^��{�����F}��$��g��}������������,��1��<�a2wz�
���D��xj�0`�J�.5���f}���|'�X�����o[�q_��BBKIk��c�H�'��%�W�}�ra��B��o���k��v�������	;m��*�~���F��`(�2Y���hK�2>�;�dU:S3#��C�C�����{�	G��"�?�����!�R.�e��i�hZf�c����\��v��=�#��K���[%��bs���gK�'��s���m��I�V���E#,c�����B��y���8�0A�!�+��	|/<�;������!�w�~���p����m��������<+���d����bW�s]c��b������#�P�"YS��q� �}�����Uo��c#��P��!{��1�{���������<N6NVxE+�D�����g�5t!����.��i�c]+�|/����G?o���*^�20����|cO=�D(L�P���AO��l�M�4���C���Z��u��bm<��	/����'�p�.����R�Xu�@����{�B�(B����>����uc��Q�w^f��/#{N>/{|��J�4,gBa��P1��������R�?�d��s"�\�����_�4u|�n��!^��!iQ���AE[����|�R�����MY���"�W��������!�H�?a����&�Yi�X����UD����E�m��7X���Y����)��1�}��>K���^�����J�{�2�b��L��k�t+B?���^��3�$a�P������X/,Vv����0�J0��<��~�[o�5����C����=�'B�/}�KZT����s��	!�>PXm2%���P��z9��>u|�n�\#�U���_(E1Jb��~]��u��W���J����Y�x�r�	�T���s�#��0p(k�P�mOO_%}c��g	�����GG	2�h+�8��,������`&K�H�|��
X�Rx�Cj�Cs����S��>x����h������<hV�c������gh����_2W�[����@�BvW�fO����"�/�}��I	��R[C���1X;Q��W$<o8g�'Y��^��g��[��'����1���w�8F������w�c�N�Q�b��m����N��������X�A|'|��)������Q���.#[�@�+|��%�
l�q��'{�%�2����������g��0���ukx8��� ���!��oK]� ��\�c
>kdA�:^�QY+�z7���?#8������yt;�&�8h�|��d=E]���=��^���y�E�:���Y���|���!�l���zq��B�O�0Fu���~.c�1f�!�_�l����&2;�S�>����.��;�~!�%�*�^�S�#�B7�[��}��=�����~��p�@�����7��$�#�Z�~���p����m���<�]92���=d<��F��r�9�����G��H/���� �@"�0B%J}�u����1��1�<}��z��[Q�
�c�F����G��REv]<��n�q�xn�^(owu��< �b�a^oR��Wd�O
_�AZ�&��^��\���Cq�c��oV���ymr"���AJ��
�
{�+�fwY�i�/����"� �*��&g�8�)f�eQV�aP�YV�����'��fo����<u�iP�����g�����@0�U%�"q�����){n�r����F��L�[�>����q�:���QP�Q�:��V(�bu�.{h>���uEm-�����U��?�h�����	BC���d�BU��'P�t������E����Z��na4��%��rV�E��x�!�,C��$�
����e/+7+��N����a��0�(�1�)�7[G��*i5�>����������J���z�h^Z^(�f�����,zA�c���Y��-��F,���u~<������Yyf�c?�p��J_��h	l���g�����n�^c,Xh�����h�k����`LX�N(PX`��3eQ~���s��>����g�$)�q�}������x��p"W�4|�*�#sp��|0��7��3���X�_�s-�3�
_����6�&�Q��"J/�gK���Y^�����~��7�*��?	=��?�����D�~ �Q�Y�'
S���9��~�g�^�����M-)�@^��=�����(!t�<�:^H��u`g��k��,��|{K�`����U��~d��q���%��yUfr#�p+�L�s��cK�B_�z��R��Z^�z����:���CYg�s�ay�F�����u���m�1=T�"��Q�H��0��<e�����X[�a���m�`���(�X���H��>�s�,���o�<8�u��O����=?���u}�����D0@F�D�C�����_c����k�^��u�A���&�
�C�/c�n����w�����7c����C���k���9;�^�*��r�p�_�<3k�v IYa�i	������������#������!�r.n��b���I�
^�$����OJ�5<7�%��$bp"�
������MJ�u
!������: ��\c"c��2;�X�. �c,��'^8'K���2�'	�->Y
O���9���)u�����������t��h��������k	H���������c�:/Y/�n����������{�2�D{E8��mC��
�Z���S�^ZcxS$�e^�6dI(��M/(s<_���n��U�Y�O�����T�zw�����k���1N�i��v�n�p'��[iy�s6��>���T	�P@���C���O����%���m����ap	�6�x��JG�'�7#-{�I��v!F��,�[���P�J[���H[_>��(�q�>�wE0>�(������f��H��c����K�-<����<�o�}]	K�w6�:$1X��!.�6>��%��.����O������t���B#%�M�/�����2?�cf�go��ix?W�7���\�G��i��a[��,�
�����yA1c,��!����0���x��������u�(P�|���~��:�x�O�����i^��5;�$Y����H'�9I�W���y4*���2�G����F�������F�"H��G���G��3)<9���P�j+a�a�w����#{n��J��]dk���P��~�\sM�_0eQ^E��|�^�sd�:�
B��w��:FI��U���$��e���(�c�u����`�����-�szn�!/v�%�&�ZBbd,�4�+�`���i~�S����n�������+e�
�G�Y�B��}n������5
������K��)�:2��^
�$������~�;
{���|��G�H��������1
�p~���\�r����6`���c���_��U�P��K���G�h�������0�����b�L�������}k���<�Yo�2��w�g�R����;�.�{������{��]X����y|f�\�E�D��N-�����Q�vk��7o?��Y���9+m���tI}H���#5M�����y�u����k��v���w}!O9�=��� �7g�T��gm�4��a����;���Cc�C��������
����Y[��}�g��7����W*c`R��`Pk���i������Lc��o�����5w�qG�$�7�����N�����`��A�j.�c����6f�Z��~>x�d����o�����5��q	���o����v��u5���!L���M=/��#�u����2q18�$JDu+��K�&~��U����Y�>����m��r���%	�ELSP4��i���r`l��n��n�u|�ne�f	G�	&t{���"�N/�����e�zxGc%��0v����wV�����n���Xz^���+���{7�^w���n�p)���gI�Zq
+�9D�`�.1�-��U2�7���4���Z�[YE��Q��?a�W�g,Bf�h�u��������5��#{M/"�FK����)<X)���.�K�yEX��(���e��+�4��~��/�J�d�K��WYu���������_�`#�`�/`��+�q	���d�z7B��%�'X:3�id�a��UE���������������|7��C9�b������D��%l~|�K�v����l���x�
YO���AxR];�a~��
s<�@�(��F K���/}>a��?�~8��LJ[�{W���!	F
e�~�c���yg�;�	�\��c�
B�V�_x+��M�x�� ����qq?�K�@�z����C�l/�P��/�g��2f��k���;�>al�I�
����l{S7E������\�	�@IDAT��z�aL���*��	�����x�gQ/�GV=��"��>7\����m�/$}cx������~������HR������o�S�C�kqv(JE�X���a���({���y���$�G�����,�g������@�
S��B>a$�S���b��7����12�"�����cG���v���\Q�����rG`� ��(Vt�K�����l�@���8���bF(#%,u	w�E�%Lw��0�\�|*�p���yuP��(����GJQ�R�Zx��#�8��#���w�j��#3���#���'�7x�:9��#�$#p���zA8����-MQ�\�_�vE��������p�"`�i
��a5�(G�pjA �������}b�}���w��GG�����7V�$%4�+Ik��2�[�W"��+I
?:��#�8�@���m��F����~��HHa5���k'�
q����I)m(��GG�p�+��\�EQ%)8�&��;9�@���p4G�p�� ���� L�P05W��sG�2�h�w��"�++�,�~���?�Q�m����e�=_�
a��G�pG`��k���{D	�^�l��_$Fi�-�u����_]~�e���;��#0��J��m+{����c*�
�tW������Q�-�pG�1��^�n��6=��#�8���e��%��\w�u�B2�Q�lqxf�����\x����pG�pZ�{./�����S�$%�����E��;��
�*��#���Yd��)������=�G��b;*��pZ��{���#xG���[����n�z\p�P}M/G�f�����6�h�h���F�}[������e����?�^/�'<��������s�!���}��������kD8d'G�pG�h'�=�l4�����(@�}GG�M=���7�����_�?>�\<I_x��v����p�F��'�|Gy{G�$}��'���>��?���f�ek����o���9O�&�b�i�����M]{����8��#�8��#�8��#�8��#�8��#�8��#���������mmY^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8mE��m�2^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8mE��m�2^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8mE��m�2^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8mE��m�2^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8mE��m�2^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8mE��m�2^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8mE��m�2^/G�pG�pG�pG�pG�pG�pG�hW�6�g�8��#�8��#�8��#�8��#�8��#�8��#�8mE��m�2^�a���-}��_n�}����F��7_�ex���p@`�Yg�F�1^�g����j/��������w�U[hy��]�/	���|%wzO�8�����*���??�K�X;$~�pG�F�:��T5~J�j� ���A�������7�����s�AV{��E`�I'���h�e�EOO�m����vm��l�m��ODW\uU��C���g4<�cP���������o�������3��
����@���[nY�4��������^{%'��:j�����3N�F�n~ k��<�Pz�:��q���\|q�;A���\b���;��^|��h���FO=�L���O�������kq?�8����W=��:}|��7R�V��l����>���x������6����|=�v ��w����qf���j�K��E]k�E�#%h�]��R�Gz�����Jk�B�;�����>���E�L2I\�/~������W�������k�fh����u�[/����t;Y����&E=�[o�M��|�����Yf�h��3�����I'�\�^p���7����p|x0�AU��%��[���WXa�h��&�>������t��B&h�)�,���i:lU��rU���������N+�����"O0������E����C��\r�����M����&�������m��$������3���������G��s����H���o�h����(�������uk���9(~aP#��w��GFW\ye��qb�1�~�8^GuT��:�T.g;q�����|���*W�3(�@���Pe
&����PZS�l��� ���~������RK/����[���=Vo���e� ��L��/����{����w]?���~��W��v�FU��.����t�2��t��Y-��}�44��_�}��'�2����������/������3��?NC�'��N�����X�����'�p���>����&O[l|z@�|�����w�y'z��^<nxy�C�3�:�c{���<��{�ro4���>������O�st�w���k���O9C���+����_`�X���=�����}�)����_�0w,)�\��k3/\'��g��������c����8��T���;-A�C���C���.�\4�\s����>��`$��%�.V����j����7����K����>����{���������LQRU!���r���x�j��������@P�"����cE�>{�=��E;�C�;�[TG�W�5�<A�w�����=�������m�<ma���uc�W������K��e�����6.@��]<?G�D��y�Y�GY���ye���,�g��/��DqZH"	� r��#�3���~t�;xf*m�����	^U��u�+����A���p�<*2E�/"kl�����.��w���{���+x*!��|��>�X������������qc�w��k��}I��DW\~y�_^��(�6�l�N��N<1Vvu.��#��s�>;���>�����7��@h(�C��z�k�Yl���U�������8C��9���7\}��O�����(0H�K�2N"k�P�k�����������z+b'�+.�x�j��m�����K��e��3�<�sk��h^�~� ��Gjk�l
^� F�=J����;�@w�+�M6����E	y|���w~��#�8��#�8���@`����T����g����w~��#0��a�#��T:Q�?��#@�]���{NO��8 ������^��#0�pu������{���+�����-}�[�����������������d�M��G?�Zx����E��?�)���{��o�-q_�/���������S�����{��;������hy��m�=���F��y�"ty����j}�����\k�h�Yg��}�������������oB!��������}��]����)��*���,�wN���UW[-Zt�E�?������>�z���3%���Z(���Xb%����~6�������|���������q�X��
�K���1f_�r�h�3��{���~��0x�v��'���vb���?���������]�a3�%�?��R������m������~�����~0���5�o�s���;�M��.�v���[���n�B��^y�U�oL3M��SOE��;i�yi�� ��s��1"�I�fO!p�������j,���k��c��#�c]u�d�0h���[�y��?��\j�hn��~���;�}�]�z2O#�xQC�2�7���T��'��!,s��w��$�0���Om�����.}���~A��V���FE0������F�����������}N`_tQ�7�B���c����/L�o;���i{c|��0K_��_|1�]�o������+�����)|�o�����9���+F��3O��?/��R<���}a���/�H|=k������-������w�Q�����1��~U���+���3���|Z^��r������b��4?�(>��f��O�I+sa�����71��>��������5���2�
E� ��������.�6���h}S���� �Z���vLy^�1xNx�$"D�(�M���7d���������P�O�3�����8M��N��3�~�i����,2���:��3���^��R��e�����������S^�y�'��!X�8�L�_d�c��������������N�2���=A�a�cI�>��e�gY��-���T��(�m��h�H�k1�����m��n�&H����<j���5m�aY�\+k���d���2���G�s��*�C3�Y�H���#�D����<��i���x������A��������k����y�uZM�?3�<s�uY����/^����(��]��������~��(�B;S�����?�����V�+z,:���?��V�_~���2�L&�"�������������5(�&��?��O"��c�=�����Wf}�6�0v����q�b<�C�YI� ��"��g���l�����'�_K�P�9���o���"�{�����~��W�e�9e�~�i��;�G�{��@����]����i�����&��g�X����
�����_[���������^����u����],:gj������AFK@��D9v>C���2���_JB�3�36!�e�ZF��J��c��}����#0�SM��^Vr�y�o����?��b[_����g�KH�O��x��^��L2I�d�5�������^�01�I��t��v��"����JJ������}�Y���)�_
�(z�W�e����A����6���pFA�����F�X�{g�������O>�,�c�cH/���	�]6�F���gYYt�.���E�{n��&)q����;L-�~�>�y�T�gH3�����[�a9(�7��$���=W��A���T�sD���<���W_c�����	�m�:�/Bl�mdO�4ai���"��O"��^{���;��]B��}O����m��>"�n,m�\Z��=o(m;K�=�c��w�0�� �+�������g����������A�,� �N6�p��Rn��&q}	K�Dp*�>� �v�k,�.���� >|���~U��Q��l�������0��7��n���E��/�'�=�/�=!=A����qse�$��<�"pa,_NBI��>L��[m���?��s�e����H��}�*|�=F0Eh!��Y���92:F?,P���N;����gXx��q^�a>w��{���*w.���hO�����Q�*c��G{�0Dh:z��c�-g-Q*cBZ�.Jkh��������O���r",��|I����k�hK�"�M���A�.x���w�K�.�S�32'*�����.!�K"����a~�Q���c�e�@"���E-�����	�_c�P�	����S�ax��E|_�*��#0�Y�o��}Z�C ~���~s���d���u�
m�N�K�Y>��'�=[H����;u�g8�����>�MZJ��)����+��)�(+B��'��n���H1���\�_Z�f��Hx$k�C��*��l���J_���������� <aQ��_T�6��d5Q&�"|��s�.�����mjC�����/���4-���\u�����|@Yo�EpN�M2rcn��o~!N"�������Ii0
`~Gp�D�%�������Nw���������~�k��00K"�E�YG�����w/�����s�|�q�]v�����y���?Nk��p�d�$*3�$��v-�K���Tc���(J7X�~�Y�w���A��,#<���:(�M�k����qU�G��3B��n������b]1�������8#6���1p}��I�!�����EG}t����?���^�q����l_
����eC���WY���U�G�O�1l�y�3u�W��E�Cxr����;�vX��9�����v]��yv;/;gV�Q��d�?-
c�:�^�,6���Qx��1��>|Yg��������L��~>��z�I��'j��!
O��$%)�GH8z�-���?>�e~�)�!;��6I�3��h-��^�c��+R��i?#��W���*I�<�����R��V@���A�a>y~��D ��$�y��xQ�W!�4%)���z�xN��	1IJR��:!^�M��$'�f)I�<y@���������A����@�w����(J��~���fY��b+�o[��oI;@lY�Y]u1)���s��aw�bG�C����T����Q^�-J�%=G��H��+i�A�p�0���^~��f"\�"pdi����
�-�U���8�#�,!�����(�� ���cLW����0{9�����7��B������\xa���>�9cst���4,no!s�J�2t��1�7n�x_\%S��t�* �QBi�E`��2��T���C��h[Q���)�%�^q���PI>s�X�#d��'LC;F!r�I'��u�x�)}W���I��#��8���d�%��X7�x�$L�^�^e3�y��y 8����������K/�$�������4%)���~g-��V��c���[����SO?=^���L����e����x�3�pT���*�����
�.5=���F��{y�U��:�
k���8��s�K���9��u�$e,O[K1�3'�1����u���=����^������F[I��[D�o�4%)��\��e�z �U��5�X�%�&	I�x
��S����U��U�9�o��e�������^��4%)��g`�Z3��h�6_*�����{^u��k}��=[����3B/��x�c`m=��y������g_5���6~�+0�%��2T�7�F�x�V��v����Y���^����O���=g��]&2��1��d%��Sz)3h����������������b��X[c�M8�$D�%�#$�"���)�#���&|�g�Q�	��w�g��a�}��X��9�~]��Qd�K�{�JyY���{V���xa
���b�A��nx���']�Z��������:*�������!a����5���{�����<[xW���.!�gE/�Sm��Y�+K0����u�Jh#�hL-a������+���&	!��&D2�
O�,B�dixk� � B�F�����-BNB]�F��oIhh=��U���@_�a<�hGJu��^
x`�q�,����5�<�]g����k����G�7J-�����{�����S�����"��}�(r��"W��,x��ye��vm�]��(|��e�b|��\B�������������{��0����h�(�Qz%E0 �
Ht���1�4�%��s�(��o_���BQ���>P����G��V���/��3D���4�0���pB8�/x��8�DX@x�\�6y�+���lFp�����A�p�����P��1�q�t������
���/eU%�#�2r<$|�AY@)~�+����5���+mB���i����Z�R�3i���v1�� ��{�;V���4��&i�(����X��M4�(����������_���������;��ZC6&x	�g��R|S�Rw�g��|k1hRb���@�u��`�	N���\M��m��&4��!�`^���+cc)�����i�42�^�O��B�����U��u{,�����@�~M�}O*����k5b��N>���g��gm�_P�K�#�K2�|e@���d|���>��������r�G�~<0�Qb<��~s��!� �(���%��b���5�g|���z�%���=Tv�����"T��������������lA����C����D�yTB��fa���)�3����w�DH���� ��1�u��'���DcYP����kd�5�f&=��'�4
��JP�S�fb��a�$
��is�e�j9��4�h��IQ�I����rO��`^dk���Mh�?�5_Z�O���_��h~.��k�w���^�2��1R*�����Xu|	�������f'���|S��!�>]�����)��:�{�}'��i��g�F����KU�8��c}����a���������<�c|�-��ywdE��_�������8���{^��� ���1
V]G�����HD���9����!�+UY���ii���3u�W���y��l���pZ�U��2<jZ]�:��0�V��f�e+��O�~�'����l��y.Y�h;�c
�b�mF����L�|��#�f\Q:�_�TQ�)SKUPj��R�l�"4��L���+"n~C�
�G�w��X��~��E�^���X�<����8 J���1��Ly`+a�v��?6�$��*���M�%�JJ&j{	�E���D���eo	������9��VQ��k�B�i�H�(��&��t�n(����(���%	!n�P�"�S��/�`Q��\����*[P�!��Cj����%�
�(R�$���z�j��2i}�#��5���:��]�7�c���P�jY�2(�taF�b��4��e�(�3
��d�m	F�U5�@�o�(\��v��#���E�*A	AK�I��%$c�bA��"��s}t5�)��&���)��]@�V�O�}@�m�lku�G�7��\������=������4�����v�q�h�=��/������S��Hx\#��d�b
(��e:�Q�B	E�#(��F�_om��6q�1U����[R�]�&�����#����<Q& v(-��P�~�=���aEiwahD��1(y.���E\��jA��4!���O���S��d ����(�F����C9$1�f�D?1 �0J=�QvZ��r4�����}���������<����
�MC��n�!�����(E���J�#�	�~e��\�dOW����I�O:��^}�.BeUP~��G�����t91$�c;�\)�*L��I����W�#D	BI���o���:;�����hE�*����De�#e��������;���Dv-��x7�����p�s>�e0�������������Y��@	)�r^��VmA�4�-#�k��	�u=��V�i��:��x���
m�;Y������t��w��F��Q�,J~�g6?=���Y����i�������4�(E�����-z����h�6=|c�%�����T~���,����n��s�"d�o�z/���W��(����G��(������D$aQ%xi��U���B�s�o���_��U0�P"�UEi���6#5�<��j�eZ��p,�v��"X��g����j��X�]����N*���Y�GM�CxMC�#��^�M�����g�"�����0��^�y�Ud�Z���������C��0Wvq�d�$�C��0Adix�Z�>"��oE�����EI���r����y��./O��L�~Q*d��!��go�P����CM	��2�z���o�DHL~�A-��ty~����<���6�e�%�xK[�R T�r�X�`���q��O&�>x�f�\���!�������K��(L������b�L8F��r�x*1��o7���EA
�y�����=��T���$�k.� 
$���"�H����JR��\k�h��vJ��*I�w�(�lHQ����v��>�s��6
H���1)/{�_0��.��./�Z�R��(���BY����9���tY����F�e��M�����^aJ�pDh����x!����g�Z�m����1O8FP
p[��C�b��M��I�2����G�\��'|����F�KJ(�m���zD@���0�	Yo���o�����c�;������
��Jx����^��r@�L����!�r�
<X�IV��{��Ua��Di1 �4�x�+5vW��N��PI�=�.���!���9����O�H��PIJ��b-I����<iP����d���+�cMQv[e�F��>��:f�W8��o�u�P���V_c
-.V�[�;72N��M|���8�~K�O� ��%���JR���&��S��DN��:���U�s�H0������]v�����1RB���O��/��K.�T�G��-Cj���vn���A�5"qI_�m���v��]����%����1���;<"|PH�K�g:fq��)������%���P���s^u��c}��F1x�JR���u�������*I��Z� ��f���o����R�C%)�N���0UB��-����X�7'������g����~Rv�Z7�y��Z���^����K��55g�{"�����0����mV�{)3����s�F�����K��'B�.���b(��X��_X!4�_��#�VY��Zd���������R�=�*V����b}�X[!�Mk�q��z��4y���#|�)���B�0]��WA��V�]`��J"I
]�ElZ;�4e�-�+�<�EP7zX�w���)�������NJ�v�����q�0UoF����k�D������+>e���V"D
{��W3�Q�P�25|��f������O�T�e��3J��Pb���@�}=~u�a�g4�N�����������6.!l��e�4M��!���3�o��mC�&��F)��-���N<Q���b�EuZx@��X|�|�0���V�z��c
��@��5���U�(��2�����*���{�]-{�*�����x���%}��~'I�A`����������A�x{/%<��Y��*�i�x)���[(����t��`/
�X7�y�<�M3��y(x��V��P}�o�Mo��u��W�Bg�[Vz�I�[�����
�������Q6����4)`��B�0���O�sd^��){M�A��^iu.�kK��VB���c�����]w���s��u�����;#�'�U�~F)\S�u=�9h�1a4���X].�#�����(�v�.�D��G��I���������[0po7�v��)L���]'���[��������h]�\X���]��R����ik>xKxL%�tN���k�zd����h�n��c\����7$�5��~���g��z5�����6*v��8�@">0�*m���z�yl��c�c�2��r��L�n�]�6�i^h��3u�W�G����]����9s ��B	)���(����2S����@�C��}e�_��{��'������t�J�=&g2���I���"�[E����"u+��=���(���%%X�1Xk������Y��zx��b�T����oH�X���P�z�pM�|rn��N"�1�~�k��:K���P��i���z���%B]L��-���s�YST�����F,h��n[��cP�'i�����=��#�d�s�-��{i���}Wf(���������7^��YZ�HK#;&=��si�����d�F�$azqZn�Wj��t�O��K��K��>��m=��������Qb�D(q����a���[����pv�d�,����tE��y�����gR�HB
�2�81�=����2^���8Y��~'�B~	��4��pB�ro�n���o8�l����Q}�=��@�C���f�f�g����q���f� |��~���8O��N�V���^�xj�$�)��5$���\��&(���/�>�t��f?Fm�_���S�-{��:�k����S���}C�N*}*�R��o_U��(|�v��g�{=����<v�P���������g/m%�Y-JM�U�
k��6k�*�h'��I����&]N�V�|`��eO����zvbLd���e$T
���F��E����>��C=�w�h��4�B��Iu.�����z�^��k|)S�n<"<�Fc<!1����:[���^��M�qE�Ga]�H��?0Mx�z{�<0B@�h�{��+�m��s��JW#B�����9������y���)M��kS���&k=S�z�:4����@�ij��wb���#����}P��@�L����9UpEiU+<o�kC�a�I�f)C4=J��e��Z(N�a���2�^�W��e�����O{��w�����m�l���2����g�kL�i[���x���*�����x�N�IS������vG��U�9���#�'�rJgA�^�6�C D(a6UG	�f���x����$���Y�O{��c!���������:��(��]3��h����Q�UG��2��W��VO�hC1��#�K����������Z��2�E���|7�=���C�G�� �>���\���Z�'�K��O]��J�i����iGm������Lz%
;�%�+r��q����V�c��x3�!l��}J�e;��e�w�������?�7�W�UZ�l�w����pI�n��sH�I�B��3X�~W
����<���p�vD=���;��v� �����bT�3�O�*J�K{��l�(��UQ���AJ(�zA��I��:h��w�V�p�x�Z������a�6^UE�n�:U�^(^���Nc �k,�v������1A("�h0�x��<M�������.���o�w�������k>���X:�[��I�v<���}�>�J�Ihw�0�$\%���=4m��56�F]6;3g$�����I�����b��h�����@��5�}xC�+��
y��f�)V�����rC�ax?�����*�#[O�C���M��d��Jy�c�Kd�g��������A��-��U���+�vm
�n��&��3u�W�C]}�����~Ss�@�#|G��WYG��^�L����s� 0i����jL!�y������=����4����C���.��~��^�Ww�������c�Pkv�\5���<��R,�4m_��r�#L���s�|���`i������_/$$(��xA�?��j����nK��cP�uO�Vy��xYR�����P����m����	�e���v��IF���*|�z&�g��_m%<�������5���x�2~���o�XEY��~U��vt�<<A�<�~�������)��h�e�R~�S�M�b�V����!O������?$��a^����X�jh^��
����JX������}g�/<��.|������>a~��p���(��9�^�(xT������0D7����n��q�����S���n�o��
Y��S��0�Q�
���V�3[Th���d?j����5���m�6�^y�y�4;6�13������<>+m�����oS�d�I�a���~�%e���y&��x���d�I���]�KRZ�f����/�Iu�su��J^u�/E���{Z>�������w��U��:�G����}�>�;�5NZz�7��m�g�1��)L�b�k�2��g���2��3m�3�n��Rf��.~�h
�(m
���H)�r)ka�������r�0^,��j��r<N�
0z]����#��(�,���d{K�����?E|_<q�4�	�J=�\p��}�BC���
�}�[L	�h���y��a���������Kh}7����MY�]"^�E�Mmg0�Au���7LK�Bn�>���P�s�9gGx��1��#J����������'��3�:J(#Pv�����x�6�z6hz�x:�v!��$�t�m��+��[#���E;�"	��xzP�t�	��/��
�k���L�P�C�q���O�[�GB}���=
��]f�c��^EU�a������t(�������,�/�,m��|�v�*��+?���bP�G�y^[���E�����2v����UIB�$�{Wi�U�x3Cv�������?�n���d��Y�b'Q�'�4�jq���O�H����s�z��
/�
�
�P�R(�����,������ �O�?�~�d�M7�D^h��m�f���7���m�<3���c�H�1F=�������(������m������|������Y���*�E��MQ����J�N;����1G�w��Ce����S���L�����e��HC;������H� jQ�����I���������,�o�c�_U~�l=�|����H�����2�����-���*c\��:��ls�Z�z���[}Q]H��$5���\����5�iU�X�j��E�#����y�7��msfS�Yg�����Y_��(���E���!��[ou2C��a�:��6i���Zw���>]����������������&���Dy���l�NE)
�LA(���E����4���_�B �Xw�u2CM��YG��	AI�R���l��w�����Mmg0�Au�=�-������M6�$�^�A�0�eQ����!��b�-�I��/'{�&y��$MS�~��G!��p�d����_~�_m�������!�� e�<�|���K0jyW�6�E-�����C�gQ�f�_$��z�-+-=!Ay�4���y3|0��,�o���,4��
��']���(�_R-e���H���>��?`���$�:V�sU�JU���*���������Z����4�^�?^=V�e���b�4��W�Z�c��6��V�F�
�7�U��E�ua[H���>�W�o��QH���>�]vYGQ��y�e.���D9���T�*�H�uP�����(M�����#�L�R����l��Na��=��+�O�k�[�1������B�1�U�WH�7��E�9#�����I���V7?���&��9��'�!�=��T�5_�A�j�ZN��"c���>���E��K�u�i��B�L���cS�����k��z�kMaZ�:��Iu(�m�1����z��m����m�$o�������s�@ 9fK%y�!�!��
Y��ji��xu(� Q/��G�Ty�5}��OS�i�m?���t��1�JR��-J��@�
��u�i�%�6V�7��*���x��	����	��O��f�G����d��G}�s{�����'��a5M���`���Xg�������!^+���%;���I��s�J���-II�}�ri�g���"�Ok��A�{�I'E����R�q�y4u|S<�Q*)�9ROKo��������9o}'����nj��{LO��3�N���
	���]�X"H����]x�����"�s-�O��$VW�����(	��,~�2B~)���*��4�yC��q�y	��V=���*�J�tU����&�Z�{i����w���]������}���[V%B-+�.����8�X���G<<���z^q�z��1�O/�a��8�G�6��$%)���������4����i����\{���3������|�����9'��f�5jT�0	�z�r�_&~t�/��m
�^���������V^�nd��9-���x�F�h;6���g��l�=;���%Qh�g�8��/��r�.]�Nj����~e��s|)Z~7��c�cxP������I�m��k}��.M�Q�!-���wk����������k�LQ��Y�0��:������y�����~�?�l���}�
/��93�����f�Q�^�i���YG�)\Q��9�e �{��'a��f�6���>H���[���
����7i7���k����:�#o�	���a�m�mC������^������m�+�g�QG���BP�$%U���f��d�����	�����;:C�����w�ObTl[�a�:i�I��N[� �W�y�uO+#���~�{��s�x.Z�r6\��_�i������'Z6L3���G�!~���K.�V��������su��p�����F��|T�8��"\�3�����-m����������&�.}��i���}<���SJ�yFO�EYd"E���8����>�v�h'
?4�	T�R������H%�.� 
�@IDATD��CF��	�z��se��;|�$#�%%���2d=�0D`.H�%��]��+��������\f�Bv7�k��8��Mc]J'�<<�D�0>	<QvU%�G*���?���e�����R�1��P�_��������[���gV�kk�������Lm"8�����Y�O��1^�t\�P6�c�:�2���R�� 3�[�����X��h�����6MS�����;s�'k���y�UWu����m���^1��p�
m�;��l�P���M����FH���N��.��U���N��?���=�s�����n~��������h���i��	S���u�w���c�1���Q]�S&��������	���u����i��/����V�o�5=�K#����~l�:0�c�Z�/����E���G�$b+k0�����q��w�he>!j����.�i/��e8UpEi�jx�
I�����h��f��3!���'Mh�Cb���I�8��[�
�����B��>;!�U
���+���D	u�A�E���3F�M��P�F(��%�f[BJ4
"��5���N�X�#8xI�m}E�
/�	���*���
k���"����� �BT�k��Ba��w�(+B�m` I��@T	�������v�8)���u��[9���*mc�!�Yc}��C���-�d�6�����A{��YeN���\�O0�����B4�s�^{��mBA�$aU�1D�o��<��N�,�.�<�����	��(=��z����`M}A��o���#G���8�D�\��y�x��9� �p����[dq��!����*��>�(M���0�����M,��o�(v�;�m�2��%nZD
����;�o��v��2�5wN9���[���0JSr�d��2!R�oNi5��x/�9L[���v��%-{�[e}�#,�Bk,�C%�}.��-c]V��^����sUr��]Ei�f�i���VOq���U�kw�uW|J?S�.u��5]���c�O����UG:���_D �c
e������P��r�������;K�S"	��b�t�9�t��I���[o�������I����M�u�X�w^��_����}w������]l��<n#�P�p���+���\�5	ss�!�6�B:X��w��Iy�����m9��"c�A��N"jPGKI�����b��v^7?W[�JfT��R��=Cy!wCyQ����^��y�V��Z��k�i�?������-��������ab��<�X��.��x����4n\x{P���:��c�Z�/�D�������#��{��}�Y�V�:y��������H�[f�y]2�����#����j���z������&4��%��X��������z����|}�1���{��!��]�R�X����X�,�������r�����U����
�(E�/���Sv[O���*��Ep�v&Y(]���N�m}��z�n���P�����������<�$�����+�m
��s�����d��W�C9$ZS�e��v�m��=����	�y�Yba���B�����7�C���n�=��u�xR�8����q
qM�]W����{���P<�xc��g����~����s��Q��/c���.OY����,:�U���b ��	��#&l�9����b��M���E!)���m������`�'x�����2�,��v�x~"��_R/�����d��4BQ�~���#B�,
<D��u��C�y���P��[���q�&��5�@�N�n�7x��E�����U��������E	�3��^i3�)h�_�����hi�=�9����SO��*��'��=�:��.�lKx��!�uYB��>qv��S��X����I���������DG�y�*�����JYj�XW����	����D[������O>�{������!��(��� �Nh�x��e	l���e�.�s����D� ��/��S�D�qI�r�<�^�?�u�1f���%������1�\�)���P] �pc	)l�zN�uB3��}X�w����3Y���E�M��&�kr>`���9���b���2|Az���?i��6�8a��g�������qY�|,��{�_��&W�}��u�c8����1�L�r�@i���6��em�!��C��$QO�9�l�f��&��Z+X0�������vhcY�"��y�R���W�y�1�:��U�Gu�K�<���>A�����U�`���D��������vOck�a����q�&!V��&L�`�
���1�����:�"���-��B�k�c�}�=!�1�d����*�A��p��L���A��
D^��l��8q���G�4{Eu�L{U_/�(��{��E���X��,aJ��,!0eQ�JR�!(��J"��GqD�[���k���QT,���M��~�&O<,!`eb�=��*��%�t�,$��I�!��6fQ���SN>9���g��9���/7������aB��g�P���1����P�=$^i�^�A�� ��[L�)�E_�����GcC���������1(�5�w]uO���u�^��m�}p���^e�g^r��~�M��:n�������VU��>�^����������(���b��m�g����XR(V�gS���B*YH_b~�c?T�./{m���{��9C��~N����8�A����O(G1���P��S���������|�]���F���
ee��s���*?���N�F���
�HJ��#��- �y�L�s�0�S��Q�$�m�?��E��{d�b�i�,�}��e�iv�,��,R#�&�������i��VW{�W<�-3�<��&����'�����'_��r���;�<���y>�P�5$�L���/2���*dP.�,�	zEc���}�K���x4�JR�m�5o������&��M�n�z��y���m�6y�����b��:�s2���B�Q��hiV��BX�)�P����%��
$$;cEYz���:��a0;7�dY1�",�%�+�p�u%1�n������k���_��G������I��5�����k��������:�U����Gu�S���A����g�$U�����e����x��D������uGx}�����0�c�Z�/�.�����73���$*�]W1�V��y�����
X��N0E6?���j��������^�#P��r�2�G�!��{Q	�r�����i�X~"`%���a�j��"�z�-c6{�s�/��V����^MX>����eq�*pm�u�w�i��L?u@�TE����`���OJk��%����J�V,oC�	������0	�i1��-���<��n�����i��<)�����{!���=��
��\b�hF	A9��sF��>�x4�@,	�������V��[��������Q�Ee_�l�b�>�5�+J(j�~!�@�3�$,��������.cE��Mc�m�I�A�������em}�9���)
Q��C�D��g��(��(�`N�}:���a��G�\EG�%�m'�N�
#��O<�o��4���[@�?(/$����T$�}{�yP����"%����������K{�"�/%��u�^���#�@?�0�x�aJ8������f�/�s�bJ�jf����B���c�����un��y��-a'��
��AyN���./���~Y�|�����_��/����x����\e���:!`M��K���9��#xQ��	��2����n���l��������m���"���Z��c/x��w�:�Nw��(���{��o[�������4zR<X�hImW���b�������1��k>�h����M�uN�\\�@���~���A�=���Y>&m<�gXW0���M]Qd/�76�sIG�D%��4�������B]��{��_��m���j��|*sKY���7�2���r���|�V�?�q[�s�cMO���y��6�=I��?��w	���X���p�J��K������������1b���5�w������!�Z��|�:�9�L�O�����������������Q��b��:�P������*����t�|�h���&����'��:���>�<MR�z�6�5�����������H(]�|fq���>���=����#��K�i�1
�����>�����-s�W��'����>�-m]���������d���U��U�"�a��G�g�<����s���D��/�]�����Zn�cs&e�w�������
Cx�UX���6m��M��Y}�[>u�L�~t���$SL5My	A���k����?���DvC���(����e�r���^���YB,^af���b��-;POO���&��i����a]�{&->���@�g��E�����B��Dx,��D;���������t��dV����&�PFEQ�E���`2�x���/"�H��mk;m��a������W��3?�I�9��<����J��,yc��a���imL������#$��G��D�������u�%��H8T�8����iM����}	#�~c�w/I^�i�����>C��?��^�x���y��c��K���v��������oo���9!���)`Y�ah���4c1�o�w��b-M��T9�7�&a����wR�M�I���$����I8���/|m��m��������Q�}"�|[B�3�1��c�;!g�9cI��"���H~U��6~��=�[�@���PxT�I��	(�����>�����������91��hk��`�m>�o<������0� <��������M�/�ufk"�"�-���%?aZ���_7�>��:���U���W�����I��5�{�������
o�7e�J����Ie�9��ZB.2��A�H��uM�U����bZ��"��Z�V�g0N- ��� 'E!����m�T��&[�|Q������TV���U=V��V-��o'�]����v�+��#0�XV�l�2�����'�Ez�	[{����[��srG�h?h5l/a*7/�_R�f	�����v#�<A�����h3>t�:7�����L(V���&Y����������m~�	�GC�A��#�lsu�n��#�8��`W�z��j8^�#0�@Xn���|�)�1��'�.��C�a������3���G�h/������*I������p� D�;��#P�l�U'�5��rrG�)�m�TI����o�B��uG�h���/��;C��;������pPk�^�A(D5DT|���>{�����z����#�8-C�\��'T�SL��fx����P�����G�y�A�	�G��2�����F�o�r�GD���KKk��r������?���;���~tG�p��Q:�>���#00�}�Y�v�n���]�$e���VY%"���#�8�@{�u�Y&R����f�n��J{��	�xG`#��N;u�\�N7�tC�m��G`��l��#��@O=�T4n�������8��#�4��{�6��~{�U�������>����� ���+_����=+{[<����Ec�D����0����#�R�}�����6�������z+�E$�|p�o���oF/<�|��_���Z����	{/���|���������3z����y%��9
-�?�p4����/5~����r��m��SN9e���o��j$�3��1���'�D[����pG��L2�T����/8�7u�%V^�#�8��#�8��#�8��#�8��#�8��#�8��#�V���C���ey�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��W����x�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��W����x�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��W����x�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��W����x�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��W����x�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��W����x�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��W����x�G�pG�pG�pG�pG�pG�pG�1\Q����#�8��#�8��#�8��#�8��#�8��#�8��#��W������^�O>y�������6�Tf�u�h��-y�z�1��d�w._��W����oX����~6�p\��x3������@����)�?�gPo���G`�"0�TSE���������9u!�6�����|G`p#�r�������@Q��~���o��>{��K/E\xa���/G3�8c#�s�E�m���}���ig��H��t(�S�1lcy7�������{�U���l�m��ODW\uU��C~~0?0�4�DW_���;8K�����Q�YA���s�9H��w�x������3H8��s�9(*�����;n=k�z������n���;�fzz���]]�o�^~��������}���I&�$�V����A��n���3I���j������
�\�V�W^{-,nW�>l��ZY�����S����yL���'�tR����������>�m�Y�`
���O�
�@�gesZ_�EU~����.���=��c�����=���ko���6tr�8��_�K9x�2f��{���j{���	m/��`�"�r�A��^�A��+Jq���:� ,��q�7>|x�G������K,Q{n�UV��{����x2����eF��$�N:i���q��l������r�)���_��<��(I�=:����M8�����^�������z��1�D��b�)��6�&�v�o�U�
�����F!�v�mk�_}�������ee<�T�����8�cJ'������kk}�s%��'��w_��/���O_�y3�D����|c�z�S�b2P�Y���WmQ�n�ctP��>�dSM5Um
���7^_���u��n����6p������&�`�6�����m����_ �r�~�L^HG 9���%��3l����#[v���G������Nh����}O���{�=�3����������wm!���_f_�u���*��R'��71��o��&�l�M��>���`���_��K���X�:��3{)H?������o�s���`�z �q\	���u��o�;��n�:a��I���K-�t���ayU~������m��`S�`���=�~^���
���$�{���}��Z���4:�����L��d���C��~ ��O�����{��u�8������7��#�8��ny-��#���Gi�H�^q���y��'���[������y�����&���ON^4%�K�����o����?�;:��@����������' ���!0l�=�>� W^|����]��Zm��4�:(�'���~��l�e��V^q���/��oub��2��"�w�',�oU�s�?���M���S�t�����x��1%y���P�)������Q��?������}p`4w�c������h��>k��l�q��Be�����h}����@��������p:��@��v1�#00p�����m������j[����w���b�R��U]08XX�jL��
S|�UWE�Y�o����G����S�M;����`S���Ey����_��=����j�������������^WG�(C`����f�����j���p��6����~�p.�L��uu���{�zOpG�h�G�����k���m���6VAEM��?������������k�����
V��"���#�8�
B�*����z�GG�pG�pG���Gi5�DM��4����J���k�Q��������~���s������>?���3�<�[k�%���\k��{��^v�M7exl�4�Sd���+������g�y�Jy��g�������*�dc������v[�'��z@w�~{-�B/��������/��B��#�d=�P�.�o��f�f�e����������%�����?dH��}���P�u]k����[,�������]w��}��?���������Z���A�����f�i�l�/�?�����u��N������g�2������}��G9^���?E�m�78�q�E��-0�=�X�;�y��1�"x�H�O5�T��� aR?����?����c�>����o���
6�0�)%���g2��z���������8������\B��}w������U��u��;����0��y�i��.#$������p�
�^%��H�ZBx�/n�~�Le�B���|�G����Q*��^_�',w����z�����Z�������'���+�GZZV��'�|�_"��}={6�ol��'��7��/�������~����zUV�_vY�Z��X��5�L3ec�X��|w�qG>>�+��'41^��g�q�1�����	��YS�l��DO�����z*����~��c��c�<5��S��<X0o���~��o��o�����1�E}_z��������|��z��sn��f�l�`���s�9kY�mX����^��x���~��R����,s�|����0����?�pv�=��Tc����m�|���/�H>3O0<,�BY;6�=������mAes���bi� ��_�^������>�Y����Ns�\
}��������L���c���;��3g��>;��7d^|���������WH��W$�-7���(�HY��y-�}p�������d����;�l	�9��#/��3���t����QU(���<�^dB�{�o������������ae�����]������S���m�zN&��^���e�T3|e�c����#}{�T�4���o��f��="cP������L���a�)��*sZX~�~:Vn��E�c6�����]��l�/�����9���&�k�1������w���[x�x �C����R�/��3NF~��!<$|cz�{�.���{��sy��Q����?�FR��E�k�%�Z*�]���Q��������~����t��W�VXa����N���9[��<_���`���f�!�ovX��en���5Y3��\-r����:V��ZQ�+���:�<�����T�&v���L�A{���jz@��!'�M����2�d������O<��{�x��.)e,���Z����;���~ < <(2(�!��Q��Y�/6�(��'?����D~������H�s�[n^�H^U4W �e��'Cf���o�<y���(�N�;��-�
��o��u&i3xi���E�:}�HVm������gcc>�������#�TC`��'���jY4��<�-�?����5��K�x����������{�9y��>XS�^+^Y�����C�K����2Y+�(_~��5��^�#B��/�(�7(B��DQ�4M�8{�GQ��-6�<W����������`�l�M���[EA���Y�|������G�m"�A���,V���Dyu9��3�#�8"�U����k���[�x���C�;e���K/�$�w�}b����I�b�-��c�#�����e�{�^C�2R�C�b����5�X��]d='��l��b�k�XT��#������g��-+�����m��N;�Rj:�����:�I[�,mt�, �4�\�W�x�k�9�X<��rs�Ce�S�UpR&(�a��;�����/��~DY!�@]���O;������*V5�Y\m��V�U�-G��5Yd��>��a��z�������B��=��3��V^9�Q����-�*�xw�����$F_�A��e����uV�v~
���2f��d}�<0`���C��$]K��E�?��sa�}V����z�j���y�f�c������.����D��
��c�`���#��!�����l�OzY/gM��n����v�q�l�u��/�H\�Gi��z�T�j��/cd=��hCQ�@ec��g�����U�M6�����0��N�U�{�gy���gAXD���s
`�����B(�������o����o0v@|{3��B�����
�}��T�O�Q�!���z�1��M���[dGyd��#�l.�����ou��r��,?��I�#JP�"�'{i�A��n�Mv�y��
�rx��PH��A���w��p��pf�_�2��!����������'��"@j��(�_��SN;m�w��N�9�DT��-�:����b���"�G@��:L�ox#!�c[c��W6;����/���������1��q{;�=Q'�����������/��bN��p�O�t7��=��3c��G	�u�����U�K������Z%JN���	O�����j��bl,S(��_�%���l2�X�~����y<F=1:���c�9f�[��q������$���X�2�0��x���BB�-q����C����[���9�2�\��5|�ILD>�+z���n���6�L��KG������V{��{t�-�=l��||��<�L`51"�)�R���$Y�a`�f��#��>�������zY3M6��2�T|@�k4[/����������9�7�k����8�9d��S������L���vX��
��{�^r�1������o�l{����f��U�u�_!�yur�iul�: sY]�Ed�w6�]G��jMWu>'�p����,6n����L~t������k3�QwC� J�Ce��
�����E����z��l.�)fUhU�tvF��<(��+�z
��2��@1��7�_�����*R��B�[��X^U��{�y�����1'�7�����R%�^o�����f7�wE���|�;��v�J��*���cb�����?�����������0T����������"�?��{@c����"%)��P�!LjL�3��3�nEJR�����:^`e����PV�T����3�*E}���@�N��L�2m������0(R�����z����X~(YQ)I5�3D���R�,���,"0B	�����a�^��E<C��*J�e{VE�4z]�Iz���������^���^��g�*���nE:�YJ�?��?|����>?��f��`����*�=��(S��~<j��$�LRZ�2^�d��J��D�(#UB�OKP��W��>�K$����[ia�3�h[�c�^���]p}|Q^)I�O9����{yGq�Qb�c,)�a�����L��"���W����o�]#�$%
�	c���DX���hlA�N��I��+[-�%"�F�\47���^+F���c��k�x��W�9���������K�79vj�d�Yt�z�9|U����j��' ���l����%0f��j��e�b]8J�|EJR���C
_my��$���M��cJR�Q�5ap34�y� ���>�)�����v�C������H��uE�����%-s�eb�V�?����W<�1�!c(���������=}g���2����w�k�O:I_Y��R���H�F�#�~iW����q���l����8'���������y���+I%�8�1�]�o��N���b��������uj��2��}=$^�eJR�	���x��R�E;��*k��r�uG�h�qO�)S"���Z��@'F,�	������]P�qH��/����!�8+��@,��B}KB]�����D����[T���� �B�N/�x�0�x�<IX�VH�a����S%�/�T���B�_'��UG���*��*������&��r���&�+a�x�0#`�L�d�P�q������R� �F�� �]'B:�Q>���/��`U�!t�0��� �]B�c	��#^V�X�ZlQb��6�^�xx�
HyVtx?k�H��������x/cu<�(]T`���u$�#�N���E�Y ��
	�2�\�
�Qv���e��(WQT�����N�G��YBK��=	)4�(P�r�-Y<_ �s�����T���RB�~[����22�1�A�38�P������7������\��~"!lt�C���3^����%�������C!��W�^<�~%����7���I�W%���y���P@������.���
�xo�����X(.$!&�����Q��A�,-7�m�ro.BQ��t\`~IA����<?�w)}Uz�$���c;���`��E(X��������c����	��
�x��m����^{O��:��7a��_'j&,�����@��Gms�3���xf�\@��x3>+FA���{�>�(�7�xc��^�e����C�b��^ubD��|�}�-������U��������������M����-���o�����J�Y��2��H��s-��[2�,!��pd<8���s��0<a,O���x��=o�m�G��0zn� �� ����TX��/}TB~�)�71�]-�W���
!����g�������:��7�_F��u�[���yM�Z�*|e�1'���.����/|7� d�y�.���uj~�]e���M�����;?��}��[�p�u�n���C�r%;���K�7�L��M����HQ>���t�v'�:<�Rj��Z��Ed��J��_T��4Z
���[��q#T�	��!��������kY�;�)o������YJ�y�v�$��H����-�����d@)��*��Pe��z�:|��{%�_�0_O%ku�s����9D�*"��u��<��\�����[P�i����}���%�>�(��Z=U�����l��*��K�x�m��|�,��$/�Q����2�|@�5��V�H�v��8��[~����;E5��p�BB�X	����-�f��������:�_�9<-}~Y������A���u$?]�3��������H���V]�eR��z�tb��:��|�k��L�Q����f��h��������w�������m���pZC����V�)�x�a�z
����^���n�P3L�@PF����a��cQ�m([U���^����
�����K�����bB���^�,��V���w�	n�X�+�'����o,2�#dX`���j'm&!8�X8�/aT���h�E*D���R��y��JC+�//u��Y1���U0q�!B�(� {�(���D��X�:�`�	��%��Zw��E�=�3������_�0W0�+�����|��=J�"�g���_�^�F�\d�5�2����g�Na������P�k�b��������=���3��Q~�����$�b�% �@�
��D}T�:��@�Z�O��g���t�x�b�}
�DUD���mGBF)1��(���������Z�o��Y�����U
�*��,������h���BYk���a�S
����[n������s�!3j�A�B1�piz������Wa��y$����U�����������\�(_`��bo[	#�D�W�o��Z�������K���~4=�2�~K�'�X�PB��G��zT�1�2�Ajd����\���Y��d�k�9W��H���!!�����d�y���B�PQ�y��+s\��:2��u�;��GY
KJX/�ik��
�p�J��
d<�����`]i	�[f �bY��R�U(IKA�'�X��eX�C:F"xYR�a���1P��k��Ty+��,_�r��z���Q\����f(��������{��z-����J9�
$~����:G�@������qL��l�z��u������E�����u���_��l!1~[�t;��#���%���	tL�Q�z;QN��ORf������J�X+X����N�W`xx������}�-a5CI����";�U�!��G�B��z�t�.a	y��b���/�/����5?s����Z�k Ua�G^W6C�+����=���l����b��0�������!�����'�%@�|%k���R�����F��k�������JD1R%8�������*5�="�ZEm�~��*�r�vc�`��������R����T�%6����cN.��K����T�����:�`��n����8�;O���5D��F����[Ykk=��8��V�����D`9n	\X������>(�����dQ}a��?4�`99B����u�k�����E�y�_�#4o���keQ��JR�n)a�Y���`��h?U��5�(�Q4�~�K�^��b��#DQR�c����eO�}�2S�IL��i���A<����7�<��+J%�������V���e���uk��JR��'��N����k�/�\H,���c����Q�Svdc�k�����Z�CM;=>��O��.Di?����j���`eY��Q����Z�����6
��<O_�C�r)��do6%�����1'T���qY����5Z�|Z=ZO�7^}%)��,�S�UB����l���� <��<�2��C�l*���y@��&C��k��N��!m/!�U�=��PI�u�_&L!M��e�h���m�Q��JR�c<��&�V�(
���|�7�|F�g?������}q�����B'�l��*
�k��o���!1�h�s
Fz�L	�W�7{f)�!$�'�T������zo)�nW���w��0T��,�~}�����z���A���}pC%)�)Bi%�������P��8���J�}r��f�������'�}�+��,#X����0����A��+���{��gH�,��o��S�{X��w*^7��rN(�t�c��R�G;�n
��
�����1`8^s����Ti��v�/�kK�%;w�~��PI���b�L��t_s���B%)i���|������4)�����v�C����'���oU,0*�\��6��(+5�=���JR��'�w��3����>_v�</|������!����T��Jy����c��������\��o��R���H�F�XP	�#�1���(h�G>VF��k���e�d"�Y�U�����{D��]"(!c+#��v����G�E�PB�h�x���N�;U��vm�b4$d�k��
kS���� �Qj�|��Z��r{:G�(G����t�]�2F&�C�+�?+P+d'_��
�4������H�S/I�m=��Z�G��
�y�x�S	�<���^�Vp�������d�q�+�D������@Q�0�L
����B+6�����B�����s,��R����Y+�0���%{��_N�N�}����C��m�'���u�i����E82��}�
��z;��F9}�]��%\�������5�����A�(FXj�F��W���:c�[dj~��S��xY���(U��������h)"k�1�1~(J��u,��%����UD,&���^e��b�-j����%�Q����+�Q����5+�f�Y��� U�����kX`+�����e����e�U��]D������!�$�h��+�C�g�M�y=^����J�]j�!���>I<���P&*>�Ux�(1��������Vk���Y%��J�"���ml:�����;��b�����J�]1c
��������=����2��p(����PeU~����l���'&p"��b��Dx�vP'��*�F�m"6/���1'QntO�N���L�yJ^7��rN(�t�c)�R�O��n
��M���B�w�53L��������6�O���"�!�n�r��Aef>���i�@��HOYD��v\������4[>�.__Uv��y��IJ�9Be�����Vl������=�
�Nz���I�F��k���m��Z�!|������w�����*���R�������*�R���H�F������=����!^���J}�m�:G�t���b&����i��-����h����@IDAT_k���q�_�?�hK�r�����N�����u��v��uA������l���9�s>ou�m�����#�:����?��y����	��@�A
���F��I�T�� o,��)����BhV$8#?�-U�zH�},���timy��)���2��s��w�{)^u��u%J�f�}�(i�F@��<�rDi�e� Z�5{���c���I��������M��-�[eq]D?���J_�B�������)'����{���eT����wk^�����J�z��c;��V���3�G�"J���o�?�e�'v�BL-2���dO�F	kIBE�o��%�,uB��&���4����(^����({I)��*u�Zx���!�o�ok�^o�����5�i�l��������x���DP�~��56
�+�C����y��)a�)s3�7�]x��a#F������;K	c
+���}qd�W%1{��1,:������~�����"������a����D��7������������-_}�����aS��&|����>��`S�[��>S����il��2�MT�0�T�;�WV)�}��[��}�dw��WB�3gtr~/+`;y]��,��rN(�t�c��K�^7�}#zo������b�2B8J��)���yR�e�
�F���%�)��T�'��u����������9�4���j3����=u�'��F��zk���&P�k�Z��	�����{�U��v �|��V��k���P��{Xx�0
�}��yJK*> �m{�������H�
?���B#]+���D���c���;��W����e����"��G'���c�;�5��?k�$��W�����Qc�����9��\k�2��#�4��+J���R����)�w[��$�9�5�\�ib�����d�@������zb�)y=���Q��^^�}���r�=�Pta=���������e���b���%�.q��X���?��l��jem�l�Q����&uM���*LP
���R�x��,�^����Q�E���(Y��/P�BE�����>��E^���o�HOA�R��(��C�o*!��k�0�`���w#d��']��
��uDhM5$��g��Y X:WB�6J*, �=o<y���-C+��0�g��_�7F7�h�k�S��F�Y�������G/��Q�ae���=�������+_�~#��E��8�����GX3��������$V���#��R��RB��<�@�1���zN�#�U%k\�������_�������^����/{��=eT�;�=�W�W�����N��U��T�����G
��(���{Y����V��z}W��Ges��'�3?�_�X;����uS�~�������.�������������d
����']|�%�����fBa��R��Ue�������b�W��g�?:
�w"��zy��@���A��#�����T�9����V�k�l�N�x�F�9eeN)c	��V��)�h��Pwp,�$���^��M��"c�$�k_���TE����w��-D��S"x�,�� �*][!s�A��`�3��]�M�5����k�ze����#P����o�����3%�����0i:1���T)w��4"�������Y�x�eE������F��}	�S&�4���������b>�1��B������*�M�%<�,5�0�jN�L�ib�*�����+�w4Rvn)V��rm �UC636Z�
�V���h���MB���SJR�k�
����2n(Y�����;5�N�xQO�B�S�
����;�L���c ��� �n�}t����8E�s��W����JZ��E��~R�Bv�R+(b`�z�O�~�gou�L����-�!�2�oM��c�>R����1�N���`�a^����Fh��g�N���,�yJ^7�����c8@]��4�WX�z�����O},e_��>�~�v�����z=�NQ>����m��_��Zt����gl�l��~#�T|=��";h�<d��Ys�DQ!�Z_��'�Ph�u�������U�>�l����,��5��1��%�b�6I�w1unG�Y�f2Y�w5���~��4S�*�]��Wul!Z������l�3��\�x�>}V�q;Q���R_������9�@��Gi��u����~z������K:�2 ��c���Z�CM?��aH4���������B��,LT�B��=��V���	o�d�
�$l��C�D'�z� �sI��E��U�%���qG���R�O��
���p|���s���hHk����6�z��=C:��m�5h>�Q������G����F�^e1j����i|��2�s�vBG��;4�(k���
�����!����:B>���C��������[mU�<�h��5T �3�8�4+5�?X�YA��u�
��:��c���m�i���z!�[m�T�#��[�������\w�a�������"�g.���f���$��S�����-,���
���������a�}t��X���&�3�T��)b�D�n��v�K������H���#G���Z�>�s	9���N-" sWu��,+C�}�����k��C�N��������n
��~WU�4�W���SK���6rL������`?z�h���%��-o#}���K����5�KR7%��
�����Ud���X�7��@Ai�=h;M�_'^jVY�V%��(�8�X��q�N�Y�GBc�.��k^��M?/�����d,a^���S��l��Fpy��.�W���o=�E[?��)��[�/�}U����-��51�e�Ij,��YW��|[�5[�����W�yX��8�pEizL�"�%z��P,W��H�2$��]Q��o�
{�F�e��UW[-�����,�
�Q�m)��.��Z�&d�b��V	�t7�BIJ��}��/���kv��^�)}3*���.#l��T��;I�'�z���=��#��"{��i����N�k�������4>�}�1	��.a<ZB7C6�Q~��(9��t�)l+V�,Bb��F-�se�?�X�sL���,���>�����~,��h�l�W��O��s�TQy��
������;N6Ro��P����L(x�V��	/����alO�(e�$��+�Q~���=]"pt>n��V��E(]�lboR��%:�*JW���������
w��N��S��H�D���|���E���g�(u��l�,��H����.|�}�,}�3Our~����r��y�z�X^7��rN��D���KK���~Z�;E;����4���^l��ZR���62w����4sl���uk�@-_���� �<���9���o�F��D��:�!0����<�Ej��p@v�D_����*
�4y�z7��/*k�"���E��]O)c	��V��)�h�C�C�������i�2E)��*�m�i�\vo�������B��:�����	]��{d����:��*�S����}�
o��_s��C�ne}W�A@hs��S�}T��Y`1%)�tB ��Ti������Lx3!byT���$�����J�l�O�T����l�,�P/��z��gla�Cb��JK,���F�X�Z���BOV����|�,�e�_��Z���@��6>�z����l��PB�3��3�������Nf�0�j���/~QKK�������DX���p/�z�����q����h�aM���}Ev��+����QD+����k-��E�T��}+D��g\�����-�=��5<
�.�����l��&Kz��R	kb"-�%1
�;$$p���~f~�Zi����i�g�z��$�:��}TO;~��\iV�`/��8r���fg�uVv��Aj�����U`�
��qN+|e�cNQ��D�����:9���/����J9����3?�_�X��T�_c�R�s,��!��o�e�
�����@����{��O�^��/��5q/	OYF�'�mT�Lx/_�k�*;�<Z���Y�O�}����:d�j���� +�A�R�$�����2U��>��e�l����Y�����]�X��X��N���z)j����mw����o�?���h���*Q�]U���q9���k�B�����~���|^T��8�pEi:,[��jE�����<����G�5Lfhq�����#F���*�n�mM ����v�ZE^���`A������]S��Yc�e�a��������=J���6�,����_X!����v�t�<���BO=�d�9,��#��W]sM��<�zi������cx����^���	����Q���Z����/�TKC2�zs����F�����w�1�Ka|x��gj��t��Esr�������b0��V9�����B/���:q{	���,��LT!��s�����/��V?)����^{��O,}u������=w��g��9���[j���n���%��
��o�o���=���������*	R�-��.����9^~���d�%���.����c����/J��u"(1���6`��v�]~w�N;�^s����Hx�k_@������%Dqh������F��>wk���=������+���^;[M�z#>��
�P����`���#1J�Wj?���9��q
���y#��P"Z��?�S��������M����
?���X��T�O���j����uk�>�Es:�B�}�d�}k��m���_������EF��5���}�R��Ud����z����t����n��������{��m�u�JE</��&m7=�������C�#����c���R�XR�)�h���Jl'Rd�o��v�i��������-���/�zL�O+�b��:�`���e��k]['���E<�}������2�~c���n�/���9�@1�(-��cw��
J��o�f����gC8j�5�#6=�F�d��;�==!�~}�Q�b�hAa��+�d����[���vO'Ek�V{(��;��TSO���$�&J��mx����UW_I��f+V�b�!{��;����`��f����~{�22&�;���{��Za��z	1l?���B�o�{�%�6"�x�s�y����-�B���q����x�eCd���^��q|8��AW�b ���
4(��}��Z����.��F�Z62�*�Y�������
�-B�K��X��k4�F���w����G(uco�!�?���0F	�h;F��0e���x�F|m	��t�x���%���m�M��Hp�H�S�}���������(����A]xa�=7i��v��W��C"�)������R�W=�dS�����GB��0�U_}w
�0(Y%�^k�H���t������UBy���h+���UHx�*����([��.���^��ad��'u�o��3<��0Zk�?�<���
p��0X=i����=g�(_�b�)*#u)u���k������z�tj~������uS�_)����O��>��/��h��T�\��w��a�b�Yo������]e�������7sO��1���<*�u�����";H5���WEf�D�W������D?d�����n�����f�)���&�nZK���eM���lC��u���*5]#��2�|@�5�e��(��-�^��h�z��9��HW����vC,S�����L7���UJ1�T[����3�9Cb"��%k�h���}1��e#�zE�n�cx��<�����#������� x���~+�:��.�)
A�LI�SC��1Hb���f�*����?�=M,7Q��	�	-m��	G9}�^�)�L��b�����%��ZB�fx]���
����f�Ta~~��<Bx������xU~����O%�������w!`��L-���z���3�>���j�qOQ���"�*t��BH�"#���y)e�Z,�Z%���g��{�c�B8�9��a���o}�"��t�]weO���
h��� ���<&�R���_�����}��Q���/�7�H��>H�FXa���;H��i�~)��3yn��Z�]���>�,�N�[�� �XD��7�����I����|P�cR�e���o�;��@�:Q��D�]Y0p��u�����6�t�Q�,.{c3����k"��d������]N���^����O��a�������_���jY��-�����"H�x��3�E��p�7�8@�	�LN�%���z�|h_���6�4[o���&=�$Bk�Q���e>����D��������2wX�����c�6r���5�y��y�0��2��J�K#��!l����b����WDy����.����d�`,$��'H�x}��^�=8O0P����CQ���aX3P�P��+S�9�\��6E�@���a�Q�R������������{�?R��)���s�@���SK���.Z�3U;�������5�U��0����O���m�0�	��x�[��n�_�w��>I�C�8��y�dQ��]�J)��*��T�PY�7�d��a�9(���2o�_��T�i>��{n��L�u�8R��,��R�7���h�{���mg���5���H�y�u���AX��N��/)�l)e,���k�"oZSn��q>Gdd���:�5}�[����7R�UV2�85�r3��{��s��O1�T[��r�&��V�Cq�����*�6+#z\T��������kY����p�y�=:9�@��Gi�X�-%�D��(����W��PCw�1V��0�l*�JR&I���D��7�a���}��?���D�w��������MQ�(1�1I������G���R�YA'��P��R�"���%0�����*x��0�?����bT=��:�6����L����<U!��@
,�l��/d��&�vc����P�R�����3?j�U����n�5W.�J�O��aQ���������e�Y���J����������X����.zc�8���������#�v�Yf����+��v����(��q��N9��(�B���:1�_|1&�<���F�P�ue��K:Q���7�%/���En~�>S�<E����JU�Iy���~�T�H�Q��6�]hxm#2"�pY������+B��!a��Zf^���.�ly���dxl�y�}@��N��}�U��/�����_Em��c,��7��#�*�;m
�c�	�3��+��6���3P�+��b��U��9�\5%)� �)�|e�1',#�q�Wc}����ZM�:h��^������3v���M�����;?��X����e�R�s�;�FZ�Z�?f}�Y/-� C�&�%,[#��%Q@))ol�k����"�EU���� �<�X����H4x K(��'����z�!6iG�YcD�`~'5s�����o�����:\���C^�JR��R��l��-��%�j���
+d�R�c������G}�x�x�v��N���r3�	���+Zy�y����0�l%�<�8�`�����P�n ��f�/�����-�Hg#M�=��G��O��k~�a��� ���y�L
���MZ0g-�`.X��%��2�=������8<����������%[>{N[��������2�v�W��G��C�7���3���U����z�ZNo��R~����<�rf)Q���T�,d2�R�mYh�ZV�m/�"l����"���%��Js>��
�����P�X�.��(F������b�2��8�hs!�=�+���G�(�m8{�|�WaI	%�0F0K���}H���m���A�75m���?|���������b|���f�	����g��!�9��>�{X��Vx���>�Rd.��.�3���k��g6�+����&-�� �?��3��s���2�&��|��l1�3H%[�Y�{���lZ�A(����wQ�@3BmAYD�s�s�2�}�����z]�xl/��B���^��^o�C�W}'eb1�<���K/������)i����0`���
��n��m[���S:E�;�'����K���V���MH)}��m�z�����<6��
����9����}�xg���l��s�kCS��cX&+�!$��F����K��S�
����U�q���s�F�m?�� ��=�6�,G"u�s�id���a|�cML!h����������9=������������G���[�����Zk���������`)Q"�>����ld��r�mQV�z�I����e:s*��1�N��W[-�������R�iZ���OwK�c�=W����/��16����1E;����w�u��{�Qy�eL�I���.�p��N'�;�Z�%,�����ca��W���4��!=a'��#[�z|��������C���X��==4b�!��YB�\��m3;?�K�b��kx
�({����y7�V��>����}��6Xg���s-k;{��5�~��S�X�R�)�h�����_��RL��C4;d~1y�mW�����X��`PD���cG,�������w���!b��l	�����;U��>�����S�H=Xk�T��Dp
�������]�7$/�n�=�=�h��t����_r���p�C`��'��=Pso1���o���3O[F����tjBq��n������{��M,��,>���f���������:0��[��E0��~x��Pk�O���6�����>�yJ�^�x�&%���7�b��,���M&q�e���|��$���H���/������f�S��g�=�N�JQ�"\�]��'m��0((�i��Nq|�C�>8��A�O>�DtAP�m�3x�����}����H�X3�(���5�/�����4���	^��A(p�	7;F"��6X�S�B������B�S�o�U)���v�O)��g��	�Z��J�e�}g��������^�����/��eQ���^�J��ZB���L���hy1TKI���;���;�-����������b�>�/�0��	�}E[��&lu�{�0y����������+S�9a���~����y�S�{X��w
^7��rN���t�c)�R�?��N������O��'�kHB����>v�&�%V>���y�VY����%k���r��|}�A�y���5��$��y���V�����D��+��*/_�K�����>��������A�^)d,)��Tu
�|�����R��fe��[+���*1��P�M+�:(ipQ����l���3f��U�)���c��"b0@{1f HI}1�����}���������@����tp��AS��d�5d?�����Q{�$Q��A	K��N��#�8��#�@px����
���d��l��9YtkhA�l�}�Z��sG�p�.G�LQ��E��9��E�LQ:@���r�>F��+J=�nw }g8|��Z�yX�6��0�x�*�%���$4�U�������G�p����,�\{m����K�$%s�|�Wx��~�8��#�8��#�8��#�8��#�5��5%��8mD���>��0?��^�{��$�$��b,	�a'~){|:9��#�8�@�D�(��f�T��pKcA�;jG��e��G�pG�pG�pG���G�`����lZ~����<K��1%��G������#�8��#��O��r�1����sN6��K�Wh���y��w����pG�pG�pG�pG�{p���m/Y8�����#Gf���[6���fP�.�����?�0{����|0���+��L�P��pG�p:������7�x���?;��3�r���G"T����K���sG�p���������������=�������p�d����Z^����]_^/�#�8}��XO6�7�,D����X��G�pG�pG�pG�pG�pG�p�nE����<�n��,/�#�8��#�8��#�8��#�8��#�8��#�8��#��
W��
Z��pG�pG�pG�pG�pG�pG�p�W�vk�x�G�pG�pG�pG�pG�pG�pG�m���m�z���#�8��#�8��#�8��#�8��#�8��#�8��#�����[[���8��#�8��#�8��#�8��#�8��#�8��#�8mC��m��3vG�pG�pG�pG�pG�pG�p�nE����2^.G�pG�pG�pG�pG�pG�pG�h�(m���#�8��#�8��#�8��#�8��#�8��#�8��#�t+�(����r9��#�8��#�8��#�8��#�8��#�8��#�8�@�pEi����G�pG�pG�pG�pG�pG�pG�[pEi�����pG�pG�pG�pG�pG�pG�p���+J��g�8��#�8��#�8��#�8��#�8��#�8��#�8���+J��e�\��#�8��#�8��#�8��#�8��#�8��#�8��#�6\Q�6h=cG�pG�pG�pG�pG�pG�pG��V\Q��-��rG�pG�pG�pG�pG�pG�p��!����A�;��#�8��#�8��#�8��#�8��#�8��#�8�@�"���nm�.)�,���M?��]R��[�q�'[r���o}k`|r��/Zx�l��&���K�8���6/�����}����[��p���}�{��!C�� ^G���@��2*W�3pG�p�$�M(<����]��}�=������o����j�����������?�r}��c���7��.����������v������,�`���������~���������_~9�l���(�_�?����O������6/��v��"��o��n>�s���OT��`�6^
g��4O?L��F��t��������G���o���k����Cw�){����k��>{�����H^���`����	�I�Q�A��~�I'�����_^���sG�p�&pEi`�#�;�Xcr����^��k��}����z�^��&�,[|�%j�VYe�l�����'��"@�&�p�����!h�q����r^�xp"����;��N�Wv[��h��3,����+�y�]{���/R}s���^���@��{�\oAx�{��|b���	�x)�)��<�80��M2�$5�}�I'm;�~_�+��v:4_g�t�)���_~���$�@����qt���6YFY������P''G�pG�H���7i��\Z@`��'����w��TL0A�Yp)���{���������/����:�+�,���^:�k��������l���kkY��TK��������;�/�/�)��*���c������^��^�>���^��Gw ��o�;j��p�H�Bq-;s�����\���������#�*��7��/�����wr��B`�=���]n��Pw�ygv�	'�Q�N�2�x�_pG�p��G�=J������(I1"������8<`���� mx�,��3�<��r�>����g�y&C����k:����P��d��~A�h;�f1z��G�k$������e���N�w�{���0�u���z�7i^������o[��������D��o����|�}�����_����d �����O���J5��1��,#�~��8��#�8���{�vw��i��;���?�j|��g����Z-�.z�����������8�@A`���w�+��J�m����G����u����x	G�2�&��\!��pG�p�!������G�pG�pG�pG�pG�pG�pG�� �����*�s����Yd��=_f�y���W^�~����������h���6[6�,���^|�������������?dH��}�s���������b�-���O��~�������>���������/6�(�u�Y�I&�4{����{��7#�4�4�d?����sB�>��s�y#����o;�s�9k����oe���F�7�{��7j�c'�
��x��a�o�����������?���>����J6�������v[4'�h�lii�%�\2���g����X�af7�����s��M5��y�>*�R�F�Q�/�W�7�x�l��6���~��J��}����<�����q����1G~��Q�����s��~����_<�t�����c��#ED�]{�u�����~�}��G��]}�Uy[�=���>��s�O�G�~��G���o����=�e������W��>��#j���������VFK���r������������k���;�����M��]B���7�pC�^�}�������|e3��o�l�f���;���^��9���d�����||g�q2�[u�����A>7�xc>�g�C�1�{���O?�45jTv���|s1b�~���[�����<D��%�Z*��g?��~����+�������;��N����g+��B��u��5�i�5�\3�E���d�o������n���X�������"�.�������{,}���7B?��.�������1��-�<sD���x�0���1�;��9���/���]e���c c!���2�=����9��:��b����/�D����~�M7eW�x�SL�-&c!�����g�?1�?'s_��u������V��0�T���fK��|{��?~>'�����_[�m��z�p��+���'�<��o���we,����k�_[��������|O�^s�&�|l�*/L9�P��^8[C���7���%��G*j�o�}��]�����G���A�<�Z����;�&�
?1��)��q
���y��{�g�#�}�9�kY�0�&c@+�R����76��F�����}��?�[����-5���2N���&�d�l}Y�W�sZ��c�]T�����doo���1��b���Cd�o������>��+���x��r3��Xh����ek�]w�y~��5���c,�U��Vd$��Y��]5�������\X�R�:�s��!�(�X?k�z����`|���y@���� 6�������=��������jX�@�E��+O`������q���>��O��x���cM�V�����U���;��#�8��&�l�o:Y�y���^��<�X'_����{�}�]v�5/��L��i�P(s��y�o��&�v�����~����+zS.^q���������"d��Gis��f��������M6�4����f+��a� ���(�YR7et�Ee�,�L6�Xc���������j�lgY������}�e����Qz_�Y��l(�C��K��*�����#��0�!Q��o�9����\�}�0�J�L5��������Z�#�������7����b����{����qH(P��wp�- �����RY��t,rN;��l�W�XT�����"<����d���������Q��oJ���6,�
��/� �v���C1�����*��#u_V*|���G��E
�	�THaY~)e�E�t,�����dC��?{IT�=],m��~�i�1�,�_~��Zz��#?|�t�\��o�����?�n1B����e(��P�}>|��2�������m���o
���+Bzf�yyQ2n��&�v�������m��X$�}�9���gbt��'f�s��F?�`MQ�R��SO���i���H�76J��m��&j������[�?�/����J��w1{B�����HW�����k����1-���r���C-�P�B��pE_�y������hi����}��E���6�L�z����1�<��k��w��;���������=�+
Z%@�2�����^G�
cW�Oz���
_�G�y@��:��1zIQT3�����������]1ez#�p������T��e(���
��`5��AU�C�08����5c,c�7Fa^�Q����>��1����s����<�6�1��>�U��p�bn:�������O8���D1�������'�x"�i����J��#�Z�9�g�b�����Z!���*�<P��(��Q���g-s�%�d��Z6�V�k��0O�m�jz�������v���e���1�ap��W�Y0gM/���bh[/�v�H��e�����f?����x=s�&2��
B~i<1h9����|�W�o�
�=��%��BJ�VE]U���V���c9�H���*#���R������l�lWb]x�l��JD�n��?�|��E�"�~�����i�����i�����h����O�x�!�d��Zj�,���UW�(�-�Zce��@����o�]�/��Y�����yz�3�����#�8��#��]��_ij�G����^e���q�\-Rh�=�=uw�"�H��@��I�x/"�:�1��`{�2�2�+���k��^�C���XY�6Kx_=*��EJR��mP����J���c/Q$�R1�E��"(Z����7��~��xc�*��>���t�!�h������~QT���G9(O��E�V�����
Et�����+��g�uV�����k�G�]/J�$��(��SZ�g����/�����w��������Y"�B�U�$�~X? [�T}�r���c�=K�;��f����"J�z�w?
���J��P�!��(�$,R����Dy�@���o������(�
���G<��� ��c�$�{z�#aw��J������F���v���p,R����R/��z�1�8�������l����)Q������\�U�^fn,�[y�w���z9e���?!�e��,���w�3�gZ�d|?����J����"�-R���~��/�!j7��+���bH5�h9��6�x��xA����cl��|�7����OD\�I�'�*I�}��gs�)��G��GY8^3������}K�l~�y�����N=��*�0�FLIJYP��!�$�p+��of\�)I)��ofe,.2" -s�5�N�Q��{G�k��)I��Hw��)��;]�N���5v~����9�%<���������~�D��C��0���-�c/��Y�^)��PI��
_�j,�LU�{��N�HR~W�ek?M�%[����Dlb�k�M�����`�e�)�7(�v��#02h'������=Y�=J�#Dt(#�%"~99��#�8�����`Ge��+U+����r�Q����I( ��g���.��V��V_yX����ZX�����a��x�V��c��o�&�?HX�E>�����'���%�"����E�y��oA	����5��KkY�#�[c�BS	x�=.�
�q�#�5���t&��

��^Cl���r�V	�D�$BN.(8�"�?]����9'��%<�-��!�T�����=)�x�<��������M��A�,������"���������a��S#���Eb�N�Q�Z�%	Y��X��v�yVE��B�B�-��PUZ7�W3���
6�P���E�^�����Uu��g���WQ8aB�(���o���Y�%yA��b+d�A<k5<j~��#<���z���f�}����#D��E���	�E�������keV��Wx���5/����mI�\�B�T	�!�k�""
D���"��_�T�}w����lB4i8v�sZ��6�y$�};6���������S�<A�;
���qT�m�8K�x�M��B|�xo���5<��`�Si!��6�������%�����\����D�Mx/�'���lH���b��(YQ�*��]"���q��!��W��un����i��{��+�`��Ro*�"��G��w-��E�3c-�y�](����}�sY�����uk�7-m���x$)�%��=�����o�8��|ul���M�(L��\��������-JR����%���W�_�v���x�I�.��Q�������
��M��o��cGQ�*��j��T�k���x%��S��A���P;	V��~\��F�*f���t��{���q�q������QG��62<Q��N�7������Be�~A���u�w��zn����tf���������4��G��v-/��#��T�0����.�^�e�Z�f�����r�:���~�G�)��q�5��,��w�Z�>����}�N:�e����hKKh���@IDAT�"��<#�J/x8��X��7��4�[����
�r�X�h�����%)�r�������$�wE��,O[���������:dng��	���lw�A�&=<�������0��9�\����2W��SCL�!����&������������6+�OW���^+:������E�j�:���8�����]}�o^��$����pG���3���g������*J������I�B�*��B����p���P�E�.��%<���Tbq�����%�������<���BiU��Y�������kTQ�U��>N�Fm Fp��u�cB��b�m)V��D)p�-��>6}��x6Y�)��K��P�7B��m)��!��Jz�����;��VB��W���{�����|u����_�*����j{l���M�<��t�]�N��F4*x[K��S���	�#�OX������;CE9��pL����5|
��oK`{J�++��e����SI
 xf	=}��}3T���.���J\%�����h�������%��@�a���,��W�
��������C1b�!��-��oW<����>� �hKC��J[��p� �����{��[bn�C��������$|����������6���jX��y�a�9
����;R�X�����O>����<�@�����h:��hg�H��E�P��#)�X���1���V!��u{�a>n�7��6�W�}�t,�/sEXo~��R�;�����I������7CU�W�v0�Q���"E)��*��S`���#s��=��y��Z�3�CdLj��b��{�9��f��a�����y�*�V%e��%];�f��0��a�1�A�
�<�1���sQ@Yo=�|�(mW����J
�%J��+
_�K��_FU��,�X�}eu�r����?5�[��t��}j/�"�>�J�{��1��S���-K��9�3�4����k=��LT�(��B�Q�s�����S��������l;�y���P��������p���r���EP���AN���� x��H>��Y�!/�P�7)iXK�,��C���{����f��������<�����}��X����0�+�Y���#�8��#0������Yh�r�*�
	��5D`��o���x�t��B%)y�T��i����Q!���A����{G��y�g�}�E�����w"�e��F�+mKv�=���}jh#��4jMSvD�i��c�`�}�(�����T.t��A��R���l����
�~�A�U��a��\[�N��V�;�8j;n)a�B%)����}3����d������Ya��T~���E������oo?���[�b�*��n������T�`�~#�}�<����#T�R��%��U�QG����w��[�����Kxm�JR���2�)�6�;<������.#$�u���u="�]D<�cci��0�����4,���=���W5�B��w��z<�o=�'�>������QR*!W%�^������}���K����_�v�9s�������>����um�\'D�*+��KIJ^��m�P��C�����(�������x�+���L��F3s�}W#���+b�N1���v��&��43�2��c��x�Y��z�h��c�����D}5��k�T~o�BH�9y���*II��t���BR#i�y�~��,�ox�]s��Ei
f�j�s�v5��V�;5�L����*Iy7F?VQ�w��RK�R���
�������������;��������="��e���v�}eu�{��q'����#gIh{K(-���{(�7�c]�o��[�$%/�1k����,�/��!�H>P*���X������$�w��V�����}�����.X����t$W�����[�w������2�*Fq1~��7xJ�B�j�f�)����qO���k�"���t�lE��8��#�8�$� L`�a&5��3WKh�P�������JV����(��QX����!����1e�������L���xi��h�X	��V��T�����D���F���+�_+��[�C��iIh�s%T+��f�������aJ����]�#��[��X��&������6J�� ;F�=���X,"t}�������V�;�P(��o�p�>������B'LK{Z�Cx��x��e������{L%Ea��L:���oN�Ee�C�#�I�?������F����J��W�Q%B8��/<7oFh�'���"�BB3G�XJ��!0��P��2��n&�Et�����C��p�(bt����/Fx:�g���("�����������^�/m�{��T��xd�R�p���h#��1�4K����7^$�$
��������=��y6FxT��5TW������k������W�����P�:�s�����<�j�	���H�6{�A��tQ�����^��
��Z����;�p�F!�T����c���N����g����Q*�������{�C��1O��9�x��!�[7J��f�rk�i����`� �<$��b��������g��z�7e
�na�5�����(W.6a��}�;]�N�/�o�7FV1���=o�4�����}be	oz����\���5�[��S3�cMX4��_M&_Ru,O�����-#I�]���������8S��5.��E��v�'b�r��3V����]b�Z�����k����K����
a�u}^'y��V��?�8��#0�pE�kP[�wdo�c���^�0�+��"
ZO�g�9G�*`���[���y��!���B�� T��^���Y��)*�jR��n�F��cA����P����P@T�e�7%��(Rj����t:�Z�8�:,#X1���g��G�K�TF�`�]����*�=�~<�������O��C��F���?.F��x����d���awE�t���
T����x�(}-���?S�ge�)|&U����v,	����E�*�C�M��h�e�1�x\��G:�(�w!���`�{RE������Dhn�kxn������]7��E(n�uIXv	x���}M���U���dB���J���D�T$��y��d�G���p}F<ZUQJY��b���m��R+<)*g���>U���h
Q4`��~m�I�vS���f���<�j�	�WdPB:������Q���^���S���!T�������
���cy������0~� �H���0+��^o��S$T���=,�K��f(5�\���Q�?�(�F7CU�^3���v	�oyw�q/��Z����v�}!^��{ox)���+Y����:|<�m{��)�5
�k5��kBM���mcR�%U��T�}�e$���Y�i������bE#���2�7J�\OP&�
���2SV��J�F��l�v�W���<.^����C���1���������;��#�8	W��������E�2�2�.C�{��KGh@:�^*eT�DU!���V���E)����Y��Q�JXg�v���1ar��?���"���3�zJ�XT��=���GO?�Tf����}��%��+h�v�R�\�'��%�Ly�U����a���{?N?�5%��k��E��]��-��2D�]�O��X�1��+��y��w�oN��q���!\��xV4�(M��my�	�)��b����2���E8�����5�)���zqj��X�P�h�`�c�\	/�(��ow�>��P	%.!����='c�����&k:�Db��E�#�!!zh����x��	VOAi���=����x=zT�Cw���l�(��^^���(cI#��D�ZB�tV^���R�i�\Q��������|�����=x;?���~��^�#U�j�]�Z�(�~�|�1j�X^6�}�>��BT�3�����{_���zc^�9'%�\�F��_3�S��f�[/����J��2��c�
k�K���v�}�:s��]�^��}S���X��#�~�}g���.[c�����[��T���U��o���]��6���~�����������,ub=���G��%��M�-k�������q�$�����#���*�[N\t�EU`�gG�p����Tm�ye�e���B,�6�a����OD�	�����`'���Nu�
Ka�'����{o�pHXT"�?T��y]���Y���F�	��
����B���4��$�������
]I;�x3�&��F�5�n����_�5H�.��	�YI����_H8k�[e_��@������yk�������X��#k���{�|��'dp=�u�u����o�u�z#��w�^��K��}t�PE�|���+�_OQ��� P\f���P�1#
l����{�?.X�(:Zt#c�k&�y������v���~�i�e����T�������<�j���)�W�z�1D�Q�~�h<#���Q��9���<��w��b��������8l�g���w���n�����y;�^�2���g����mc��������o��gq���F�l��hu�������2���Xn���w]��}�e$����:]G�s�Dga��g(z�/��o?!J�u�]7�|���U�b��,�x\�)��-���4�(�1��0�c�X'G�pG��/�Q��=�z�!�e���w�����*k����k�BZ����K|e�4-����4CM(B���#L�z-$�������Y�����w�~u�g�_���R���0�W\~y����x�����[��5�\��D��/a�-�+��F=��s��m��p�{
'���H��JMwHxN6�5������E���W��Z��j���Z�2�c�4�N�g��w��pZV����}��>�����(�	���Q�F5\�T}��w�9���������p�u�geYY��un������<���l���1��	��s��KBuo������(�����9���;��VXn�^{|�5�{J6}}��pN?�pf(Y%��C�D���
���0Yb�n�S%����HV���V��c�-�pBa[�_M�������<�j�I�aQ^��W��c�zE)�6��M�����U1�_�������Fot��������?�=���{�T�G;�^�"�z<�u����PFe��N����+��/��f+�+�2��k�V(5_��Xn����};e$���f�~kK(�1����f����y�O��<r��&��gV9J�h��������>����F����n�qO=��la���l�]dMJD����z����#�8��`E��}���I�OK���]�����*���x9��3�?��k�=�����
�Zr�����E�`|u�:�e��o��L,ZFH�F%��}�C���n�5�(E��
����#�=��[�'�b�-���Rk����M�oE����bJ{��s��J�Q�x�V,~c!�����S3e ���?��wm�]<b�S��7do�#�8�-�J��my5<ZY�Q)�*���S	�Z�l�ZU��O��c�9&���^k���n���?�<�\��~��{fr��E����{F����?���b�������yn11������������=���52f/(�]KFt;-���"2��$����(I��(��\�bH5����'��W�yTQ��s;{��1�w�^zi����W�=?��.������5��c�]ZK9I�Y��Ka�i�\��I��^�b�z��>d���������O����o��g��-����,��1vX��|I+c9e�cV*��<S�HR~Wa[�����R�9qu��������u�KMt����TDA�]�`�]�������b��+����
����t�D�*��7��{67���f����<���&����d���.Z
�Rx�Z%�%���FP�F_p��)�)�����U*<.�/e4�%
�k�����.$"@� D��e.7���Wd�92����������_��lz�Z2�5�S6u�S�Z�B�_��ao��E�d��r�K��l���<B��/����Sj����k�<{J`�������$B�����m���[������=ep����
*$v�}�t�p}v�)���"7IR�.����hQ��)Iq�*I�l�B������<�t�������b1������n������F�g��a
�BWs��|0�Y�5�8�i&C���@��#=g���k�m�\�U|'����D9�Q�*;Se���������Rt���C6���L�����>�Us�J��:LI���dC�>W$1$5�d�k�e��^1n��4xVx��h"� o��s��$0���vS���}�=���}�����&�Y��-sO������u���P}/���]9��J����$�t�y���B�/��J�x�5�[�JvM���l��Kr��N�;��OBF��w���2�_�����(����*m���'N�T�BN���=����$��B���>������e�.�:Yk�w%D3�"@�(G {��Z�%�B���n���%AI:��O}OP�
Z���\F�{m��H������r+x������o��E�u�����hB!BIjC���F+D�B�|�������(��n5y$��}1t�(J`��<���)G^��uQ��G=3R.��c�i'�_ GJ!�38���v���	^l8�8�j���#��L�)�!j��M�������?�?����$���IW�6={�(������X����'U��kXS�����`B�����s��|��D�?B�	�7\������P�P �;����y&e<�� ����c\�fl��������2��>��
w�b�n��^z���^z�e�i����������������"��R�Z��](|u��d�`gC!kYl�K�k<bs|�r��`8��e���?�/������D-u
�`��v(�������W���~���m�y �q'��IK�{��z��'���zhJ��;B�&����D��wK��y���RN=���T3�;���sO1�� N��.T���=Q��3�?��=#��m�@��4:�|^;/D�eI�����`��3�o���RpMh���/_��X��%��KF��w������i{,d��oD��m�b�[E~T{�������?��CSX��q������q��9]����'�r�_�}YE�3�@G"D�"��!@Ei	�������)������z?d�=��C7X<�4\-�A��C�b�pB��s���^J9�`L��g�*���B�#�~����a�'H.�I�Ei��&�B�\�4~�x�R�������#~y��#9��=���L	�$��;�����R6�L���uP8?)����/���[�E>t�u�y�������S��ol��x��S,�����?���m'��C���(�� hyC�*��%����*"���yI�H�-w�wo�������I��Z�=F��L���6��sZ�h��8@�����c���������<�c�Y�=�q�U���z�/�$�Jb�n=+�<h}�q�����w�y�z�!���W]�O	9|�2��z������-X�����~����<���v��h�q��e��m=�:G�#�����J�0 ���2��|��<������,s<�q=BdCk=^!�w���V`5l�0=�v�������/� �8	!�%�^��C�E(�R'(��x!o?����B<�g�JsER�@������I|��}��FN�ao�0���0�m��SO;�
m�rg1�XD����;i����mU�{��7q��w!�^�m
���[nI9|��wV�0��F;QJKT�������_
U�xF��,��	1��F���5a.T�$���N���k�B�H����}_c��Rt��1�
�$!YW_}����������o���k:����1������zCV�s �K�B��_%���41�C�nV���H��d��z��8��AF8D����j=�- D�����P+����C��r7�Zx��w�� ������DIj���4�}t���W���{b!0Q���X�Y!��"��5w��%�-<��G�l�p�{#�B��C_I8[K�	�}���E�K���I��������'UX�s���	#�wf�c�N�@=�v!�c�t�(�����C���<g \�(-�i����
���8����y���p>���]r�I�8���
5�>��0��)���(���O��)������*�����^����n�A�OSx����`�/0��M����}5�
r�@y�|(���
�`[�n�{���)(����&ES,ZAV��m����!�����{J{x�[y=�����.�8�GsI�y`�P�vDdXOc\�������j#1��
AI���*�������r�t�(�����{���o�W=Y����
�~�(�n(���W$�(r~#�����E�v�9W	������
�1{�����7��a,1���c�n�R�:�
J��t�x�
�P�*�V\g���}��9���N�[�
A��bm���2��{w5���d����4W$5$1�d�5��I|��-����W����"��5Ia���Vy��E�t�M7�_�g���bA9�`�P��E��*�=����0��x��^6m�S��>�"�`����h�a-��VyW����}KI>o�1�����J��{���<�{�3�����'�,�e9�k��4_��X�6$��KF��w����^C��!K�R��q������j!kq;wfSw!�>-���w���I����/��RA~W���
v�yg��bX���y]h�D��DI�WI�����<"�Y�q
d@�y��3�p�+
j]���7 D���z���[=T,��W'H+O(��JR,T/a:�0��Y%\��P����*"Q����E��E>�CEY	�
�>K`����ABd���%,���������h������C�}�����ar�A���+��.7�0.�(N#���;w�,��b!
kc�<����!(|��z�BA%)����g��y��-�D�����c}v��M�>]z[`��� J���K�Pt������w�W�%�����(K-AP��x���sJ�|�z<��2�9(�o�0���7�;�bPIz���1V���L�I�y��0���(�K�J?�F��9O�*��X�v�<��C��cKPI��p�,�����>;��0~Z�w��	�����z���mQ/\�U��$���!�U�B��(�����$(p���c�oQ��q^���Bi��zF�,�Mp���F>W\~yTu%w���N��4�a��P~���
�LT������$��L�&u>��5��{�u8�n\I
s���$|������.TI*�8��l)i���_���b��Qxe{�}/�6�)���JK��1Z%��o��)�l9���������}����5��+�g*I�g��U�y��d;���I�����$�]�}G(���}*)�,A���u=���{�9��-�>�t�7,AF�>�zU~���2�`�����g����]�r._�*I7�a9�@�W���M�=�-�n�t|���O� D`yF���y�`B���0�QP�l"X�m-yd�x���i�7B��'^-IX�0�;��6}�Z,���v7X*!c��5�z�n������e�?H,{o��k/a����m*J��D��U�����l,�:JA�O���B�
���:[�m�}����� =������lx�?��pg\�c������'V��PnP�Gy!���o����}����v���D��y���P/�������V��7��J�}%��Aa��:����L��{�L�����
�<�X`��KPT��#�����C�)�����5��gV}��J��8fgj��a�A��'lO���p����]>T�oN�	��!b��1�s���@�g��<�rq������OY #���.;���5a�mY���{l���A��}��==�[|{�����/������r��=�������mO�v�E��D�XIGy;c�A-|�Pv�����
���G\�p���
�'K�f�����3P�Nr1xyX���/0Fb�k17�*����
#������l���T�Em�����qys�?�Hae�\{J�!������R�+l�������$��+N���K�{�d�}X�I�{���1����_���-�$�Z/�����P�9l�
��] �C&
�>i�����s�a��t�OT����m
��K
�n���nm�n�O���o*X?�a���8��=mS������8�{�K�$����8|c�����m-&o�E
%����n��E�i(o���k]������[{���[�?H�z����^{?{}px����:����|��|e$��L���+�w��v���%����yR��7E��`�Y�P����9J���������yP�'�pBh���X�c>MW�A����#��|���v����C)�*)��d� �����k�����dQ���2����tFh��'D�"�� �R��-S��
�d�o��w������*psJ�z���Er��x����d�O� ��A��@x_��U���c�\V�2��\�`�2�c�DB���^���/������x"��<��h�|���zX{n)�20�"�������z@XXC��_��M������D�
o!2?+�L}�>�|�M5O��s��	�����H8c|Pv@)��]�A��]C�L��?��]g����o!���.����E��u�p�J]�w�BH���V�7�u�Y��&��_Y �CVsN�mw�rI�y����$LV5�~��YX����}N�����G�U���P�[I���=�B���{��3�}��-Z��
����\��`J����b�d����<�����1�y<��0�=�y�!z��ey&s}��8W$5$1���'������0�6`�A$�sF�AK&~V���-fUa�	bV�9x�\'��rmG���|	�S�k�PR��oL�~�<k���"k��c/�x�oc1 C8{��D�O�s
E��T&_��q��2�W��U6�k��D�q��s����g>e���#$�]��*�A��w�vJgL�����WI�������gb����tV�������/�ug��� D��U]�GEi����%}�3�!��J��U�4�/!U��M���$"@�#����~�X�n ������F�5<[�UG"�t�R[��D�"@� �#�(��f�@�����p6P:��S=�[��- D��$���R��M�7���@N@f�����m���v�����
�6��B��r����["@2 �-Hhox+�'�u���x+}jWz�["@� D�"@� D��@T�_z�oB/#41�"@�G�F�a%�C�Q�^q���
bx���?�8f	��]|qdnK[��D`EG�D��>E,G7n��-!���DQ�.]�S��H"D�"@� D�"@J�!�!����������*�7"@� �PQ��U?���9s��I����JR�I8p���|9���G�S�N^>V{��v���9aA�'����$"@� D�"@� ���*����2�VB���SO�On� D���(
���������>��wq�~n�u�u����<������{I��6�=���^b����w$U!@��t�R7Cr�~;z�;�������>��?��-���U�r<��"0x� ���%�d|&"@� D���M�4�n0u�����5"PeX�`�C�l��8�(R�[e_'N� ED`�FM[�W��������X�^D�"@� D�"@� D�"@� D�T��z�j�
,�E� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"@Ei��6�"@� D�"@� D�"@� D��B!@Ei��e�D�"@� D�"@� D�"@� D��,T����a�� D�"@� D�"@� D�"@�(T�
Y�K� D�"@� D�"@� D�"@�@�"P�d[��"@� D �W��j���j����W��V��R��"�|"������K�����-Y��-�w����|*"@� D�"@� D��@��� � D�"P�@AZ�^=Q��*����D����@�j5����|/uEY��-Z��
�Jz�- D�"@� D�"P�PQZ����D�"@�@��S���_?��XX��a����/Z�b=<��"@� D�"@�X�`���"@� U��u�QIZ�_![_����"@� D�"@� +T��Ho��J� D`9C��J"D �-��""@� D�"@� D`EA����M�9� D�,g ')��.g/��S�����E"D�"@� D�"@�����+�[���X�V-��Y3W��h��o�-�p
6��r^G�(y��c���Il`�D��V�|ml4 Y ��f`�Y�n�v�N�6y���Wt��V���"P�F
��W/�M�����y�x��X��j9{��?��M�6��;������l���6p�;n��0i��f�h�97n��s�q��>FO?�l��'M�����M�����r�=�����c]��}+<#�l�m�������|"P��3<�j���|�>�d�m����/a�K��].���U����w#%���W]���X+�C�Zo�����������>�C����\�].�������7Fb�����s�'�9n;o�Jj�J�gR�(�����@e"������&�'�z��8q�k��}e6���r��x����7������#d�GuT�{�Rhp&9{�6V%yT�gb����%*�}}2x����C�-X�];�(��I�&��R���x4h��BS�y*�����Tm��i�Z����[����;��C<o\<�J+��N=�t��OTxN � �@���[�N���eK����k��]�@�eD`9@������'��P�zM��+��j58�U���V�i�
[��mC1��q�����Q��k�mw�1W��7��k��.�����^��[��2�U�z�!���-�U�sWT]I����-�r]v������~}����?���U��bHPO�����������7�d�7{�Wn��������sv�M�]����Z�p�{��������8�Hw��W{�-�?����Kbu��x\x�E�d��5�y���N?��x����S���[�� ���!I������*�BUI��X�j!@Ei%�����E_%6�$n}�}��(I,X��N������}R|���Rx���)�����^�n��9�\����gw���Wyj���D�@��������5�t4m��������9��wo\��8������'!�z3�H�/�{�[��bW���c��������������~���+I����~�?V�j�i�RN
���b$P�Qk�v�������`���loJe����F����:�����*m�zM���[��}T�����N�H�� ?M���wx��B��r�)�M�h�"7z���ad}D�T!�(�������etZk-��2A��zk��������a_|�^��������>}�;_,�HD���l���n����@Xg�u�0�W�^2�1l�0�\g����%yb
I�?���.�8�7b��B�.��/�������
7�0�V}J���h�	�E�q�n�e�m���Y2���l�����'!�z3�H��3��W/r]�\$
�%�����pS~� ��q�Q����l�~cW�N��{BqWQ�r�����r9�J+�gK���%>���<���K���;����� �1T�&�jn��z��[7���7>�����do �-��p������zv������s�c�!D`�E���J|��m�I����{��}�=��3�#p����,[�O����	~����$����y�$"@�����J���4JR�e
%Du6�Ie=c�w��w���*������
!�;�Q1�D�)�o�D!b���"p�Yy/����;���@>�r?Zc�c+<Z�������p�gN�p.���?���k�T�U�5l��A��n���S]�\�����~���;+P��.���X���=�J��!�D`�E �<i�}\>X)"�<�*tE�^|���}�~@IDATRl&�D� D Q;�0���{��}�&Z/+#D�"@��z��"�+U��Zu��W������5C���,v�JT�?~�F�w����L�8���9Y���D�����@�����;D��(]A����%��5�X�}���n�����~�-��o"�2��w�:u������G�ca^s%�;h����^�z)Ul��vn��Y��?���}���)���H"����1Mb���w��H]�t�+��M�6n�}�q;vt-Z��0E�P�a�����Y���Uf��~p����B���k��;���'O����6�l3��U+��C����������k��������y���$�D�����SYn<�����	�~��>x�}�x�b��t;xo[�;]]��O��+��o�9s�x��|�2�����vK�,IW]�s��Ixy!�����l?K��(�m��$��2;�1A8� ��!'��rK��x|�9?��S�����H���r������%a-_�5�}���KC�p�����o��7������e���>F��U�~����;����~V]�}�u����?���{�����'�1������b�M���E�#��1c��(����J���|��]]o����=
4(�;F�0������������|�xC�qCd�����g����G���
�X�c�pD����"��v�o�a��[o�O�����<� ���n���x��q��g�q����-�4���+�Y�f�xW���'	!Po�n�����vw���	���4�����'Iw��MVuM�m��������=u��=e�[<o�vm��+�F���N-�7�������u�q�����G���q3'vf���v6_c+���z�n��n��^�E���(��!���_y���h��V������g��I�����i_{����P�q��������K<V�����I�z���l�M�{!����_{�q����e2�d��f6s���t��t���V��k����o`���h>�/����p�^��6Ex����Lx=�i#D��Z s�|���l���'<���o�,\�N?��P>8_>7��u�l��g��>0������������������_�We^�K�?8�@���k;�1F�s������]o��s]�s}��-�T���WwK�����?s�,�����������<�������l5W�i;���Es�������}1v��1b��9n��A2�����}��jH�T���%f�7k�������W���s��_F{���!?d�5�����������y���I��2�Kr���D��0�V�9�U��]��]�&me��fM"��[��[�7�����*����E����p�s�W�*�Ot��g[e����'In�5�8�+�����L���0��v�m#�`-��O���a�C�X����<��K�����l+N������(�2�\��4b��A���w�D�C��M�����o��.�GD���.����� [��-��<���={z�� ��Z�`��|�a����L�w�g�h��������e�	��(�����O	���o��f����-d7U��w���60��m����;�� �����Cs���8K�v�db��vyc�2��H��x���y�[95d����y=���;�;��1p�'����_Yt�6@q�|x���{�-�D2!�R��-�9�LW�x~��6��9|X�5���zw�����e
2��+���u��Q��N����r{��=^vQ��W/w��X���a�<�L7����D��	$a\]��v����nr-"��k�8�
=��O�m{����V���O[��fh=E����-�j�v��>�+l�9��'3(��<��E���>�	/P���0�N�[#��`�ax[~��I~��k����'!�A��y���)#����}�+J_��gDQ���O��h9l1��-�w�LfQ�>r�=���O��x��w��A�'L��'�sx���sC����r�������Q=��o7R���w�I'��.��"����t���<���#�NBi��(���A����.�/���a�������a�G���I����+QJ�-B� �p���P�XA1��0q�����,��a�jDq����o���������p�_���1�piS6��>��1F��1�����c�m����B�y�Q��+������1� ���Y��3�������Q4@��s�9'�t�q;�E�8��Z��}[��*�m�06�|���p�>��v��],k!�s(]O�:��4-�m*�rU',N�Mc��Lq���������bFK�&�9&��a�DZ~�Sf�������{��)����!����"�p#	�7>�>W�n���E)��}��IB� u��L�V�S���DI:���n��^w��u�F��F�~�'4m���y��P<HD!7���+(x�"��	��A�'����]\���y���zF�p��`��w�b�R����W&��R�U�lov�E	F�E�;��s�?�;�XQ(o~���7����<���#�������{��/���F<F����c��G��>�>�fZ1xx�]A������=��^1��E�5H�mE�j��q�!!�'�G� �D�U���]��
����	Z��{�1��L��-�5�E��L�>��'�o�����������W=�=�e�~Q���7s}���O��>��*�8��
��A$x��}�f/G�.��~��Wd��{���;����?�Z�"��7/�o^�����[o�e��A��k��z����fG<�Vc� aL�<�)7�����\�
t����;�x�,����^*��6:`Y�������6L)f��vSQ�v���
�*q�Q�b��m�[)u�����S��T�:0��G���Uw�H�N����M���7���*��ae����W]���xE��G�����o��<��1n����V���W��<a�w�7j�������>v����n�����}��~�$w0o���o�����-����QO.����*2u�sdO����;WY�V�����Gb�&�/yduQ�&�-��#8!"�8PX�B�Qv*��8��K"�SD��]t��)��\�a����t����uQ�&��o��#?�R����g�������Pk�8����_��v��k����z,_�!i�2^|�%�	I�h�0^;�������W���'�t��(k�
dh�{�{G�H*�����]���CA
��T�������/��8f|)��(��!��J;�;�c��Tu��2n��8����������X��b��Gy�{B�(%)��xR����	�����E(��((����w_Q�a�5yj�\���$I��@��b��P�������n_�1��t���L�
F`v �Vzn����m-�zF0�{1��W�L�����K�GEH�'�����-x}��s��P�=�����C��B4H���?��0�X%) 0�g��X�!���C���B��J��aJRB������<�0!�n���c��(��o���$�u���@Lw.t�x�t����������y�]-���1�����(Y���|�}�s1H�$E;�$�R<+������a������[n�5��X��]�5�{�q���@�uh�� ����IQ�'���d��q���E@Xk������UW]5���,y�O�i����F	�
�v0�h�!8_(�]�	Q�>�&��#�W� ZB&B�����}"�Z����7V��F���*����N�:~��t��D�8�����o��w�)���y:X�K��E���w�|�#�m��q�wx>)��\u�U�;��������#\���$B���E0p���7]_�{%��&��
��%���v�|���\�������-�~����z��n3��]o����1L�:��P%)
!�W�=���aJR��7�6'��D�y�N�Nw����g�c����uq�m���]�$E=��sk�c��k��[m�r�t�K��#���J������t7�)V[+4>�����*�R��|���OG���4�\�k��#P����Q_��ej�w)xz���x.�G���'XxA�a,FX{|,�2&�\,��c*xT�y�I�O��>H�+���8��P%)��X�1��vg���`�6<��P%)n�������eod�k��v��m���JR�	E��0�I�
=����bc`�X�vo����a��0�7o��g~"�����9�#��*IQ<=�M=s�P����<��Oj��R����U�����/�'���0�]z�e�N�������}Q"E�'1�A�w��7�����s�Jd�(%)����
�r<��c���<��D�BIQJR\^�:-�C�|��g�{��%k�t�&�o��5�]w�m/-�>���J�������g�}���d��!Bw��� JI�:p��e�m=y3��}P�*���Xe ;V�q"���������W]t��U,:��=�����Q2_�g������X�B�`�3g��B���H����7����A���
��������
!"��:D	!Grd�������k1o����Ex'x���i+Bjms�<Kgx�%E��0g`���/BD��c���L�$�z"@E8�W^~�cD�F������Jg��O6U�"L�p
Z���t[`����x"<1L��L0�G����k�����,���K7����-�P��K����.��S������"T�}�1���h��C��5�����������)���*��{����Q�1P�au���	�9�J��E��	V�q1I<�6�B�;�������s���	���oE��}v�2�M�>�V�Px)�za���s�j������0���X_�e�E����=V%;or��'��%~���oo]T@�w��qA��$��>Q.[c����������"����[r�i�y^�q��q1d��
oE
��w���(Ey$��U��1s��/��5�a��!���n�:����(";w������t}�<��m�����a��{�u��2F�u�Y��2KEX�nS��l�������V����7�h������.�����?(�����,Z���1��C����3Q��S��������3H'�X�1(S�����a��q��E�p�����4{��/���v*ui��-�����y#�2�t��-~�<p�7��VQ�9����,�/%�s���&�9�E�@��	�;���@��6��`����������kZ=��v��{�������	������{�x�u]��zy�68_C��v������c���#���[�+!<<?fO�y� �ck�(R�������6���]a���2��3c��w��B��Z����cUQ��� �Q3�B�6l��������\�����~���)x?�i�^�y������g�9�3/|,B�j=(o������E�x�`]a��x��n���2�P�}����R]BN���8�{l(��n��u(>����Y&
}���w�s����+���(�m�+���_�m}����>����i���Y�
�z�����n�XB���J�&�H(YD��\��j���/�Nb�a7z-�3�+q����]#1"Z�`�W,	>�o�V	|����<^q3���_����5-G�|�x�Zc����>vs�7���� �L��A��;e��i	_P�)RG`��yD�R��M{�8����b����}�lu�)
'D�9y�7�#�x�U�y�j���xl�����1�����w���1�S���^x`��J{�����3�>�v1J�y3~t��}(!��J��S��\�5q��7=w��o!��\���`�6<�n	
�l��9��6����^?m����a�r�f��n��_v�\�~^�xW���>k���_<�������o����i�����s����j�T�11�o:D�u.�Qxq��a���;�|�����T�q;��1W�:�� GAt0�|eyZ��*���d:��!�2������0_z��>F��e1RF:'�H�u�	8^(����m	����*��-1>�\F��
t.^�} �Q���������o�Jq|��b����0��V�^�z=�*��S��R��NZ&j>�w	�	"��@f
*��U����$B�U��g���8�rX�� ����*w�������a�VT��>}�����eC]H�<�F�k�k�h�����PQj�X��@�R&@]�1!| ��T�� ��M��?
KU\��C����q�����+
��*J�fAsm���VN�_w��o\�Q����	!��6!)l���#���k�8�G&� �BZfP(^#m�H�P��qT!���4�{����9���0W���6�<&T��g�^(��Z�8�	�����/#�r������;X&W<�����#�J��E m�8��Bzl*
�m����e��+��N��C�
�lJ0����� ����x+����|0��'-�[��@�
K`���M=���C���(��<0E	��N"��<}Yr��0�+|��A}E���w���&��?0h="^�jpA#�U4$��Pj)��Ye����A0��0��
�����R�2�:N+�5E�
�����$�s0tB��@	9o��9C�KW	�|3yV�a9��A�P�BY](���1;�a!����^>d�
��~�a��=<'��C,J^.��ae���(nQ�*IK��v=+J��d���V�(��Q�����e�p�8��V���:�|�L���[P9�L��8�#4�$���� �O�)Q�p�����]�.���uIl;����~xG/_0��sP2v��ZQ�.3`k����Q���0���z�'����}}�#NL�������Ny��L���O��9%iSQ�mq�S����.��E9���T=Oy�Wr6���P�����~��/;P�������	/,�f�&t��a�Z������^���o|����U8��]�[�����D��n��}>�k�n���/�r6�%���E����w�2����O�y��p��(���Qy�-�f	� ��e&x� w�x�u������e2`B����`!HI�H�������2a��W��2������O��-���,v���B�Y�������cADA
�]%�)+��?�n
��e��f���?��l����^�{#1�@�e�)��u�J�X��0h�^��:�]?�����M�[��%n��Rn����L��R<��(�����;{W0R���*6Qn���t�����?�j�YV{n���Z��S/?�3~#���2[C*�!�9�u�}���5j�v��Z��^�L��G���b����p���`1e����|w����_3'�����9���V�
:�)s%�-�#���k	�*"0������4_Y�������y�%;�a�=MdDH��t�x����c;l�}��k~8ht6�l ���ZWp�y�z��*^�V�e(����R�HG��l�a!�C����$r�0�U�
�p�04���AY(W����A�! �*>8�ux�r�7}�t�����#�+��cS�E"+BT$%��!��z�R-l/�3��c0��M)�`1���#��{�&-!5�*JE�<,�.r�j�F�w�����e�@���X,w``BN��
���Q���
1�%���@�R~T����
���+.k�����Bxn��`�T��>Pb �!&N�S�rK���� ���'����Z��2�#mrq(q��^lVq�����_K���r	����"�t�$�q�(=-��"SSaC�N�<���e�����}�#�mR}|�AG�P<A�j%(�UY������j�E�v�P��B����(�=���c���Qa������V�x�{�JQ2�xZ��UJGP��yX�^����Q��D=��RU��	9T��Z���&�Qu��
��*I�����8���w9�$�G�
�B1$0�{�7r���y���7����P��U�m�����*IQv��s�����Rc�G�)��1#�fj�_PD���v�]����1}����3(���B�c��B���|c��G;�p-a��
;�E�4�	�K���Ta?�lx�C�CP��/�����9�8��X���`���_���L���;��Fyx�� A d��w����xG?�D�%|c��/XW�m���>�����o^��{�j����q�!^���?�����!�;4EI�u�g��r,Zu���B�_=sb����L�Z���
�p9O�������1}���zz�n�����TP���������S	���y�g�m]	���EG���o��$���_=#9����!o_5�fC
L8�E��b��Z����s���!�	��R*f[��Imm~z|�P8��y�PbyU����aJR�M��@=/����G��T���H��P
3��s��6z<��JR\����@h�t��!�������
JR����+������B�O��>�RM<�m����~��$��1
���|�Z�P[�wA%)�5g�WE�|�o��|
[��h�_>u\%)�����$Z��Z��|�����;��B�9���^SL��kn].��'��GLQ�j9l�Yi�����cq�
[uvuL����/�T�����)q���}�������knu�_������9��r�#&5�� �WPI���u�%�H� ���V)	Y��e������<�0�M����b4�4w��P������Y�<o��zm��K �d	A�l��nD���
���h�o/T����b�f����d#������|#���u�g��s���$X+�,�A%)�9a���nq����JRT�}���z�b?�>|+JQ�w�y������D�"P-nA��z�,hV�b�J���y�`����!���~&�&.;�j�$��y��E��e \���z���\��i��Jl|+���B���JP,j�
x�(u���`��@�
e�����q(�p��(*t��;l�Q�%0��I��)DHF���^��v�����a��X�}��6$����0�j��	�0�E��.���C�(�����B+X
j[�����*-JX���J�<�P�t�b��C�6P�p*�.}S�����3A	��������M�����re�5@XZ�M��A{��;���$�����Pj��F��-0�F�1��0��-���S�"3X�`�@U�!v��y��$�9�mDY(����[k��oI��0� ��z`�c
i�O�(#�X��te�9�����9�G��m&,�C�%�1�]��������{���|��!AhtJ�;4��+���^B[^�0WH����<�#*��u�
���O�U�Y|�P�^o,�/I�K�}u�����|DX�!�2�08��Kx%(�����u�^��wF��gO��x�m�5k������]�&���0�^�����]��z��(�7�|��
�`yKgL<���Bt��5yhh���TU��d:�8��P�"��Ba�K�^�*m&��8�^<^� �
��2P�*���U���g�-�*A��,���-<H�]�7B�b�U�������o/�����E4��/]dx|GQ|�~^����O�=��k5#�
+u���?P�����z-��,���8o��tN�N�+�3v\����M��>-�� �e+���������-�F�}|����;?�J��f�/��6�Y.����s���OH�][	W�j���M��mE��f�E�4���]0����v�c}�(�����������_#��!��B#|}��mD�����^q�}�`iy'�[k�#����e����E�
JJ��U���(���:T�"��d"V��������a�A[V����Ri�d�Z�z[��Kw�����E�*%���a�
K%x�������g����NS<���(���� .�o��O8gyR"7N���y�lT��
Ld3xO��@6���0�V-Ff_%���JP���8��q)L���t���?%�;��E�c;R<�4�`�d��^�o�6p��t/KT���O���S��`!��U�RU��(�v�C)��`f��Z�(�=�
��t%��]�
��*����FC�F�Iw<�>	�J���"��t����RJj�B!%6r��l������W9ag�S�P�(Y����t0����S6!��z���uAZ�"������Z*�O6c<�a�_lB~U��[|���=������F���L��'�D�(���[Y0���?B�Zk;�w�����x�"x�(���m=3��s�>a��;�-�-�`�Ta\~_��n��E����BG[�b
�i`���v!��+��� ��gU�c�������oy����]6"D�'4B�S��1�b��Z<�=��Dx�0���%kh��r�����`�	��z���x~)?/��?v/�#�� 4o-	�X�^3W��`(����=��U�y��8FK/�#��a�h��a�C���gMGH�UK�Z�-�W���������tM9���q�A�5������Gn��C o"�7�}3�������LK[_1���!Y�\M��Y�L)-`��p���6))>"X/~�U�t������G�/!�����C4���iGp���i-���]���c?�;,������ ��W���!�_���{�	�3kR���)��.�O�(�?k��Z�Z�~!w�aM�}����#�9���V,��B�9�R���	����*gO�6�B���~w�$�t^$��6���Wa�O�A|���~A�oq����z:r�<���=#�<����w�\�~�j��������P�����~���+<���U��k.k<�<�JXX���: �=��#��;3�����>��a������x���
����-XG���|m��fez>��*y��_T�5�;��l���P<��,w��R����O�k��A9K�<�����E6t�(����������}���l�eY >������0��Z	BV��N�Gm���F����U�N�I/!���&�c;Q@��"�|�3)�0�@�����������C�RU�nQ:��"(�'� ��=�cG�H��O�\)�Q�uMZ���Y�D���Xd*<�t��7�������ZB�Q���}X^]s��an������l����l��SJ�L�
�!�:!8��(�L?<Q�3���E)<4�X�\�x�P�_v����'�n�|a������#�t��!��-S�*a�����vE�(��8�?�2P�XeR����0
z"G%����NR7{O;�Gy�#?FUR��5�>��/OU:CYi�6�B	�D�@������+��6Or(�O��KI��\�0�xLr�@�	����O�����M�cy��s�n�=�<��3-B�	��u�6'�$�O>���!�c;xm!~O��!�_��k�v=�z.�w����($��M7���]:�r����[MV�������K�.78���l�*B���{�
M�6n,��N�V�qKm�=�D�N�{�����Q�};�(���	���z���4�M��A/��^��b�U���68oa�����F��PwR|���\�k���jT@����������� .��%���3��%�C�\��L��P��b��o��6n�Z��?j!��������I�Xe��{�|���i#2�r����Q��^��
Zd,��@���t��t���=�'n	��M-����8�� �F�5+F�A�����������(���a;F����D{2��U}��LC�\�������#B[:J7�#rB�"��l3�?���^�K��"F���-������/�j �Mp������>.�(���+�z�������F�lB��c��\qv-V�`������r}�`t��,0�^�Q�8�dR�.��L��^�wU�c�&�|M	����m����j+���i� ��\)�k����k�E]���tB�����7�RX�"=g�V1��Y3��@.@��Bp�B��@Uv�8S�����	�&+D����v�JH��Qe���?]��sQ����CK�	#����
���Hn;X���L��S�sq�lp��L<�P>8���2�U�^��� �?k[�P#��}���Ps(��3#��v��E��0� ��#aN��;d�T(S�L��J�L��=o��(#��7��=�]>��v��3F����6�����]|?��YK�|���"��^ ���F-��F�\5h�kt��{w���x>��
� ������cL7�nS�d@��-������"�����n���
�YW�^S�a�C]�3>t[��[I��Z�!�m�>�e�u��\�v��$��k��Yo��x��o�8T�V�8�B�T��=O?N���z�����+�	O�V" ��v���VP������CQsD���^��`���P��V(%)�
�B*��Z5��w���{Q%���"�5Fe�O������V�Q����A�b*I��Q��d��G��9�of�������Ps���v���X3����~����]���G�\��>����������5Y�\�����0<��'<�b�3~��=7�����!�
U��Q��Y%Y0�J��HR���q��w��^��0�h�� ��X�8c=k������#�H?A�a2�����%B]T���uQ���g�|�]���XT��q��x���O�����@�?����`��
��Q����t�["z���j�)8_��������G�M��)�E����D�����gJ�%`(,������9��L"���r�7�a��x��������^���<��X��`B�I!�x6�!Y��\t�m�R�
��U��N�x������_q�v<&�7Xo�P���e���L�i���w�*��z
��Ti{���n�+������xp��m���
�������(c��%O.�PC���%E7Q����^�E�������hVef���~{���7^)��w�yg/,0�>-���V���d��+e
�X�����7c
|�P`�������Q��Q��jC���)���G���R����(�/�����-g���q��.�o��P��������\|�C��k��&�^!}�*+S
~d����R&�@��(3n��/�`!�����G7�7��u$�>c�����wE���K.��\�&mSn���F^��O��Ix.����/��W���#��_F{�_�5�!����%|�����_�v���������v���-x�8��bD�X���c��%�e����/�����}7�� !�8��m���)��k�|�/����)o�O��Y�{�R�2����'�I�pK�:�ER|��2������f���f���GI�j��a�5��YR���d�LG�w1�a��BK���=��$�z�m��+��RX�&�<Z������Zu�j5�8�OZ���Y��u�_��(�I�&Ww���o��������j�	���1@[��i�A�������C��W��6'j6�#���NM{���*9��K[fE;	����s����%�?B�������9���D�����Wp?�O�y�]��������mD��!U��'��E�����Bn�Hk���S�J�|�l�����*�������x�����S���yh���*��|������.��,d�H�cgv��L��i�]~���>�,Wf���]Y�X��
s�7��fIn���F��L��v��g�=����.�$�(��(��b���o10)P��Vb�!��k����t�X�Y!�&���������
l����������]�vu��$#�2��y�X�	^�V�~I�~���/��������N�7,1!�O�Y�y}�~�MN
=���F��(��Q?��{k��`q�	�������O���a[KH�Ae������~	��?B��u�Yn?Qp�B	a��_�G\����L7<���]���L�{~���R��{�v\���*E�:�-n��3�H����������^���Y������F�
���|�_K4�q����B��"��"�����;z��7���j�6!��y�����>u�UO�����������9
��E�U�s�<���e��������BC+����$�M�m�VQ�|��k�[A��J��E���Z�;�P��&I��bB��9�D��b�nm	���e'�	�0m���n\���2��&��bi���.��4	>s�U��'B����
����
����t��7gUn��>��:��G��m�B������e�/��T���6^�����G��w4���n������{M$�oc�I@���|f���I�9U�m���T�
�(m��"Xf ������[�$5J�+�����zVFO���_�P�,':KX�I����O�B��!���=�����.r�n���$Qv�s��Y��J���
�:(������F�GQ���9,��oXK�`�`�]
T(�!W������Me�;���VQ���
�P��M6��k����aw�M�/�T���(M�g��l[U	|cb���z(TBs�[�0Jx�A���t�D�vMW>�9U�hY���cv�g'��0������|kJ�H�R(�@s��uV�����S�Nr�n'�>J/����u�$�������:_���t/8x��CI�I0�O
�� <<���uc����^(�����������s����MG����?��O�8d=���TH����Bj�������]w�%�Ql(�3�S��Ey���/�|H�~>u������W�^����
��Q���a���c��ie�0�������%�~��bY����c�"_r\�'�32�&�|��fw�����t>���R�B�%�����Ba�h�����,tw�y����a�rA]e��_��6�Y	�g��[u�����K���K�v�_��$E��k�7�7Jx�e��ikl8?{J��Y��6�r��T�zT���������{wu/����e�m��B��
���%���8�-�v:B���b�t�D���1��b����F�y�h1�
S��}6Y��F�	��U�o�T�=o�����NGX���������e��/����������iT�����E�^n51�H�
��������7�z���~�N����T����X�\�<���V^=%���)�Xa�3�g�Y^0F�6as���\��
�����#!���r����aE�C
�9?��=~�������K�{��M����r9��G�����>:0������;l��K/����x��ga�����&y|�+/����|��hK[�f>�����"��Qq�����7X.�0����jY%h���
��U�����]S�cI�I���7BY����0��R�������������2��g�1"��Q�����7�v�PF��(�W�	�7B���k�BP0����V���	^�J=���L��4a?��?J��q"hU��BKV9!�
{�	���8�Qv-,�T�
�x�V=mr��yS��"��2P�rGz�������7�r�������{���B�Q������[���"x��*]%��Q}a*�n��1�����������s��w����b��Y�(�%<����y6�F�}>d������T���~H��P�I<3��j;^�1M��`;��z����J)����������R	}!��a��O�$�B����Ei��!��/bq�����{�z�I�������KC��cQ��rH�);���C/�� ��u�a}m���@��;V��{`a��Z/3�}�t�PD����/b=|��	��k���8�5�o��^p�LxA{�����
45�f�:�J{{-���]��i?Z@��B�^U<fn8�jI������?������I����i#^��B��������;�Z�q�@���{����6;�I�n}����:Fn�����G�V���D�*9���O\=������������v��gp�K$�������v?	>"�w�M��~D�����E�	�n#�q�i�}��W���R	���<��pO��z*e��u�o���)'K�G)�O��&�'<���qF*j�Q�v}�����M�7�&�[�!�1�B�F�$�x�uv
;z��6?2�rroq�)�~��������g�Y^0P�~���:x@��m���Y��r�o��u��0�n�}����wl�'O����� 4���v��@IDAT�a��3^S��������:��Od�`*0�-Qk�3%��0G#Z�V'-�K7���,�3q��<��s�����C5%JX�?Q"���Vct���V������B>����� g��E�Sb�j�����(I����\eb�����J�����;�7P?�����wfo�RA ��������Z��\�I���h�U%#@Ei%��R�=���e���Gdr	������28�Z9���eR!�����|RuAc0|� S����PI<N�0�!��4L�G����{�{C�EA�^W�C�2z��A��]����'�^�s�����U��a�e�$	���X&}+��~�\���o;����=��}����#��$��O*�U�����]�yc�D��������TI�����v"�d��������8K� ��~�0{H0�x�?|���
k�f�p����o�I��v��I��v�
���U������;BV)�g��m��Y�AB��]Y���=�n������S�t��z�f�� �w����?`pf\��o�����RA0���%_��Z������0#���-�}��SZK�+����_\�;�C�W���$�!�y��������9X��Y��qs��NG��|������r
�]��1TBX� ����z��s�����*��u�on��E����^8�W��E'/Dl����-������c�S��*��6���u�;z���X��EN�#�1��������)�J��&�< ���R�U�aK��Q������U9?�R8���y�����=��E�m�k]Pyo�'������������3�2Vo[�^�6n���p�;\�V�����2x��6�[�i�m�?k�(3:m{FJ�T����$��1��x�\�z���@�r1����x��_|1�g����m���I�vl��~�D��^0��)��lX>na-��g�Y�������3��x�X�����)k�;h�b�S����>-?~r��u��~�0P���G����q;^���s�H��^�{�$��C�|���G�E������7T����������e*_������S?�
�V��UC��or��)F>��w>"-�K��9��p��w��]n\��>7W�>�����j�k��>�����_����E����y�k�l��4I'�	FM�EX[��u!��C�@����.XS��%DE��,B�]k��9��V��z��a
���`=I������k ����k���|���\��I��(�#�-�3@�r��>3F�a)����B>����lT��%"�)���B��JX^)�?�N
k��Df[
�$���L�@�x��c�C��}���!��(������w@��[)`�H�J�J�W�c�����$nKX���E,�t���B1P�����
�\�z��=��^U�>��1��M.�r��9��#?��%	���)�y�B
�Z�4#�lP��4�#� ���l����{�>���svr�pA� aj?���B(E,:+
�G}��O�4/�>���������-yo���r�7}#�2V��b�^��'��yZ��y�N	JQ���B�XI���-�-��
�,�m���3gz��*�V,D���Q�1�|w�`c��!`������F���l	��]��`��qc/Y�)@��L�^�C�8�_�B��g������!�qO�;8�gF�������(��X� _����R������W#��D�O@0�/���&�����������4�'��W$,�[�tC��a�
�.����"�C���j�)����30�A���q�o$a��A��-�\�m�[s��oi���\��<���^.L��V���H�4E�Y�4���E������j	7��w$�����:���;��BNKn��$�|��Q���E�P�^M������=�<_2�K��O��K�A����/�J�����N�*�T��	c��c>��S�����{��?/��|I�<+x/�G|w�F���
�K����O�
Z�"(����n���B��O��.����fM��L6k��K��6�����
������a������<��+! �Y��m��,�-����k�W���k�����I��d�\7��[�g�������;�_Q�~�������f�:
Z�z"��4~���7�\`�E����.���w{�� �����'���C�=,%p7�]��x�q��>KRm�
X�o-Q�w�q��:�M�z��<�1m��2Py0���b���o����c�w��$�1���|���i�#c��/�CB�����n/1vA�����,.q���� �[(@A�$�!h�o�S��1�t^D�uo�<��h9w��}����!/���bt������X����J`�����a����'���$�}0.h�i��0���=oI�^�����s�?���?���}�S�wz���x��u�e<������r�Y���:����`���{9(��������3\����7/��)��l���s���3'�	�=�:n}�w��}������������@!xyju�'�v�������4����C0����\Qo���si�eN�<�jx��a��q�4E���X�{~(�0��P���u�U|e��"�������#d�y�\M���h]�J$1KI��l���#t�
ekp�X+�y��H}�ktY[����������k�u����!������k�������<���CA�����{��������8BDs�<<"%��A�cDYJ����LLq�G0�|��� �?���}������n��Ain	����{��D �q��j��*�����qQ�Y�B�����/����p>������V�����K�m}D�����!!$U���������yX7YE)����d�A�%�}n���e��O��d��'���"��L7�����z�)�)_��O�'�.��;��c*4	
�Q�~���Dq�]�PZ@!�(��
�g�EmVyl�W�>��_�O*��"/�9��p����kT�C��,\v��{�[��ki����FI��7��g�������y�IQlm����zUTL�Ps�9c�9����5b��sB1��"�3(����1���Zj�����;�3�y�������~;LM}u����>����������cb[$�g������&�����I������E��>{��?�5�9�j�V����u�v\e���*���'�'�����/�q����&(i�pc�)��J��?	�����=��r��h�0Y���*�0�o�����p��O�����Nl$|���s����=h�����#�:��c��"����E�I|N��t	�m�Dc���w��I�bwb0�EbxalP�t��zhf1
�I����Q��t����W��J��=��)m�����_��:H�ml{��s���9-<�L�
E���~���\��^���!w���;��R��g��s�5���"�a?	��5������86���n�5+q76��t�vI'z���g	��������]y��Gc�T��r������L]7v�3��a�s,�Z��}��:����������u�:�?~��<���������,���v_x�T�e�[%W�wpE>�nVk�k�7���3�Wmn��biGXE��������4p����OYVN;ac�y�L�~y�	�����}��d���,W��������v���"���hM��ic�����n���_�@����8����H����A����pz�b�>�
�G��[}�6����w�m����O�h���u��-r��N������K�)&�t��{�?l`PC��9��A����.5az�$��_����I	A��7�������4�s��y�F�vgI�����j��� [���SM(�/6����6��yH�W<�?���J50ik�#O�0���Z����OB�y�r(L��W�/��Y���!�2�
����I����
�!WN)���s,r��6p]��2}O�odrL�k/E��C�jm)��1���#���r��9A�q���4�7��I���CS�J�*�'��a����=��n��u��U;�D��e��L��T��
></��_�����Yji�M1�4�����J5uXz����i��zv]s���*|=�$�����|�~]�������:���u���WV�NB��9	!in����������X�BI����M�����9�X�G�Y�FEh��2y�x���0�_��!��>�;��l\�����T���u���N������2��EmT�6�������o�h������M�{O��k� '�<�����r���&���/��U�Ez�A%�s'����	u��<�!C
?�?1�������ea�~Y8�f#F��@�Ol�����g>k��&k?
�������7�`��������}_w)S5���\	z���Ko��R�R�w����%����GC�3���7�O��F*�YC�Y��W0�U���~,��F�JX��+t��_�L#5�S���}x����/����B�k��y�k���������P��A�}o�����TTyj��]��~9&K��4d�i���S��Y������M)���$�<.���a8q_�����$Oz-/t\�G�wbx���$Tnl����n���s�}����>+L��A���{����/����My_g��#OdM�����9P~�����/�3M{��)���-������}{���������	?�Wn�����G"h�U(��O�����NG���y��x�Ig�:�cS>������1��o�tM�����g��_��y��m�z?�g��:�8����t����G�E���g��O��W�����eny�Lx|���_���_����{<ShUG������7+{�m_�dy��]	�_��{��5��_
K��R�������>//���;�����nh����7���]�g+��6x�r���H�A ���/�\C���Z��3j��S^�kZ��0:A�;Q�SJ;A��^�d����K��QqR8c�f�Q�<4u��a���c���^����r�������o�5���Y���V��ix<�wx|>�o�p>.���+���?'XX����������{��.��VtO���{����I����������Jp��w�N��4-�YC���+��}�w����w�����c���/�����'��������?�Gj�+��P�C������3��k7s����4W����~�4u��`)���O��$X�����$��Zo��u[��&��Z&k��+��[�������}.e��7���w�r�z�5�w��r�a]�����k���7��I�V�>H��QY�z��������U��A�f�	��i!|����L�����3�*���S�~��������[�G�{�=���i�����-�TFX�J���A������jY��w��E�Q?W��z
ri��Z&���AC�	i&��}����V�V�M�{�[x/�e�L�{��5Q��h3��J���o7����9�,d�;[O���0�v��ae�cL�7������7�M��"~��u��n�?���G�4�[��@`�Vs����6��;��l���4�$lY�Rno�����E�����M�`9�(��<wr�W�M��tL���+���ok�0����0���/�JxP6�������Th,�D�B
�\��?oj�5��|j���b\�F�+���R^Ga#�����*����*O	~��F�*��5��+��������X����x��5X%^*DHc��z��w�;G���R�����;&a����50��Q�s��$L�O�>�uiS�Hy�Lo��F���w�\���-,�B$
}�������!|4�V��I+�^��������!�7�mA�=�
.�0�S���yW�Z
8)&B�[w�����n��:["o9����q��T��u�
��������Im$�W�u�
Rx}	��OZ���i0��A�YQ��K�~u��.VG��q
���f��\���,������u"��a��O�|�y�v������Z��RC���{�j�e]��Iv����^*�K�B��M7��-�����_��V!�+e�Vj�����w���o�����B��u�Vhg���r������L4(7�^��U9��7�d&�d��&
�Q�5��H��U��]�����s�S��l��>�hG(/�b�.P[F��Y"e1F���wY{�
�4
����K�1�hS
���Hm����.�|����R��P�i�+~Z{��&�5��������6_{��ygz�wq����.w����[O9��K���f�{Z����M�SN=���3����Yb����ba�5��!���9��A9�t�6��k�c�k���r��)��������Q�����������94��N�Z������������5��<
�Wj
`����.-t]�[|uN��!�y��c�\��<�js����������M�{�GC��l3��OL�u��W������F� o�2�nJ������.���X�I�D���p�S�0�U@���~���d(��k���\H����e�MtRG�LL�6��@!����� ��*!��Gx��s�r�-�������<�qY>C�4D(�v>�:��������6�F�V@����A
#��a}�I��K���7�����O�B����9�d�eE�	��Svl	@u	\��g.����d)��h)~�+��w;����o�7��#��=]��+-���>�Z�sJ�)�v�8_@��	��kt��W_ur���<�Fmi!.��W�I�\!F1@
%��������]O�A���T�,�Y�!o>�����F�>�X�1�|�>���.�;�>.�N���W�*�u�������W����j�d�<��^|M����DR����B�
�� @��PZ.�Y����H--���&�>nC�T����H*���-��Iu�=�|04���>�
���!��# �$��}���sW(��,���4�'��|��g�����a9�!#��A�[G]9������W���<�5��
�M�C+T��S4u��o����U'
��������<I�
S�����	2}���M�����n�m.H��[Bil�,g��}���� �&H@��ZZ	���ItT:�I@�f�i��r��eyu���v�Ni&Z�j�w/�o�/-40�������5L`5K�����u���O�(�u�Z��y����@���rP4����g�Z�^
ER��<���R5�B�*4��
�D�&���I����7z>�&
���@�	��jC������5M$9�.��M;����N�,� @`��m�]�-�z�<K���D����~U)�����e��\��H���an�}��Z��~��@���Uv�� �x�\~�vp�m���q�����/���w^7��3�����/lD�0��R���^|����S	}�
��s�d���CS��������fn1KY�//�s�=����z�8��e�$L��q���Q���>����[V��@��3�e���o����n�k�/�Y�hZ�:����o�
yw��}w����!P����7��}���������m��\�9v����������o>v��|�}�����a��h��3�9S7;� P3�:�Lw����:�(�d�v�M�6�@�?m`�W_}��{�]7h� w���P��i�}�����)������{�Yw���`��iZ����$_
�{sO���SgS@�*H`����g���5R  �������K��y�OJ*G!T������du�����N|gV�u�� 45�]�#�nS��8@�J"0�<�1@��x�*��!@� @h���4�G@�@�������S�E�=Sz�J5�J%E9@M����}}8:@� ��#�Gi���f@����&�	�k��P=j���%=S @� @���r�9O@�@������gi����)B@��z�0@� @� PK�[KW�s� T)y�����n�-��3�X�g�iA�������_V���5B� @� ��C�t�pg�� T���*����&�Lgb�Ln�fp�M7��f�i*�'��@�%������'������������Rr�� @� T�Bi R @M���$�(�K��E�H @� @� �	���	^	� @� @� @h\�����!@� @� @� �&H��	^	� @� @� @h\�����!@� @� @� �&H��	^	� @� @� @h\�����!@� @� @� �&H��	^	� @� @� @h\�����!@� @� @� �&H��	^	� @� @� @h\�����!�&N`�.]�l�����r����?�q�:u���9@� @� @��E��^���F��:��x�M�)���.��
w����VZ�i,G������}1jT���O4���k��N;�{��w]�p���u��h�r��W������[��z�an�;���y��6dH����
G|�E�X�����O�s�@*��
��_<���2,� @� @��	L����S��|����7�f�E�B�%�p���{�s<3�t����8����>n���^�o���{�������Ms��������:��7�|�m����ui3�.��{����3��������f&�7�	����c'!P6��sN�j�G��n�9yg���fw��G���4�i?���zhr�:�6m��
7��=����q���N���Yf�����<��W_������������N;�9������8��d��_u��^�N�p�L3�����k��un���\��������M������.�m��	n��_�w����>�^z���HM�}��s��� @� @� ��	�QZ:�F/���;�����]t���S����@��}�w�d���:�{�y�S'w����������UV�d���$��������HZ+��O>�$��������M��?��wX#�{�
7�x�{��'��u�]�b'}�E����q--�����$��;ie�
���E�h����sN��6o��VN$U�h�u�����E�n)ew�i'w�]w���??��y@� @� @�&��CY�j��!$�J�T��b�h�������o�>����qo�x��=:�\3��{�[������A��}��D$���q�������d
�fA�u���yo���on�����Nh��}�y���?~|�����7�c�+C`�^q�����;7��X ���{4���
x�!��b��V}j��f����}��i���k/���iu�� @� @�Mu���&��v�VX��}�=N�����X�@��r�-�'|���On��7w��yn�n{�����������������3#��^�;�',��i��@s$p�QG9�5byI���js9\����m����p`N��;��R���w�:u�3f��x��R���
��f�n�������8��ny��:���]���������[�K�����e�@� @� @(����`yy�<a�C�T"��\�^.���\9�u�����"�V�6��[z���S�D�/��=��o��o����;����G�� ��	h���.�8/z���w��9��-_��[n��k��qn���s��GnY93�n���n�]wuD!w��/g�� @� @�j��S���9��9�Qy��d9����#��)vX�*�Ro�[.��0�g�y����+j�����������]w���Yf�\�>���l9Ub�u��$��Q_~��Y�`�B	�`u�k��}����<���Co�]vqK[�:��;v���������������3?k�����-��nN��y�m������2+�����;'����Y�����}MX���?��)��&�$���1?o!�e�-�����Vs�-,���^Gd����9��
����]����P��:(���k�����O������p#G�,����\s������)A=�\h!��J+%��|�U������t�)|gZ.����J���C�������9�O���r�-�pK,�d�z��[�g�����E]h���uo��������j�����f�m]g��p�p��	V���������u1/:�X�_����'�|�I������^�|_���T��.&R�y�}�9�p{�_|�E�����:��)�����K�~t]toLo9,_{�����;�\�����B�.d���Q�����,��M7��u��1���_%��`�d�=�e����m7�M�M����*�,�>t���������h0�����a.OyXn��V�v���x�E�}�����;�=ev.�\�
��������x=���K����}���Zw��G;�,�-����l�Y�����q.9�����O]�����0X����v��^k=��.y��9�wJ9��`�UVqm��u�gQ"�����B�����F��E����OXnq}Wb� @� @��d�����?�?6������6l�����&�u�?��c��_~q{���E��c��-m�A,�I�����|��3:K(U����&�0��:�
:��L�����un{���K���`�u�5�����zM�1/W��	e{Y��'M �����M����lb�M7��y�un�
~����M�<�:�5�eo���<�9,�m�9��~�e�#Q���nr�o�A���>��&D�k�#�������:���uv�+K������a3-<�D��<�|�!��m$^u546	Q
��������?��xq�+|������K�L�����5�~�I��<��LM��&F�ACL���:��5�~�l�'!Qv���������|�a	�|��	����e9�O>��\�;z�v��W�����s���L�~�����������TGl'Z�Gyd�X���
o�����H��{������{m�}���n��5U��������n?��p��>�e�k��"��?Y![����A�~L�Q��y[�+�i=����,�������z�9�$���)3�w\�,����z?]b������F��P������:�KF|������ ��&���Mne�L������W�r|J�a������	��6�� ��t������&H���{E�������28����QZ��x�!6Ddw�m��H��������k�8���o�qG�0��d���]z�y��Aq9����X�z�=��#2�����������c�
��]m�
&��������6�g�2��B�_�) @� @�J!��u�iK9I�4�o����d'+v��*���<�����X�����Nyy��bGsL�H��y*�bCE�x�3���#�<�'��e$�
4���,k�H��Z��
JHTF��T���,6�����S=�:,#�����Oe��E�%�=���N9��D-/�������]G���q9y)D����p��Q�.�I���=(�z_���o=�k^������Z.!J��i&OFo�HV�����6���3AS\�Lh����Z]g��3��)����y���<�TP����C=K$Uk�`��xDj ����{m�EM�C^�
��.�<e�DR��w�u7��:��I�X�%pj?��|�)�dV%!��c���C�l���mQ4���<����&���vN$m���#�Z���+�N����B"i}���s�=wn�g�}67_��t���Y"�����;��+7�'�������
8�IU^Lt?J��2
H�]����

���n!S��k��V��4�=��U�cN��e� @� @�V�=i�z�M�����G	Z�a/�������inE0����u��~>�m�<�Wh���������[g'��J[�Y��?��3'oy5zS��0��:��W�VX�-e���sX�����6�c��!��.��^�Z�zNt����Z�D�����G���PM"�:���U�t
����x|�(����8��+�T��:��wh+���&��<��Z8���H�-QSa��	.AI�RY&�3�:+'j���a��������L��K�qILJ����_�����Q��8$��G�����[7w��7M��ku2��C������{������|f�p�o$���]v�D���KZ]~���{F�X!\�5�k��/����=�3&h�'������t-��K{�,g![%�j�L�{�}�q��zk����h���c��{D��u��m"��nt�ILm��~Tio�"�:f=����yX�c>�B��c��B�HC1i��1n���
�-q>��3Uy������v��%��V�*Y!�����G�yN������X���i��g���M
},^����6����������0����B5�s�������w�<�������=�B�{��C���������*��l��au�eF����P�������7�x���j@��q�#����O��}g���>������0i`�����g'��_�{1�4�F&>��@�������A��
���U'��O�� @� @5J���^����N,t*��Z(L����:zW!w�I(���/���G���&8cS���W^9�X���S("��k�)����$��9��iz��#yS��M��[8U/hI���EN�k���7�D\��-+�����	���-������)�����[[H���u�[������1�9�����@���s�^a�(u�����D�r��_~9�U�������r	n�P�C����*���R�C��x��SOu����g-bo�[o��m`�1�I�m�M��K.q�N
%�k��y�5�kN!?O��&%<J�����C�T����s�8,�C
M���& 5�.����t_���urC�\"�?�m�],8�:�T���
p��-������v�v\���_�M������
I|{ zI\��>��$_G}��[�s���N�A�|qr�����v
K-v/^j���g�%��eM�������z�J���&tM��k���>�w������=+�y�Q��OL����z����`��G���+��������oZ�m/�+d|x��������k'�R�EP������z;�Bsoe���LV���`]#y�{�x��� @� @� �T
��I ���Q�3�h o�Xm��e�
o�:�%���1�<��II��P�� )o�X�����y��\�tBk���y��X�D��M��9-�t<0QTv����#��Nu�`����T�Y&��4�T����a�~�*�ER�S~:���L��7	
�H��+W�I&�CN����_U��� ��<lc������Of�M
.Z�N��W��i���Q���X$�v�XXMy@�c��}{�j{�5�<�Yb}�
�z�ER�%O�L�������`u��Z/�'���J��<��}&����M����M^���0�����T���y	�a�k��
�5���H�z���
���{�����y��x��kC��*��"�?��V�X�S�"��E	����w)S
zi��M��	���W]y�{��'s���o�7�F!��}��<�c�T*�C8AwB;����#U�nc�T�u��=�hnS
vH	��NZ���H���>;�Ex�=���eL!@� @� P�J��U���:|��,�B�G�u���BJ���>+�fc�<i���f�HV�Do�Z.Sy���:��z����5oF��
��DC&~���T�Hym���[nIV+,��4�K�������~�}��?
�=���nN��������:��,og�}v'O�4��Y�%��4�(m�L��^h������������~�!	1�u���<��jg����7���R���7m:�y'���Z/�'���"y��tm�p�i�u���qM+_h�C����������0���E���Y&!6��U6ky���3�=�V6��H ������T r�e������[����B�_���~��Y�v�����q��0?����	��=��Ub����]}���U�A���M�����?���xc����������:��
��-������z��E]���#�pZ�}IP�
������zWi�@8`*8��_�C� @� @��	z��^auv����,�f)����O�MY����?/�*���'���9S��b��7o�,�e�������[K������V
B�jYCX�>2!��B�y�����)T���h�/�Lf��O=��.$
	��@^� C�/��1�B��r�����XhS�-VXW�y��]n���"�n�]NlU�I�,2��|���s�s�*�Y������DW-*u?$;����]��5����L��������f����W_}��?�������:�[@����zOjY�!�^	������y�::����*4�B���]�
u[�����#��4oAo��7�-f_Z>�e&=���b���i�D;��U8�>&�+4x����_GT����K��zwh��D�j�m�)��Z��%�f� �zX��W�wy'���Un��l���r��2�4|G����r��{��k����<,f����"�������� @� @� P�J���?~|.��A����'��TF�d!����+�u�������m����1�Wl���4�_|��U{����_)&�J�.�7�}���>�e)��2� TL@�v_�un�Y�a��{`�}�������[����/���w���W�u������*���$ �cyW[��B~~���<��P�{�$�z�q����>�N�XPo����-�*u?��������v�3�
���A���	Z����)��Zu(������4~.��\� ��U�����G��Gs1�K���Bo�b���n���P��B_W-L%�)��[�^�c�9�S^�B��l����,���������7��,�Tw�}�[�<K5��T���9�E�I����sk���y�+���Q��p]�{=���)w~��7)�=SpCVB� @� @�J	z��^�P����S��j�7&���M��P}����+�tN[R�l�RV(�j)��;����-+l�_�5
;�U��/��*�[���R��[��p�+���������M���]'a�]�%#���|��0Oi(nv^u�du�x��D$�|R�O����o�A�����R�C����8$��R���<�����<#��[)�=G���~���:��$�uCER{)���sz������
H�%{���1T���o�<�����v�5��+2U�c�����]m@O9^�r�S�p[����x����������Sg��6rd9�R� @� @�<J+rJW3�<�������{��r������!�3�M�07�����[��X��u�^�i���(������}��Y!K����K��^�8����,Z1&�\Zn���<4Y�Sa�c��R�j�}��)XM)~����+���;U��U�7	�����62�R�*�1y4�:Il������L��"{�%������l7���|���v�y�y�����K�~���y��#�w��y��l�y�����������+-�7Q�^���o�������G���~a�������y��w�}�-�sue��^�p���r��0����>>s��~jm���������w������t�k�����_{-����t]k�����{��2�����
���~`���se���3���4�`����o��,s��g9����5
�5��W�^� @� @��J�,������}���D��^��*����K���%���9"���,��@IDAT��v��|�fK,����<O����m�h�p���P�3��r`�����X|,�[��bB���1�-k��R�`f�i����&�3�R-�t�GydR\��V[o���T�=�D3��<��c����{��W���l�l�P�^���&����W�P���;}��[[�0��5������6��Da	��k#S�t�-�H��Y.���P���;t(x�q8��c��I\�?k���T�cS��Rh�N�:����3�"�s�\~�"WXn�X��&!��D�,���>��d/��2����\�#���j��z�����\3.���������������o}_��n9��s��<Wv-h����E�&K$�J'L��{��r���1� @� @�*G`��X����)@ ���S,\!�r��r��P���Ef���qhE�	�t���l���8�N�u�Y�^��3&�w�1�Xg^��=���]gG�M
��?�n
�e�"^��6�(��U�c��[LT�p5[f��������v��'a(j�B{9����b�����6��B8{;vl�Kxq�=o��Yh���:�y]c����Yw]�}�[�/�t�EqWX>�j1���&!T�@�)����Ly19�L�e;��K������>�"�
�K�T���s��������j������[�$.\0���O>���a�C��\�I3��7.��C�jPM�9���������Sh������~*����
����]��5z�nI�DR�HO�L���Y��B�"��-�����H~����) @� @� P?���6���gR��O������c��y�h����)����y�un���5����s[���s���3�wwo����2����?���,�Q�h/�����[�-�[P����:����]��P����ud�	B�CQ�����-��7�
���>��<B�0���x�P���������[Z5IH���gL-Wl�<%��$,5��N|�����L�������)�(+q?�v�1��U�����������z�m�3�<�'�eT�l_y�N^���\z�e9/<-��'�4�|�r������
S��!���N>9����{�{������{��m�}^u������]|������[sE$��x�����B�fB�����0wl9�~�)5�����{�?���N;����,���?��7
�\�����{��G��|.�l5�����e���o��_����:)'s�����ET��A�q_�O���>����&�G��W_�!S}�)�@� @� �Z �P����IA�[�6�=�D.��?�
-�����?&��n��+����V|�6o~���c���M������1�0�1t?��D��{�����Z���z�|s���KH�=���m��1����(B����XT�����W�v�q
�q(_���M7�������#,\n���}�i^�Y�����>	I��<���pT9��>��>���T��$ �P��t����r2
���>�����[�������/��.4>�t�H��������s����k�r�BOc�Ms��r�H�������F�p#,?��=��?���SlBQR��<��w��;��������(���>���0>��0�9(��;n��I�����8�����5��g���7�B��}-�nh�`��-T�B8��{G� ����OA~c���
<h��A)�������������=��;����j�E^�����[���J��}�e������w���1���{g��!�j]���^��s���u}���B�_��
=~�x���i!��n����SOu:No�Q��_G�T���yE�ku�E?�q�9i�X@� @� ��"��jQU�T;'�p�o���[e�U��V8@��o����&�+�1X,Do)�����{��/6(��
&�)�`c�D�g�}�I����XB�D�b��)�0�`��3N;-���k! r����g�<,�Y�����xw�K0=Q��I���|��s�"5,�����Ix�������y�w���*N4�\�U
[;������7	��@��kz�u�_8I��,���|�<p���\��K,�&�g�q��X��9�n�i�]A�]�\�%6z1U�G�0���U����;kz����s$O���{D��
�r�7�yy�*o�B}�>_Z��	bo����u���b%�K���e��-�mrY�?7/���^TN��u)�����K/u]|�c��&I�
M���o�pQ"�)ox�'[hsy�*���+�U�)��������M�Z��L�����|��G�)$���{n���/����qy����^����y���n��S|�M6�f�����u������	�i���)�������V�)Q�\�'�<��;rC;N
~P.d}w*�����@����tzF|Hr
:�����w�����/��~�m�2������Dy���G��{R��t�L�{M���g��R[�)Ot�-���Qn����GD
� @� @h|x�6>�F��<����M����V,�^n�3�q�PS���&h����=��	��i��z�j_3�0C"N)^(���}�M6q��}�C����2�!4uK�E�����wX4�?������^�[��%I%�ne�.�!v��~���:���(���o�qq�p��b
�Pj�<��_�$�h(��ki����P_���w���6>�n�
��q��4oR�M��__���&<�<I%����������-�8��pUU��^��P]{�3��{��1�����=�)u^Bo\��K,��[v�es����~�EW�c^���U�<SER{�h�Al:��m0��C�%V�Y���X�:�0���7X~������C��z��(����h�"��Qy
����{��iPM8�D�yz����Y��yFaY�s��+��;s����N�K����z_�DpP�C9�}k���to����9�T�KS
��c�����/]W�+�����������Q��8�k���z(@� @� ��D��	^-y��VH��8q�[�<���2�h
�����~W��Y6�<[��eu�����	�x��@��bu������e�Tv6o��g�U����U�n�>}\�����4�W��k�����&^��<,nW"i-��t�y�}��
�E�����������s��:g]�\�I3���.���tL}�������0aBnu��/<����{���g���!?�r�v�p�� ��(u���j
s5~���������iZ��!d{���R���V\�-d^����_��7�~yR��+_V��[L|�#~.�u���X������y����z�,l��+v�����{Y�:��#�������K~]�T��^k�DtN�~��A�������u+D���O2K������6�C"U�]ma�w�!i������e�����{-Wi�f�[����e5�D�	���e�	b��H$� ��N����9d:��%iz��]�x��A5X�`��3�g�m�Y�RHh��>}]��}(���-u^�mWu����<5b/�>��26
�X���!��^GpUY
�������{Y����<�U��w�I���X�V��)���
-��#�2~�PY	�����X�|}L!@� @� ��	L�j��q�~����;'��z���+�7������
�SHZ��}�r�fu�6gN�NRX��S^KC�\�MY�)l��Q�N	(��.����:�S���%l4&w����hX������S>��M\��:���0gc�,�V�J����]<��3'%��������������(��9��A��L�S��v>/����2	Y�X~��X'�����T��������:wN<A�v�����)���X�<\��37�����������c��&
��R�b��7�_���F�����l�����+�	����,�D�=����}���R��j��S�'������&�7��w���	v�k�B��k�1��\m	��w��S� @� @��4w�����G���k{�t[m�uR����VX!�jK��JE�_��v�l����mc����W���+��<��lKE{^w]nU�;�H��s�� @� @� ��&���RB�V����A�%���>���\6/��-�����u0��q���
��\ER��3O?��Y����w��7����������X]z�%�#S@� @� @�@�'�Gi��D  0��}���k��uv/Qp�i������~K�Z����u�5������~�����v�Zv���:y�b� @� @� P;�(��k��B5F`�]vq�XZ	������G�v�Z~��.��<{�|�;����r�����nk!�IcR|� @� @� ��N���~�8>@`�Xj���QG��l���i���h����������r����4h������r�V���m6w�i����[�-��Bn��gON����v~���������v�������\ @� @� @�D������M1@� @� @� @`2��.�N;�T�� @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� PJk�:s�� @� @� @�@@�4��, @� @� @� P����lzg�b��M��8"@� @� @� ��	�z�e)���Q���	G@� @� @� 42<Jp��iP��!@�4��vH��R @��-R
%�@� ��	�6MS?N��0<J�a- @� @� @� P�J���rJ� @� @� @�@a����� @� @� @�B�UxQ9%@� @� @� @�0���|X@� @� @� T!��*��� @� @� @� P�Bia>�� @� @� @���Bi^TN	� @� @� @(L��0�B� @� @� @UH��
/*�@� @� @� &�PZ�k!@� @� @� �*$�PZ��S� @� @� @�
@(-���� @� @� @�@@(����)A� @� @� @�	 ���Z@� @� @� @�
	 �V�E�� @� @� @� ��J�a- @� @� @� P�J���rJ� @� @� @�@a����� @� @� @�B�UxQ9%@� @� @� @�0���|X@� @� @� T!��*��� @� @� @� P�Bia>�� PUX`w��_|�^4����zUu~�p2\�j��� ��#��W�]�J����N������J�K� ��$@��1�Vo��{���rf������8T?��;��;tpK.��k������o��#���?�>�����V��%�]��t�����m[w�u���K-U������F
gH
�@if�qF���k�v�=�6F�Yguc��*i[<��������J��R0���5�l��m���-���n���s���3f�>|���3q�D��	l��&���n�+������>�0o @M���=��i����@`�@(�2���&0�uV�p��n�]wu�O�3�ww�}�����+�m���V�e�#�����}n��e"����;y����+����������?�3��t/����y��n��_�nJ��k4%�/@�0��Y�u�6��U`���\x�E�`z��G��C����0�����sgw����e�UW_�^y��W�nI�#���%0�4��A1m��:�X4O����]n�����a~���h��>��Sw��W�!��^pVB�)�������S�]J��<^����$@��)I�}A�D���wo����L��W5��s�s�=�]���_������u���{�����5�-Z�p
aR�O����i�����n�ud����~��m��{���s�_|1s��z�2O���%�X-���m�)��Zl�����>�v��%�b������/w=�pA�T�����o���� P�fk����a-��tk��N�>}�����04G�{��j�{����1CiJ���S��:������/s�9�����=g�xg�e�r6�l�8������������_���Q����j���<>z�rK�"X�<^%8��%,�e�q����h(�SN=�]r��.m$sV�*+�Rm�A��j��:�G���;:�:���sN�����k�Qg @���g������;����fG�vO�%�����O�@�#�k���G��  �S?4"+6�n#.�/L"�|�).���K��N���@�z�b�w��N�T5����_���t�M�j8~nT`��7v[o��{��Gr���L-]�����B�	t���z�a��>n�8���'g��T��*OTy�v���u����5�<��}�������������>���J���=;j���H�X���l�@����M���~��������fay��A)��=�{����qB�@(-�� 0E�t��n��f���^�\?&N��['�����NB{����������!���v�T'|���������o�������������$�R��p���2���:'{��2W'�1���'��OZ����[����[m�.�p�M-�s�A�a������,G<p`���n{����<�m��]����:Ci2V����:xp^Y>@�V��t�vy��������;��#�r)�L8�@��C��� -L�����	����S�79'�!�PZ#��l����\�����7�tS�8Mw�`��>;�e8���

������q��Quw3�����~p�mD��%u���D��M`�&�x��B�}���n-�����a\����[��/�p=�`����������'��F�0��w����o�����w�%���}���c�)��n��R'����?�b���������JO;�t���E���O�'�O?�v�m7�u�����7�q�����[o=w�!����]3������[���"�<�;����y�Eq[n��[��{�6��V������N������H<�����:u��6�`���+�%���3��F����|��W��l8�f�m�I�l���b��e-��;������.�����������u�����u����{���l�#Y9;7�t���~X�8^x�w�5�dJ���zb����=�������f�C�5v+���[�<���k.��u�~���n��+}MP�����4���+�t��7�J�]l=��>�h�y�����k����?<�����<�~���X^��� P�����Ch
���	=���v�8�;v����*I*}Oy�Xt�Yg�����/�3��A�����P(�n��Mk�
:4|pc�vW�
/�jO���v�<S��v�"�|������.s�\)��Xb	w��':y�K���]��g���.���z�^;�D�UW[-9��g������n��1�VKY��T)�s9����s�1�������s��|��N��G@�y��������xu��W��JW�Z��v���'X���~��k����E6���o��M�}��W\?K���R���=�>���I���v�b�v��{�94�����?S�@W��9�����O��6�}���]��~@�f;��c����O>q����y���GVN��B[�D�xd�~�H���Y��}���;��-U�X���z��{�u���gC,�L��6�u3[X�G,�i��o�c�p���� �L����T(2)<�������u���5���N\	o�}lY�����a"��Q����!��N�>&����^}�����#m�l��:}�1y]\V�����[��������A7^~����mM���~�3qs��W�U������:�TV����?�t�$���v�6�l3��	�3���f�|��-��Lba���;��T�R��LT_2�����'�������w�����I�2���M<��Bg{S���/�>���'=C�^#�;}���y"��x�w�Au1x�C<���>���n�W�;Y���:���e��g^�[|keVN�A�9������kn�`�������B9�/6A2�p�y��v]V{Z���&�f�7^��P)k�j�=`�X+F�z�\�=�6��&Zd��6�o�=��<m'wW���
i�A�����bm�4����+0y��7���j'V�h�T��}y��d��?�4�R��O�n���g�e�I4\������[g�`t��.d�L���YF��vO����[�
�����vO:��K�6Mup��h��n�:"��RL���4	����F���H1�T�,���{�<$�����d��Y"�����+�p���h�H�r:�~��7����c�i���B������;���l�Y�_��u�"�_���5���>UV[��O��4{���1<��o���x}�\����y������;��n�un��h�m�zu�
}���"��K���~F��v��������"���i���g�jS������fC����~�����m���1:%����~����H�c�}���g��6�j����e��?"��B�HQ�����}���V�"6�1�?�����9�14p14}�+BI!;��p��7��E��mv�M�����c���A��S\��7k1�Te��"��]�fY(�n��m��n�
��j_���%�j������{��W�a��r��oc�y�M!xc��{I���1���H��lQ���>��b����3���\h��e�%�{������~�O��a��Y�b{�Ff�b
���B��i��7yi�x�L��i��eKw	��B�3�-��F#�����)L���3u���|��i��m���2����~{������;�w�M!���]v���������V��~�-�&�bJpV������W8��X��L-[���:��,�~9�������vY��������6�-d�Ll�Go���n�S���-�r�)�e����U����X����N��5��_�K��~��`aKyv���r@�yX�""������:��,�s��X��F#��?O#��>5�6�s6�/6�{�����{^	�d�J�%,��"��v�EQgll����fQ;�����Nk��m�($�����h��Af�H�e��� P�n����B<��R�%
��o�4��������vZ�&�{�p�}����#�@��.-���������%�P:u��w���H�D��e��x��uJ��Wn?������,D�5�#$6y�no!���B{*_���x�uB���%�z��ni�W!��Bui�|)&>������9��3J����SN)��$��rD�����4�*��r��a����kZ�:��\R���w"��;RZ�W�KB��1u>+�����ud��r5�&�V�d�)�lh
��];�\��Lu,oq�X������B�u�)_kl
=����u�����uB���r��b/������s������]��C	�]�M9��8�!���P883��U'�����������g��V��2�����Y��0$��@���6z�T������v
�|���_����O�Y,�j �U���j�@Z�e�����(NRy�ic�DTl�my=�
BR��n�����I!�����M-�@CL�=�h��
����4q2��a�
�_�����j�����4�+����E�Du�a��jc*U��R�a����}qa��^�&bS��6������A�M��`���a��J���H�4r�H���N��������b�4���S�����/����B��vO����N���W�9sVhrf3Iy@�6v���c��%��r��D���6�w�~���:��3�L
m��������z�����>Y�����6[o����e�t'���|���o����������3�x�|�M���oT�\K����F�Y��mn�����.�3?�B�mg���E*v ��O?��66v�y����b�Kz����Hba��	�e
����|X$j^~�e����P��<�f�����s��/���G�����z���������|p�U�-�q��E��^�\�
��1o
�*q]yi��e�H��Qo�%��f�E��|�JL����via���|�������s��N��7���`�8G�"��5�����q�h�x��6�y++7Xh�1@��,l�c�����E��L�6F-�1���Atjg�m���c[�W�m���N^��M��!�:x�/Vt�����i!o���� ��W*��+\�+���������X��U�[�ojlj))TW88� ����E��cm�8���&�J������&�����N�V�m��F~�i�c�>�
�������������{�"f ��'�w��&�wF��R�%
�#3iP����w��/�tg�q�����
?�M�q�R�-�=�{h���MB��v�G2�@5�����&��l	��������+r>i9>�[��4�L2�
M��L���H���ST�D�i�I�r�0������d*/�b&����a�CL��E�S���'�F� o>Mc��@J�&���?����Z�����&�z=j��M�N/����~��!�i��B��_�5��MM,�����T��C�m�z�}�U}X~�������)^�?�P��n�
b�=\���R�����s�w����x��?+��(����3�w�}��c����|�����p��n7��4�2�4b6��L�n,S�������DR�o��qN4=_��[��d
4?����`�Jm�����@)��i���Y����k�><�����xu�g�-�N����{��c��r�z��I���l�H���Z
�6Ch+Xdoq4�W�x`�AE�Y?�q���	�� >/���iz�1M�M�a�nqx���N��K\c;���R��aQz��]�
����h�����?I.�E�'���||�@s!0}s9P��L@����"��eJ��h���9���G&\�yA$���U�X���1�$���6G��V�i�*����.ka��K��S��;&�*�Hhml�{V�6������*��rW�Y,�������g���]�0��.}�r`=������?�Q�!+y�o��=L\i_�pm}-_�B���4�7�XP�|�IfU��k^������Y&�\�h��3&���Mx��e�5����v-b�;��Fo����w����ne�\�|J�r���������3�<GbT��%��'��O ��3^���W�hc8G��;��K�����>;�8z=eQP$�G�mk�V
���Z����>��;����v����4�j�>�1�k�V��'��:�~"�"��V�� ���cK��iQA�����x��0$�5Y�M�7��fa��U��������~��E�g�bQ��4��C�'��H[������Vh�L���{&�`�L����.��lh$�rs����o��"��u�P�s�\���
7J�N�U�f�J���;�R6����Vh��rt�B�"v0K(���e����R��0�X��Z�2�,�'�����H�\�]�S�K-O�<r{����z����O<�����;�T@���/U��>�H�^|�{����i����tt?��j|��'��g����e����*\�>"Y��fDo�����"��V������T�,{-e]ZGA��,��?�QQD�Q=��g��ic����1�G��14p��c�M���bK�J��
��`QH�T��W^ye��~���-�h��c^�Y&oS������}"�^N��j[*E�6�\s%�aZ�hQg�r������B+�^x��7JC`�C����6�z�O�1�KY�����}+�����h����|7�������'4%�6���X P�b�F!�*�U���C�������x���<�����y��J��*���X�V���Yn�P@T��k��$�2y�J�S�s�S44�,����~�z
�h^?67���OY��M7�,^��9�3������o�;�+��*�I��2Y����M�������\[������;�1���ei�W�j��<�b��F�����;�6F�ic������������^7������WZ���v7�pCj�B)
��c�Z�7�'������Z��!k���>�!J��6p!��6Rh�A�A`3�Q�Y}��w����{i��g����b���������h�x���{h��w	�����@S!�GiS�G��@��hy-$��N��u���q�y��}(�P^a���hI����TX������x�T����$)��
m[�����v��	����B��G��I����j��cP!z�<��$l[�s��+��g�}�n����b��+[g�R�w�y4����S'Y�S�^	�]V]�$ia�����x�o8��vSb���������������>}r�5��������S��o���g,4��z����?�7����,�����{re���5<z7�66#�q\���@u��B��;?�����<�{��}�[����<2�C�6F��1���uN���[����
�f�-�\�&�e�����f�f?�+���v����U$�%:�5����I#�n�)@$L����Q;�,�o���6�_������}����~{�h�Z�M���}l��[T��R����D3��C��d�i�c�[��GsM�o%�H��D��vO�}�24g����q�UE�	�_t�!����r m��&y��>\f�Z�rC����#$�N���3��-�T��i�0u
M��tRg�{��)��Iz�92�h�e#S�{������-ZQT@�0��uV��U�;����F�-z�j�v�o�'�j��#�Wz��&�������&�+/U�1���w��]u��I�B����v��en��rQ��R�4���
��
y�J0P�bo������u����X$}�r��g�u<�@e����uVb>������{<#����������T��rm)������]s��'�K,D{!S�q�E��&?H���p�6Fu�1$^r��y����C��,S
�o��6�mw~����g�nIk���-
��������8���������{���<�8>�+5�����V�-�R[��xE*wJL/h�[��XQ�Z1��&�rTR��DhAS����Q������%����]�}wfog�3{��M`wgg������g���/�,�(U��z�q��`_���a�����R%raE� �@�0`@<r�Pj����.5���+����[�45��0-�lE
�S�},e+��R�B�S{�D�s�{�3�]��w����F�	�+L�CL�\��Yg�]RI�BLWp�z����o���II�g�s)���
Y��������9�*�{��U������gO��j_��`7t�����
_f����9-5/�*=`a�0���������r���"��t���%�8���eiB��w\�6����n*�f��5%����z��&
S�m���Oc���_�s��
�JR�p����\�h��{/������)�V��#F����U+W��:�����Kt�5�I���_����*�$�{�����=�P����S�o�=��?���������S�y���*&5n�{�&4h��V.��[���$��j��bU=�C���m������epyu��]^����7F���F�HJ�m�Jt.]��-XPr�~�a��������{�)6WRRy��O>�>�Q������W%�!�!�)�����H�S�=a	� @Ei3�%��Sh������c}h������.Y�7N��O��x���t��E�J*w4����#�*��?��Z~���^tyK������v������
����*!�M����8�*JG�U�����d=6l��{�Z��_�0^O�H�&�=����K���BaA��W���{?aBII=m�!�J6���-Ve���+�t�xn��Y-��=���[���RZ==�����G�El(�0����=�,��W5)i��A/�j��w��]��VUa��X����}���!��C���K����}�����7^�����=�l4&h4��������=���a#6��|�-����D���/�T7%����S,��~s�'��X?�����X�O=z���&��[4��U�����mj��Y����@�u����F����X�J������k4)va��+;�#�@���`�]#��(M�i���IDAT\���%�^���.-�2�����l�h��&��_���4D�qqqO�/�������+�[��w�����L�����V���� ]����m�|��
����y���SP&������O�;+�//���p���co�[W��q�Q�.]���M�}	���Vj��&M�&��
����c�-��q���U4�%=`������P�c����n�f�4�K�R�x��3'�hUL��*L]�����R��%K"������6���9����[������w��Zt�w��i��Fs�
���I�V����e��Xo]�_1������:��h���6�iR/��V���h-�9xS�JW�����SO��-vCR/�Jw�1�~�kl�F�a���q��M��S?����A����`���6YA���k������%�~��K����~����}S!�Z�&�[[�;�B���b�k��M��J#<��q� ]��wL�����Y�����~�bb��c�j�/�9�����`M=;�n�iM��J��E��9��<��d��ys�FM�O�����6���G��c�ZChk?v�����O
w���!�7�=D���+�"N:�j��*<�P#��2T1�?l�S�2U|�Oc������u���_�Q^����	�[@��l���%]#U1�W@����Y�Q��u]R�����{/?�>_�,+�Q��2R��pN��\
���{�{�{�{t- ��	��'@Ei�;�Q�`��7�=bl�M�v
�U.i���Vx�y���W]m�9=��U����
/��CV!�^s����*o]A�
��
_�J/U��%-�����G�{���NaI-��gi~(?�~b��h�m����G
��iI^��zH 
A�Jm���.�93�/-������i�����,M��~���z"��}��EW\~y���Z��;��6,�*|���#�Dg
/R��n���o
�7��K��v�<W�bR:��c"��i7��V��;9�*���W���/-m��NU��@�s
�����^�z_��%��n
D�D��T����*�1�M��S
��J��]���Q��HG��PqG���.6H[�Jk,�'�n��p�S�M����a�����;f���/��Y/�WW�(�Q/W���4��6Vl�SL���
��$��~l_�3�@���47�p��'���S�E����.M����]+��K�(5@�������m?���='�i�"�imt��Z�{j��6d'pxvY�3�#��
s:�*;�3����<�[�F�#��V�/���4����������f[���%��f����+2������p��|������z\�
���A� a���<���]x�E=~i����6iN�A5�O��������
���{T����i����8��T7�~R��!��[Xt��L���\��z
�gG��^�|o+���m�o�_�^V|��&�NV��@��	h��{l4�Z�_�-��8�71F����c{���c|i��[c�p��p_�^�c������-�R������o�g��1n_��M:��Z����O�/�xS��j��$U>l�Qo�����N�\k����>G
�H �9��[���i�Q�%}��6
���I�:��vO^��{��K_��'�F���!�9h�384T�g>���Y����cQ��
M�?o���B�\���6O��/��S����!\'X�;�9@�~��$-��a����jb��KSn�1��J:��d���@w���n��c�_�|��asr��L��������\W����j�[�GC�%��u�+X��������4�����C����S�aE���2}4����~'����*Hc�q:-�s����4�V=���8p \��:��r�6����nd�~���~~�_����w���f�����C��pug
TRA�������z�Z=Z���^��E�9����{t��R���%U������{�4Oj�v,G��+��0�lXN��^���Ve�4��Q��%b�V��z]������������x(/1�vJ���{������������/���u��������{��5�R��6�D�=:�<{iI������cH[G��A��I'E�}�Y�j��}���_O�0�1i:�>��?&e�����
����ER�l���<>x�����b��Y�����M�O]�����k��[CU�vQ)5����]o����gcRY��C�������m������=�g,��F�������Vtv�{Z����'�#[��#�U��&������	�6T�&�]j��qh���P��q�����
	�
�Gt���M��s��r�'����V������Q���b��<�Xw��/G}�f�%��;���tI�����y]����6S��u���P5�U��y�dWkE��G���l�����
�Z��9���/���<Ya�j#�Ys����'��S�GU��K�{;K�0��Y�
�yq���s�iFr�\=��J��GG�6i��]:�
+{���_��y�����{���MX���C:�������C�A��'����k�k���Fe�*(�'c����
1Fs���A��6��7-6?���x�	�I��F>)7u����`T��r���4wz/��?�Q+c*��%i.�Z�v���u�����^Q���Oq��f�b�������h���b��vL��T�*�Zc�Z��u�C�X�9�S���Q�%��X�t��-��]es��z�T>�=R�/������a�|	����fw����~W���i'6>�
T�(��$2 �f��6�A ��'�	�REi��@]�"u��u��M��m �i�6�1M��jv��m��;� � � � � � �����Q��� � � � � � �M#@Ei��*v@@@@@@%pD�2"@ �����{�����m�Vx�@@�^b�z���-{���>��+:���m�w����,�@���=�X!��P�28���G}�I�)�6���)�U�bj&c@h�qH� �@� ����@@ g�4�'��6c����A@@@@@@���4+Y�E@@@@@@��
PQ��S��!� � � � � � �@VT�f%K� � � � � � � �[*Js{j�1@@@@@@�J����d�@@@@@@r+@EinO
;� � � � � � �Y	PQ��,�"� � � � � � �@n�(���a�@@@@@@@ +*J��%_@@@@@@����=5� � � � � � �d%@EiV��� � � � � � ����4���C@@@@@@���(�J�|@@@@@@@ �T�����c � � � � � � ���Y��/ � � � � � ��V�����v@@@@@@�8"����:�>��W�"k!� �
 i0(�!� �@M�"5q�2 � �@�(��,@@@@@@@ ��(m���i��v�>@:����A���<�� �@>�E�q�@h���i��[��=J���� � � � � � � p��(=��|  � � � � � �����m�Aiuk��IEND�B`�
#17Robert Haas
robertmhaas@gmail.com
In reply to: Nikhil Kumar Veldanda (#15)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Fri, Mar 7, 2025 at 8:36 PM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

struct /* Extended compression format */
{
uint32 va_header;
uint32 va_tcinfo;
uint32 va_cmp_alg;
uint32 va_cmp_dictid;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;

First, thanks for sending along the performance results. I agree that
those are promising. Second, thanks for sending these design details.

The idea of keeping dictionaries in pg_zstd_dictionaries literally
forever doesn't seem very appealing, but I'm not sure what the other
options are. I think we've established in previous work in this area
that compressed values can creep into unrelated tables and inside
records or other container types like ranges. Therefore, we have no
good way of knowing when a dictionary is unreferenced and can be
dropped. So in that sense your decision to keep them forever is
"right," but it's still unpleasant. It would even be necessary to make
pg_upgrade carry them over to new versions.

If we could make sure that compressed datums never leaked out into
other tables, then tables could depend on dictionaries and
dictionaries could be dropped when there were no longer any tables
depending on them. But like I say, previous work suggested that this
would be very difficult to achieve. However, without that, I imagine
users generating new dictionaries regularly as the data changes and
eventually getting frustrated that they can't get rid of the old ones.

--
Robert Haas
EDB: http://www.enterprisedb.com

#18Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Robert Haas (#17)
7 attachment(s)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Robert,

Thank you for your response, and apologies for the delay in getting
back to you. You raised some important concerns in your reply, I’ve
worked hard to understand and hopefully address these two:

* Dictionary Cleanup via Dependency Tracking
* Addressing Compressed Datum Leaks problem (via CTAS, INSERT INTO ...
SELECT ...)

Dictionary Cleanup via Dependency Tracking:

To address your question on how we can safely clean up unused
dictionaries, I’ve implemented a mechanism based on PostgreSQL’s
standard dependency system (pg_depend), permit me to explain.

When a Zstandard dictionary is created for a table, we record a
DEPENDENCY_NORMAL dependency from the table to the dictionary. This
ensures that when the table is dropped, the corresponding entry is
removed from the pg_depend catalog. Users can then call the
cleanup_unused_dictionaries() function to remove any dictionaries that
are no longer referenced by any table.

// create dependency,
{
ObjectAddress dictObj;
ObjectAddress relation;

ObjectAddressSet(dictObj, ZstdDictionariesRelationId, dictid);
ObjectAddressSet(relation, RelationRelationId, relid);

/* NORMAL dependency: relid → Dictionary */
recordDependencyOn(&relation, &dictObj, DEPENDENCY_NORMAL);
}

Example: Consider two tables, each using its own Zstandard dictionary:

test=# \dt+
List of tables
Schema | Name | Type | Owner | Persistence | Access method |
Size | Description
--------+-------+-------+----------+-------------+---------------+-------+-------------
public | temp | table | nikhilkv | permanent | heap | 16 kB |
public | temp1 | table | nikhilkv | permanent | heap | 16 kB |
(2 rows)

// Dictionary dependencies
test=# select * from pg_depend where refclassid = 9946;
classid | objid | objsubid | refclassid | refobjid | refobjsubid | deptype
---------+-------+----------+------------+----------+-------------+---------
1259 | 16389 | 0 | 9946 | 1 | 0 | n
1259 | 16394 | 0 | 9946 | 2 | 0 | n
(2 rows)

// the corresponding dictionaries:
test=# select * from pg_zstd_dictionaries ;
dictid |
dict
--------+----------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------
--------------------------------------
1 | \x37a430ec71451a10091010df303333b3770a33f1783c1e8fc7e3f1783ccff3bcf7d442414141414141414141414141414141414141414
14141414141a15028140a8542a15028140a85a2288aa2284a297d74e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1f1783c1e8fc7e3f1789ee779ef01
0100000004000000080000004c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696
e6720656c69742e204c6f72656d2069
2 | \x37a430ec7d1a933a091010df303333b3770a33f1783c1e8fc7e3f1783ccff3bcf7d442414141414141414141414141414141414141414
14141414141a15028140a8542a15028140a85a2288aa2284a297d74e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1f1783c1e8fc7e3f1789ee779ef01
0100000004000000080000004e696b68696c206b756d616e722076656c64616e64612c206973206f6b61792063616e6469646174652c2068652069732
0696e2073656174746c65204e696b68696c20
(2 rows)

If cleanup_unused_dictionaries() is called while the dependencies
still exist, nothing is removed:

test=# select cleanup_unused_dictionaries();
cleanup_unused_dictionaries
-----------------------------
0
(1 row)

After dropping temp1, the associated dictionary becomes eligible for cleanup:

test=# drop table temp1;
DROP TABLE

test=# select cleanup_unused_dictionaries();
cleanup_unused_dictionaries
-----------------------------
1
(1 row)

________________________________
Addressing Compressed Datum Leaks problem (via CTAS, INSERT INTO ... SELECT ...)

As compressed datums can be copied to other unrelated tables via CTAS,
INSERT INTO ... SELECT, or CREATE TABLE ... EXECUTE, I’ve introduced a
method inheritZstdDictionaryDependencies. This method is invoked at
the end of such statements and ensures that any dictionary
dependencies from source tables are copied to the destination table.
We determine the set of source tables using the relationOids field in
PlannedStmt.

This guarantees that if compressed datums reference a zstd dictionary
the destination table is marked as dependent on the dictionaries that
the source tables depend on, preventing premature cleanup by
cleanup_unused_dictionaries.

Example: Consider this example where we have two tables which has
their own dictionary

List of tables
Schema | Name | Type | Owner | Persistence | Access method |
Size | Description
--------+-------+-------+----------+-------------+---------------+-------+-------------
public | temp | table | nikhilkv | permanent | heap | 16 kB |
public | temp1 | table | nikhilkv | permanent | heap | 16 kB |
(2 rows)

Using CTAS (CREATE TABLE AS), one table is copied to another. In this
case, the compressed datums in the temp table are copied to copy_tbl.
Since the dictionary is shared between two tables, a dependency on
that dictionary is also established for the destination table. Even if
the original temp table is deleted and cleanup is triggered, the
dictionary will not be dropped because there remains an active
dependency.

test=# create table copy_tbl as select * from temp;
SELECT 20

// dictid 1 is shared between two tables.
test=# select * from pg_depend where refclassid = 9946;
classid | objid | objsubid | refclassid | refobjid | refobjsubid | deptype
---------+-------+----------+------------+----------+-------------+---------
1259 | 16389 | 0 | 9946 | 1 | 0 | n
1259 | 16404 | 0 | 9946 | 1 | 0 | n
1259 | 16399 | 0 | 9946 | 3 | 0 | n
(3 rows)

// After dropping the temp tale where dictid 1 is used to compress datums
test=# drop table temp;
DROP TABLE

// dependency for temp table is dropped.
test=# select * from pg_depend where refclassid = 9946;
classid | objid | objsubid | refclassid | refobjid | refobjsubid | deptype
---------+-------+----------+------------+----------+-------------+---------
1259 | 16404 | 0 | 9946 | 1 | 0 | n
1259 | 16399 | 0 | 9946 | 3 | 0 | n
(2 rows)

// No dictionaries are being deleted.
test=# select cleanup_unused_dictionaries();
cleanup_unused_dictionaries
-----------------------------
0
(1 row)

Once the new copy_tbl is also deleted, the dictionary can be dropped
because no dependency exists on it:

test=# drop table copy_tbl;
DROP TABLE

// The dictionary is then deleted.
test=# select cleanup_unused_dictionaries();
cleanup_unused_dictionaries
-----------------------------
1
(1 row)

Another example using composite types, including a more complex
scenario involving two source tables.

// Create a base composite type with two text fields
test=# create type my_composite as (f1 text, f2 text);
CREATE TYPE

// Create a nested composite type that uses my_composite twice
test=# create type my_composite1 as (f1 my_composite, f2 my_composite);
CREATE TYPE

test=# \d my_composite
Composite type "public.my_composite"
Column | Type | Collation | Nullable | Default
--------+------+-----------+----------+---------
f1 | text | | |
f2 | text | | |

test=# \d my_composite1
Composite type "public.my_composite1"
Column | Type | Collation | Nullable | Default
--------+--------------+-----------+----------+---------
f1 | my_composite | | |
f2 | my_composite | | |

// Sample table with ZSTD dictionary compression on text columns
test=# \d+ orders
Table "public.orders"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
-------------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
order_id | integer | | | | plain |
| |
customer_id | integer | | | | plain |
| |
random1 | text | | | | extended |
zstd | |
random2 | text | | | | extended |
zstd | |
Access method: heap

// Sample table with ZSTD dictionary compression on one of the text column
test=# \d+ customers
Table "public.customers"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
-------------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
customer_id | integer | | | | plain |
| |
random3 | text | | | | extended |
zstd | |
random4 | text | | | | extended |
| |
Access method: heap

// Check existing dictionaries: dictid 1 for random1, dictid 2 for
random2, dictid 3 for random3 attribute
test=# select dictid from pg_zstd_dictionaries;
dictid
--------
1
2
3
(3 rows)

// List all objects dependent on ZSTD dictionaries
test=# select objid::regclass, * from pg_depend where refclassid = 9946;
objid | classid | objid | objsubid | refclassid | refobjid |
refobjsubid | deptype
-----------+---------+-------+----------+------------+----------+-------------+---------
orders | 1259 | 16391 | 0 | 9946 | 1 |
0 | n
orders | 1259 | 16391 | 0 | 9946 | 2 |
0 | n
customers | 1259 | 16396 | 0 | 9946 | 3 |
0 | n
(3 rows)

// Create new table using nested composite type
// This copies compressed datums into temp1.
test=# create table temp1 as
select ROW(
ROW(random3, random4)::my_composite,
ROW(random1, random2)::my_composite
)::my_composite1
from customers full outer join orders using (customer_id);
SELECT 51

test=# select objid::regclass, * from pg_depend where refclassid = 9946;
objid | classid | objid | objsubid | refclassid | refobjid |
refobjsubid | deptype
-----------+---------+-------+----------+------------+----------+-------------+---------
orders | 1259 | 16391 | 0 | 9946 | 1 |
0 | n
temp1 | 1259 | 16423 | 0 | 9946 | 1 |
0 | n
orders | 1259 | 16391 | 0 | 9946 | 2 |
0 | n
temp1 | 1259 | 16423 | 0 | 9946 | 2 |
0 | n
temp1 | 1259 | 16423 | 0 | 9946 | 3 |
0 | n
customers | 1259 | 16396 | 0 | 9946 | 3 |
0 | n
(6 rows)

// Drop the original source tables.
test=# drop table orders;
DROP TABLE

test=# drop table customers ;
DROP TABLE

// Even after dropping orders, customers table, temp1 still holds
references to the dictionaries.
test=# select objid::regclass, * from pg_depend where refclassid = 9946;
objid | classid | objid | objsubid | refclassid | refobjid |
refobjsubid | deptype
-------+---------+-------+----------+------------+----------+-------------+---------
temp1 | 1259 | 16423 | 0 | 9946 | 1 | 0 | n
temp1 | 1259 | 16423 | 0 | 9946 | 2 | 0 | n
temp1 | 1259 | 16423 | 0 | 9946 | 3 | 0 | n
(3 rows)

// Attempt cleanup, No cleanup occurs, because temp1 table still
depends on the dictionaries.
test=# select cleanup_unused_dictionaries();
cleanup_unused_dictionaries
-----------------------------
0
(1 row)

test=# select dictid from pg_zstd_dictionaries ;
dictid
--------
1
2
3
(3 rows)

// Drop the destination table
test=# drop table temp1;
DROP TABLE

// Confirm no remaining dependencies
test=# select objid::regclass, * from pg_depend where refclassid = 9946;
objid | classid | objid | objsubid | refclassid | refobjid |
refobjsubid | deptype
-------+---------+-------+----------+------------+----------+-------------+---------
(0 rows)

// Cleanup now succeeds
test=# select cleanup_unused_dictionaries();
cleanup_unused_dictionaries
-----------------------------
3
(1 row)

test=# select dictid from pg_zstd_dictionaries ;
dictid
--------
(0 rows)

This design ensures that:

Dictionaries are only deleted when no table depends on them.
We avoid costly decompression/recompression to avoid compressed datum leakage.
We don’t retain dictionaries forever.

These changes are the core additions in this revision of the patch to
address concern around long-lived dictionaries and compressed datum
leakage. Additionally, this update incorporates feedback by enabling
automatic zstd dictionary generation and cleanup during the VACUUM
process and includes changes to support copying ZSTD dictionaries
during pg_upgrade.

Patch summary:

v11-0001-varattrib_4b-changes-and-macros-update-needed-to.patch
Refactors varattrib_4b structures and updates related macros to enable
ZSTD dictionary support.
v11-0002-Zstd-compression-and-decompression-routines-incl.patch
Adds ZSTD compression and decompression routines, and introduces a new
catalog to store dictionary metadata.
v11-0003-Zstd-dictionary-training-process.patch
Implements the dictionary training workflow. Includes built-in support
for text and jsonb types. Allows users to define custom sampling
functions per type by specifying a C function name in the
pg_type.typzstdsampling field.
v11-0004-Dependency-tracking-mechanism-to-track-compresse.patch
Introduces a dependency tracking mechanism using pg_depend to record
which ZSTD dictionaries a table depends on. When compressed datums
that rely on a dictionary are copied to unrelated target tables, the
corresponding dictionary dependencies from the source table are also
recorded for the target table, ensuring the dictionaries are not
prematurely cleaned up.
v11-0005-generate-and-cleanup-dictionaries-using-vacuum.patch
Adds integration with VACUUM to automatically generate and clean up
ZSTD dictionaries.
v11-0006-pg_dump-pg_upgrade-needed-changes-to-support-new.patch
Extends pg_dump and pg_upgrade to support migrating ZSTD dictionaries
and their dependencies during pg_upgrade.
v11-0007-Some-tests-related-to-zstd-dictionary-based-comp.patch
Provides test coverage for ZSTD dictionary-based compression features,
including training, usage, and cleanup.

I hope that these changes address your concerns, any thoughts or
suggestions on this approach are welcome.

Best regards,
Nikhil Veldanda

Show quoted text

On Mon, Mar 17, 2025 at 1:03 PM Robert Haas <robertmhaas@gmail.com> wrote:

On Fri, Mar 7, 2025 at 8:36 PM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

struct /* Extended compression format */
{
uint32 va_header;
uint32 va_tcinfo;
uint32 va_cmp_alg;
uint32 va_cmp_dictid;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;

First, thanks for sending along the performance results. I agree that
those are promising. Second, thanks for sending these design details.

The idea of keeping dictionaries in pg_zstd_dictionaries literally
forever doesn't seem very appealing, but I'm not sure what the other
options are. I think we've established in previous work in this area
that compressed values can creep into unrelated tables and inside
records or other container types like ranges. Therefore, we have no
good way of knowing when a dictionary is unreferenced and can be
dropped. So in that sense your decision to keep them forever is
"right," but it's still unpleasant. It would even be necessary to make
pg_upgrade carry them over to new versions.

If we could make sure that compressed datums never leaked out into
other tables, then tables could depend on dictionaries and
dictionaries could be dropped when there were no longer any tables
depending on them. But like I say, previous work suggested that this
would be very difficult to achieve. However, without that, I imagine
users generating new dictionaries regularly as the data changes and
eventually getting frustrated that they can't get rid of the old ones.

--
Robert Haas
EDB: http://www.enterprisedb.com

Attachments:

v11-0001-varattrib_4b-changes-and-macros-update-needed-to.patchapplication/octet-stream; name=v11-0001-varattrib_4b-changes-and-macros-update-needed-to.patchDownload
From e7e8aa07af0466f4a05d7df1c002596b69563f4a Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Mon, 14 Apr 2025 21:07:00 +0000
Subject: [PATCH v11 1/7] varattrib_4b changes and macros update needed to
 support zstd dictionary based compression.

---
 src/include/varatt.h | 57 ++++++++++++++++++++++++++++++++++++++------
 1 file changed, 50 insertions(+), 7 deletions(-)

diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d4998..ba06250b6ce 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -42,8 +42,9 @@ typedef struct varatt_external
  * These macros define the "saved size" portion of va_extinfo.  Its remaining
  * two high-order bits identify the compression method.
  */
-#define VARLENA_EXTSIZE_BITS	30
-#define VARLENA_EXTSIZE_MASK	((1U << VARLENA_EXTSIZE_BITS) - 1)
+#define VARLENA_EXTSIZE_BITS				30
+#define VARLENA_EXTSIZE_MASK				((1U << VARLENA_EXTSIZE_BITS) - 1)
+#define VARLENA_EXTENDED_COMPRESSION_FLAG	0x3
 
 /*
  * struct varatt_indirect is a "TOAST pointer" representing an out-of-line
@@ -122,6 +123,14 @@ typedef union
 								 * compression method; see va_extinfo */
 		char		va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
 	}			va_compressed;
+	struct
+	{
+		uint32		va_header;
+		uint32		va_tcinfo;
+		uint32		va_cmp_alg;
+		uint32		va_cmp_dictid;
+		char		va_data[FLEXIBLE_ARRAY_MEMBER];
+	}			va_compressed_ext;
 } varattrib_4b;
 
 typedef struct
@@ -242,7 +251,14 @@ typedef struct
 #endif							/* WORDS_BIGENDIAN */
 
 #define VARDATA_4B(PTR)		(((varattrib_4b *) (PTR))->va_4byte.va_data)
-#define VARDATA_4B_C(PTR)	(((varattrib_4b *) (PTR))->va_compressed.va_data)
+/*
+ * If va_tcinfo >> VARLENA_EXTSIZE_BITS == VARLENA_EXTENDED_COMPRESSION_FLAG
+ * use va_compressed_ext; otherwise, use the va_compressed.
+ */
+#define VARDATA_4B_C(PTR) \
+( (((varattrib_4b *)(PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG \
+  ? ((varattrib_4b *)(PTR))->va_compressed_ext.va_data \
+  : ((varattrib_4b *)(PTR))->va_compressed.va_data )
 #define VARDATA_1B(PTR)		(((varattrib_1b *) (PTR))->va_data)
 #define VARDATA_1B_E(PTR)	(((varattrib_1b_e *) (PTR))->va_data)
 
@@ -252,6 +268,7 @@ typedef struct
 
 #define VARHDRSZ_EXTERNAL		offsetof(varattrib_1b_e, va_data)
 #define VARHDRSZ_COMPRESSED		offsetof(varattrib_4b, va_compressed.va_data)
+#define VARHDRSZ_COMPRESSED_EXT	offsetof(varattrib_4b, va_compressed_ext.va_data)
 #define VARHDRSZ_SHORT			offsetof(varattrib_1b, va_data)
 
 #define VARATT_SHORT_MAX		0x7F
@@ -327,8 +344,20 @@ typedef struct
 /* Decompressed size and compression method of a compressed-in-line Datum */
 #define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
+/*
+ *  - "Extended" format is indicated by (va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG
+ *  - For the non-extended formats, the method code is stored in the top bits of va_tcinfo.
+ *  - In the extended format, the method code is stored in va_cmp_alg instead.
+ */
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+( ((((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG ) \
+  ? (((varattrib_4b *) (PTR))->va_compressed_ext.va_cmp_alg) \
+  : ( (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo) >> VARLENA_EXTSIZE_BITS))
+
+#define VARDATA_COMPRESSED_GET_DICTID(PTR) \
+  ( ((((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG ) \
+	? (((varattrib_4b *) (PTR))->va_compressed_ext.va_cmp_dictid) \
+	: InvalidDictId)
 
 /* Same for external Datums; but note argument is a struct varatt_external */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
@@ -338,10 +367,24 @@ typedef struct
 
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
 	do { \
+		/* If desired, keep or expand the Assert checks for known methods: */ \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
+				(cm) == TOAST_LZ4_COMPRESSION_ID || \
+				(cm) == TOAST_ZSTD_COMPRESSION_ID); \
+		if ((cm) < TOAST_ZSTD_COMPRESSION_ID) \
+		{ \
+			/* Store the actual method in va_extinfo */ \
+			(toast_pointer).va_extinfo = (uint32)(len) \
+				| ((uint32)(cm) << VARLENA_EXTSIZE_BITS); \
+		} \
+		else \
+		{ \
+			/* Store VARLENA_EXTENDED_COMPRESSION_FLAG in the top bits, \
+				meaning "extended" method. */ \
+			(toast_pointer).va_extinfo = (uint32)(len) | \
+				((uint32)VARLENA_EXTENDED_COMPRESSION_FLAG \
+						<< VARLENA_EXTSIZE_BITS); \
+		} \
 	} while (0)
 
 /*

base-commit: 7c872849407730fa01e2c13b2d47483bc3ff6e7e
-- 
2.47.1

v11-0002-Zstd-compression-and-decompression-routines-incl.patchapplication/octet-stream; name=v11-0002-Zstd-compression-and-decompression-routines-incl.patchDownload
From a72c8e8c62e4d5bb8b9de40faf85a8a3422f87dc Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Mon, 14 Apr 2025 21:30:03 +0000
Subject: [PATCH v11 2/7] Zstd compression and decompression routines,
 including new catalog table pg_zstd_dictionaries to store zstd dictionaries.

---
 contrib/amcheck/verify_heapam.c               |   1 +
 doc/src/sgml/catalogs.sgml                    |  55 +++
 src/backend/access/brin/brin_tuple.c          |  11 +-
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/indextuple.c        |   5 +-
 src/backend/access/common/reloptions.c        |  36 +-
 src/backend/access/common/toast_compression.c | 328 +++++++++++++++++-
 src/backend/access/common/toast_internals.c   |  49 ++-
 src/backend/access/table/toast_helper.c       |  15 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |   2 +
 src/backend/catalog/catalog.c                 |   4 +
 src/backend/catalog/dependency.c              |   2 +
 src/backend/catalog/meson.build               |   1 +
 src/backend/catalog/objectaddress.c           |  76 ++++
 src/backend/catalog/pg_zstd_dictionaries.c    |  56 +++
 src/backend/commands/dropcmds.c               |   1 +
 src/backend/commands/event_trigger.c          |   2 +
 src/backend/commands/seclabel.c               |   1 +
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/psql/describe.c                       |   5 +-
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/access/toast_compression.h        |  26 +-
 src/include/access/toast_helper.h             |   2 +
 src/include/access/toast_internals.h          |  44 ++-
 src/include/catalog/Makefile                  |   3 +-
 src/include/catalog/meson.build               |   1 +
 src/include/catalog/pg_zstd_dictionaries.h    |  48 +++
 src/include/nodes/parsenodes.h                |   1 +
 src/include/utils/attoptcache.h               |   6 +
 src/test/regress/expected/compression.out     |   5 +-
 src/test/regress/expected/compression_1.out   |   3 +
 src/test/regress/sql/compression.sql          |   1 +
 src/tools/pgindent/typedefs.list              |   2 +
 36 files changed, 774 insertions(+), 45 deletions(-)
 create mode 100644 src/backend/catalog/pg_zstd_dictionaries.c
 create mode 100644 src/include/catalog/pg_zstd_dictionaries.h

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 2152d8ee577..74991373da9 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1792,6 +1792,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index cbd4e40a320..ab44031d726 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -369,6 +369,12 @@
       <entry><link linkend="catalog-pg-user-mapping"><structname>pg_user_mapping</structname></link></entry>
       <entry>mappings of users to foreign servers</entry>
      </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-zstd-dictionaries"><structname>pg_zstd_dictionaries</structname></link></entry>
+      <entry>Zstandard dictionaries</entry>
+     </row>
+
     </tbody>
    </tgroup>
   </table>
@@ -9779,4 +9785,53 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </table>
  </sect1>
 
+
+<sect1 id="catalog-pg-zstd-dictionaries">
+  <title><structname>pg_zstd_dictionaries</structname></title>
+
+  <indexterm zone="catalog-pg-zstd-dictionaries">
+    <primary>pg_zstd_dictionaries</primary>
+  </indexterm>
+
+  <para>
+    The catalog <structname>pg_zstd_dictionaries</structname> maintains the dictionaries essential for Zstandard compression and decompression.
+  </para>
+
+  <table>
+    <title><structname>pg_zstd_dictionaries</structname> Columns</title>
+    <tgroup cols="1">
+      <thead>
+        <row>
+          <entry role="catalog_table_entry">
+            <para role="column_definition">Column Type</para>
+            <para>Description</para>
+          </entry>
+        </row>
+      </thead>
+      <tbody>
+        <row>
+          <entry role="catalog_table_entry">
+            <para role="column_definition">
+              <structfield>dictid</structfield> <type>oid</type>
+            </para>
+            <para>
+              Dictionary identifier; a non-null OID that uniquely identifies a dictionary.
+            </para>
+          </entry>
+        </row>
+        <row>
+          <entry role="catalog_table_entry">
+            <para role="column_definition">
+              <structfield>dict</structfield> <type>bytea</type>
+            </para>
+            <para>
+              Variable-length field containing the zstd dictionary data. This field must not be null.
+            </para>
+          </entry>
+        </row>
+      </tbody>
+    </tgroup>
+  </table>
+</sect1>
+
 </chapter>
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..56099acaa89 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -222,10 +222,13 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 				 atttype->typstorage == TYPSTORAGE_MAIN))
 			{
 				Datum		cvalue;
-				char		compression;
+				CompressionInfo cmp;
+
 				Form_pg_attribute att = TupleDescAttr(brdesc->bd_tupdesc,
 													  keyno);
 
+				setup_compression_info(&cmp, att);
+
 				/*
 				 * If the BRIN summary and indexed attribute use the same data
 				 * type and it has a valid compression method, we can use the
@@ -233,11 +236,11 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 				 * default method.
 				 */
 				if (att->atttypid == atttype->type_id)
-					compression = att->attcompression;
+					cmp.cmethod = att->attcompression;
 				else
-					compression = InvalidCompressionMethod;
+					cmp.cmethod = InvalidCompressionMethod;
 
-				cvalue = toast_compress_datum(value, compression);
+				cvalue = toast_compress_datum(value, cmp);
 
 				if (DatumGetPointer(cvalue) != NULL)
 				{
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..b57a9f024c7 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -246,10 +246,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
 				TOAST_PGLZ_COMPRESSION_ID)
@@ -485,6 +485,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +530,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 1986b943a28..cbb57d17799 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -123,9 +123,10 @@ index_form_tuple_context(TupleDesc tupleDescriptor,
 			 att->attstorage == TYPSTORAGE_MAIN))
 		{
 			Datum		cvalue;
+			CompressionInfo cmp;
 
-			cvalue = toast_compress_datum(untoasted_values[i],
-										  att->attcompression);
+			setup_compression_info(&cmp, att);
+			cvalue = toast_compress_datum(untoasted_values[i], cmp);
 
 			if (DatumGetPointer(cvalue) != NULL)
 			{
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 46c1dce222d..345e3dcdb2c 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -34,6 +34,7 @@
 #include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "access/toast_compression.h"
 
 /*
  * Contents of pg_class.reloptions
@@ -381,7 +382,26 @@ static relopt_int intRelOpts[] =
 		},
 		-1, 0, 1024
 	},
-
+	{
+		{
+			"zstd_dict_size",
+			"Max dict size for zstd",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_DICT_SIZE, 0, 112640	/* Max dict size(110 KB), 0
+											 * indicates don't use dictionary
+											 * for compression */
+	},
+	{
+		{
+			"zstd_level",
+			"Set column's ZSTD compression level",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_LEVEL, MIN_ZSTD_LEVEL, MAX_ZSTD_LEVEL
+	},
 	/* list terminator */
 	{{NULL}}
 };
@@ -470,6 +490,15 @@ static relopt_real realRelOpts[] =
 		},
 		0, -1.0, DBL_MAX
 	},
+	{
+		{
+			"dictid",
+			"Current dictid for column",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		InvalidDictId, InvalidDictId, UINT32_MAX
+	},
 	{
 		{
 			"vacuum_cleanup_index_scale_factor",
@@ -2097,7 +2126,10 @@ attribute_reloptions(Datum reloptions, bool validate)
 {
 	static const relopt_parse_elt tab[] = {
 		{"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)},
-		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}
+		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)},
+		{"dictid", RELOPT_TYPE_REAL, offsetof(AttributeOpts, dictid)},
+		{"zstd_dict_size", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_dict_size)},
+		{"zstd_level", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_level)},
 	};
 
 	return (bytea *) build_reloptions(reloptions, validate,
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97e..3b028993f0b 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,19 +17,41 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
+#include "catalog/pg_zstd_dictionaries.h"
+#include "access/toast_internals.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
 
-#define NO_LZ4_SUPPORT() \
+#ifdef USE_ZSTD
+static ZSTD_CCtx *ZstdCompressionCtx = NULL;
+static ZSTD_CDict * ZstdCompressionCtxCDict = NULL;
+static Oid	ZstdCompressionCtxDictID = InvalidDictId;
+
+static ZSTD_DCtx *ZstdDecompressionCtx = NULL;
+static ZSTD_DDict * ZstdDecompressionCtxDDict = NULL;
+static Oid	ZstdDecompressionCtxDictID = InvalidDictId;
+
+#define ZSTD_CHECK_ERROR(zstd_ret, msg) \
+	do { \
+		if (ZSTD_isError(zstd_ret)) \
+			ereport(ERROR, (errmsg("%s: %s", (msg), ZSTD_getErrorName(zstd_ret)))); \
+	} while (0)
+#endif
+
+#define COMPRESSION_METHOD_NOT_SUPPORTED(method) \
 	ereport(ERROR, \
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
-			 errmsg("compression method lz4 not supported"), \
-			 errdetail("This functionality requires the server to be built with lz4 support.")))
+			 errmsg("compression method %s not supported", method), \
+			 errdetail("This functionality requires the server to be built with %s support.", method)))
 
 /*
  * Compress a varlena using PGLZ.
@@ -139,7 +161,7 @@ struct varlena *
 lz4_compress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		valsize;
@@ -182,7 +204,7 @@ struct varlena *
 lz4_decompress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -215,7 +237,7 @@ struct varlena *
 lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -266,7 +288,13 @@ toast_get_compression_id(struct varlena *attr)
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) >= TOAST_ZSTD_COMPRESSION_ID)
+		{
+			struct varlena *compressed_attr = detoast_external_attr(attr);
+
+			cmid = TOAST_COMPRESS_METHOD(compressed_attr);
+		}
+		else
 			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 	}
 	else if (VARATT_IS_COMPRESSED(attr))
@@ -289,10 +317,17 @@ CompressionNameToMethod(const char *compression)
 	else if (strcmp(compression, "lz4") == 0)
 	{
 #ifndef USE_LZ4
-		NO_LZ4_SUPPORT();
+		COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd") == 0)
+	{
+#ifndef USE_ZSTD
+		COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+#endif
+		return TOAST_ZSTD_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -309,8 +344,285 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_COMPRESSION:
+			return "zstd";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
 	}
 }
+
+/* Compress datum using ZSTD with optional dictionary (using cdict) */
+struct varlena *
+zstd_compress_datum(const struct varlena *value, Oid dictid, int zstd_level)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	void	   *dest;
+	size_t		cmp_size,
+				ret;
+
+	/* Create the session CCtx if it hasn't been yet */
+	if (ZstdCompressionCtx == NULL)
+	{
+		ZstdCompressionCtx = ZSTD_createCCtx();
+		if (!ZstdCompressionCtx)
+			ereport(ERROR, (errmsg("could not create ZSTD_CCtx")));
+		ZstdCompressionCtxDictID = InvalidDictId;
+	}
+
+	/* Reset the context to clear any prior state */
+	ret = ZSTD_CCtx_reset(ZstdCompressionCtx, ZSTD_reset_session_only);
+	ZSTD_CHECK_ERROR(ret, "failed to reset ZSTD CCtx");
+
+	/* Set compression level */
+	ret = ZSTD_CCtx_setParameter(ZstdCompressionCtx, ZSTD_c_compressionLevel, zstd_level);
+	ZSTD_CHECK_ERROR(ret, "failed to set ZSTD compression level");
+
+	/* Check and update dictionary if changed */
+	if (ZstdCompressionCtxDictID != dictid)
+	{
+		/* If there's a previous dictionary, detach and free it */
+		if (ZstdCompressionCtxCDict)
+		{
+			ZSTD_freeCDict(ZstdCompressionCtxCDict);
+			ZstdCompressionCtxCDict = NULL;
+		}
+
+		if (dictid != InvalidDictId)
+		{
+			bytea	   *dict_bytea = get_zstd_dict(dictid);
+			const void *dict_buffer = VARDATA_ANY(dict_bytea);
+			uint32		dict_size = VARSIZE_ANY(dict_bytea) - VARHDRSZ;
+			ZSTD_CDict *cdict = ZSTD_createCDict(dict_buffer, dict_size, zstd_level);
+
+			pfree(dict_bytea);
+
+			if (!cdict)
+				ereport(ERROR, (errmsg("Failed to create ZSTD compression dictionary")));
+
+			ret = ZSTD_CCtx_refCDict(ZstdCompressionCtx, cdict);
+			ZSTD_CHECK_ERROR(ret, "failed to load ZSTD dictionary");
+
+			ZstdCompressionCtxCDict = cdict;
+		}
+		else
+		{
+			/* Unload any previously used dictionary by passing NULL */
+			ret = ZSTD_CCtx_refCDict(ZstdCompressionCtx, NULL);
+			ZSTD_CHECK_ERROR(ret, "failed to unload ZSTD dictionary");
+		}
+
+		ZstdCompressionCtxDictID = dictid;
+	}
+
+	/* Allocate space for the compressed varlena (header + data) */
+	compressed = (struct varlena *) palloc(max_size + VARHDRSZ_COMPRESSED_EXT);
+	dest = (char *) compressed + VARHDRSZ_COMPRESSED_EXT;
+
+	cmp_size = ZSTD_compress2(ZstdCompressionCtx,
+							  dest,
+							  max_size,
+							  VARDATA_ANY(value),
+							  valsize);
+
+	if (ZSTD_isError(cmp_size))
+	{
+		pfree(compressed);
+		ZSTD_CHECK_ERROR(cmp_size, "ZSTD compression failed");
+	}
+
+	/*
+	 * If compression did not reduce size, return NULL so that the
+	 * uncompressed data is stored
+	 */
+	if (cmp_size > valsize)
+	{
+		pfree(compressed);
+		return NULL;
+	}
+
+	/* Set the compressed size in the varlena header */
+	SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARHDRSZ_COMPRESSED_EXT);
+	return compressed;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompression routine */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		cmp_size_exhdr = VARSIZE_4B(value) - VARHDRSZ_COMPRESSED_EXT;
+	Oid			dictid = (Oid) VARDATA_COMPRESSED_GET_DICTID(value);
+	struct varlena *result;
+	size_t		uncmp_size,
+				ret;
+
+	if (ZstdDecompressionCtx == NULL)
+	{
+		ZstdDecompressionCtx = ZSTD_createDCtx();
+		if (!ZstdDecompressionCtx)
+			ereport(ERROR, (errmsg("could not create ZSTD_DCtx")));
+		ZstdDecompressionCtxDictID = InvalidDictId;
+	}
+
+	/* Reset the context to clear any prior state */
+	ret = ZSTD_DCtx_reset(ZstdDecompressionCtx, ZSTD_reset_session_only);
+	ZSTD_CHECK_ERROR(ret, "failed to reset ZSTD DCtx");
+
+	if (ZstdDecompressionCtxDictID != dictid)
+	{
+		if (ZstdDecompressionCtxDDict)
+		{
+			ZSTD_freeDDict(ZstdDecompressionCtxDDict);
+			ZstdDecompressionCtxDDict = NULL;
+		}
+
+		if (dictid != InvalidDictId)
+		{
+			bytea	   *dict_bytea = get_zstd_dict(dictid);
+			const void *dict_buffer = VARDATA_ANY(dict_bytea);
+			uint32		dict_size = VARSIZE_ANY(dict_bytea) - VARHDRSZ;
+			ZSTD_DDict *ddict = ZSTD_createDDict(dict_buffer, dict_size);
+
+			pfree(dict_bytea);
+
+			if (!ddict)
+				ereport(ERROR, (errmsg("Failed to create ZSTD decompression dictionary")));
+
+			ret = ZSTD_DCtx_refDDict(ZstdDecompressionCtx, ddict);
+			ZSTD_CHECK_ERROR(ret, "failed to load ZSTD dictionary");
+
+			ZstdDecompressionCtxDDict = ddict;
+		}
+		else
+		{
+			/* Unload any previously used dictionary by passing NULL */
+			ret = ZSTD_DCtx_refDDict(ZstdDecompressionCtx, NULL);
+			ZSTD_CHECK_ERROR(ret, "failed to unload ZSTD dictionary");
+		}
+
+		/* Update the tracked dictionary ID */
+		ZstdDecompressionCtxDictID = dictid;
+	}
+
+	/* Allocate space for the uncompressed data */
+	result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+	uncmp_size = ZSTD_decompressDCtx(ZstdDecompressionCtx,
+									 VARDATA(result),
+									 actual_size_exhdr,
+									 VARDATA_4B_C(value),
+									 cmp_size_exhdr);
+
+	if (ZSTD_isError(uncmp_size))
+	{
+		pfree(result);
+		ZSTD_CHECK_ERROR(uncmp_size, "ZSTD decompression failed");
+	}
+
+	/* Set final size in the varlena header */
+	SET_VARSIZE(result, uncmp_size + VARHDRSZ);
+	return result;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum using the streaming API and optional dictionary */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	Oid			dictid = (Oid) VARDATA_COMPRESSED_GET_DICTID(value);
+	size_t		ret;
+
+	if (ZstdDecompressionCtx == NULL)
+	{
+		ZstdDecompressionCtx = ZSTD_createDCtx();
+		if (!ZstdDecompressionCtx)
+			elog(ERROR, "could not create ZSTD_DCtx");
+		ZstdDecompressionCtxDictID = InvalidDictId;
+	}
+
+	/* Reset the context to clear any prior state */
+	ret = ZSTD_DCtx_reset(ZstdDecompressionCtx, ZSTD_reset_session_only);
+	ZSTD_CHECK_ERROR(ret, "failed to reset ZSTD_DCtx");
+
+	if (ZstdDecompressionCtxDictID != dictid)
+	{
+		if (ZstdDecompressionCtxDDict)
+		{
+			ZSTD_freeDDict(ZstdDecompressionCtxDDict);
+			ZstdDecompressionCtxDDict = NULL;
+		}
+
+		if (dictid != InvalidDictId)
+		{
+			bytea	   *dict_bytea = get_zstd_dict(dictid);
+			const void *dict_buffer = VARDATA_ANY(dict_bytea);
+			uint32		dict_size = VARSIZE_ANY(dict_bytea) - VARHDRSZ;
+			ZSTD_DDict *ddict = ZSTD_createDDict(dict_buffer, dict_size);
+
+			pfree(dict_bytea);
+
+			if (!ddict)
+				ereport(ERROR, (errmsg("Failed to create ZSTD decompression dictionary")));
+
+			ret = ZSTD_DCtx_refDDict(ZstdDecompressionCtx, ddict);
+			ZSTD_CHECK_ERROR(ret, "failed to load ZSTD dictionary");
+
+			ZstdDecompressionCtxDDict = ddict;
+		}
+		else
+		{
+			/* Unload any previously used dictionary by passing NULL */
+			ret = ZSTD_DCtx_refDDict(ZstdDecompressionCtx, NULL);
+			ZSTD_CHECK_ERROR(ret, "failed to unload ZSTD dictionary");
+		}
+
+		/* Update the tracked dictionary ID */
+		ZstdDecompressionCtxDictID = dictid;
+	}
+
+	inBuf.src = (char *) value + VARHDRSZ_COMPRESSED_EXT;
+	inBuf.size = VARSIZE(value) - VARHDRSZ_COMPRESSED_EXT;
+	inBuf.pos = 0;
+
+	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+	outBuf.dst = (char *) result + VARHDRSZ;
+	outBuf.size = slicelength;
+	outBuf.pos = 0;
+
+	/* Common decompression loop */
+	while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+	{
+		ret = ZSTD_decompressStream(ZstdDecompressionCtx, &outBuf, &inBuf);
+		if (ZSTD_isError(ret))
+		{
+			pfree(result);
+			ZSTD_CHECK_ERROR(ret, "zstd decompression failed");
+		}
+	}
+
+	Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+	SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+	return result;
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+	return NULL;
+#endif
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346ce..46f0381b2de 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -25,6 +25,7 @@
 #include "utils/fmgroids.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/attoptcache.h"
 
 static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
 static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
@@ -43,7 +44,7 @@ static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
  * ----------
  */
 Datum
-toast_compress_datum(Datum value, char cmethod)
+toast_compress_datum(Datum value, CompressionInfo cmp)
 {
 	struct varlena *tmp = NULL;
 	int32		valsize;
@@ -55,13 +56,13 @@ toast_compress_datum(Datum value, char cmethod)
 	valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(value));
 
 	/* If the compression method is not valid, use the current default */
-	if (!CompressionMethodIsValid(cmethod))
-		cmethod = default_toast_compression;
+	if (!CompressionMethodIsValid(cmp.cmethod))
+		cmp.cmethod = default_toast_compression;
 
 	/*
 	 * Call appropriate compression routine for the compression method.
 	 */
-	switch (cmethod)
+	switch (cmp.cmethod)
 	{
 		case TOAST_PGLZ_COMPRESSION:
 			tmp = pglz_compress_datum((const struct varlena *) value);
@@ -71,8 +72,12 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			tmp = zstd_compress_datum((const struct varlena *) value, cmp.dictid, cmp.zstd_level);
+			cmid = TOAST_ZSTD_COMPRESSION_ID;
+			break;
 		default:
-			elog(ERROR, "invalid compression method %c", cmethod);
+			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
 
 	if (tmp == NULL)
@@ -90,9 +95,11 @@ toast_compress_datum(Datum value, char cmethod)
 	 */
 	if (VARSIZE(tmp) < valsize - 2)
 	{
+		Oid			dictid = cmp.dictid;
+
 		/* successful compression */
 		Assert(cmid != TOAST_INVALID_COMPRESSION_ID);
-		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid);
+		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid, dictid);
 		return PointerGetDatum(tmp);
 	}
 	else
@@ -654,3 +661,33 @@ get_toast_snapshot(void)
 
 	return &SnapshotToastData;
 }
+
+void
+setup_compression_info(CompressionInfo * info, Form_pg_attribute att)
+{
+	info->cmethod = att->attcompression;
+	info->dictid = InvalidDictId;
+	info->zstd_level = DEFAULT_ZSTD_LEVEL;
+
+	if (att->attcompression == TOAST_ZSTD_COMPRESSION)
+	{
+		AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+		if (aopt != NULL)
+		{
+			info->zstd_level = aopt->zstd_level;
+			/**
+			 * If user marks zstd dict size as 0, then we don't use dict compression
+			 * for this attribute.
+			 */
+			if (aopt->zstd_dict_size != 0)
+			{
+				if (aopt->dictid < InvalidDictId || aopt->dictid > UINT32_MAX)
+					ereport(ERROR,
+							(errcode(ERRCODE_INTERNAL_ERROR),
+							 errmsg("dictid is not in expected range")));
+				info->dictid = (Oid) aopt->dictid;
+			}
+		}
+	}
+}
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d2..f4b1cbe494e 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -51,10 +51,15 @@ toast_tuple_init(ToastTupleContext *ttc)
 		Form_pg_attribute att = TupleDescAttr(tupleDesc, i);
 		struct varlena *old_value;
 		struct varlena *new_value;
+		CompressionInfo cmp;
+
+		setup_compression_info(&cmp, att);
 
 		ttc->ttc_attr[i].tai_colflags = 0;
 		ttc->ttc_attr[i].tai_oldexternal = NULL;
-		ttc->ttc_attr[i].tai_compression = att->attcompression;
+		ttc->ttc_attr[i].tai_compression = cmp.cmethod;
+		ttc->ttc_attr[i].dictid = cmp.dictid;
+		ttc->ttc_attr[i].zstd_level = cmp.zstd_level;
 
 		if (ttc->ttc_oldvalues != NULL)
 		{
@@ -230,7 +235,13 @@ toast_tuple_try_compression(ToastTupleContext *ttc, int attribute)
 	Datum		new_value;
 	ToastAttrInfo *attr = &ttc->ttc_attr[attribute];
 
-	new_value = toast_compress_datum(*value, attr->tai_compression);
+	CompressionInfo cmp = {
+		.cmethod = attr->tai_compression,
+		.dictid = attr->dictid,
+		.zstd_level = attr->zstd_level
+	};
+
+	new_value = toast_compress_datum(*value, cmp);
 
 	if (DatumGetPointer(new_value) != NULL)
 	{
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index c090094ed08..282afbcef5e 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -46,7 +46,8 @@ OBJS = \
 	pg_subscription.o \
 	pg_type.o \
 	storage.o \
-	toasting.o
+	toasting.o \
+	pg_zstd_dictionaries.o
 
 include $(top_srcdir)/src/backend/common.mk
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 9ca8a88dc91..01d70ebc53e 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -2771,6 +2771,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TSPARSER:
 					case OBJECT_TSTEMPLATE:
 					case OBJECT_USER_MAPPING:
+					case OBJECT_ZSTD_DICTIONARY:
 						elog(ERROR, "unsupported object type: %d", objtype);
 				}
 
@@ -2909,6 +2910,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_TSPARSER:
 					case OBJECT_TSTEMPLATE:
 					case OBJECT_USER_MAPPING:
+					case OBJECT_ZSTD_DICTIONARY:
 						elog(ERROR, "unsupported object type: %d", objtype);
 				}
 
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index a6edf614606..d89334f7e87 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_type.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
 #include "miscadmin.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
@@ -381,6 +382,9 @@ IsPinnedObject(Oid classId, Oid objectId)
 	if (classId == DatabaseRelationId)
 		return false;
 
+	if (classId == ZstdDictionariesRelationId)
+		return false;
+
 	/*
 	 * All other initdb-created objects are pinned.  This is overkill (the
 	 * system doesn't really depend on having every last weird datatype, for
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 18316a3968b..0ea61ed1dae 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -65,6 +65,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -1464,6 +1465,7 @@ doDeletion(const ObjectAddress *object, int flags)
 		case EventTriggerRelationId:
 		case TransformRelationId:
 		case AuthMemRelationId:
+		case ZstdDictionariesRelationId:
 			DropObjectById(object);
 			break;
 
diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build
index 1958ea9238a..8f0413189cb 100644
--- a/src/backend/catalog/meson.build
+++ b/src/backend/catalog/meson.build
@@ -34,6 +34,7 @@ backend_sources += files(
   'pg_type.c',
   'storage.c',
   'toasting.c',
+  'pg_zstd_dictionaries.c',
 )
 
 
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index b63fd57dc04..ba3e52e5323 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -62,6 +62,7 @@
 #include "catalog/pg_ts_template.h"
 #include "catalog/pg_type.h"
 #include "catalog/pg_user_mapping.h"
+#include "catalog/pg_zstd_dictionaries.h"
 #include "commands/dbcommands.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
@@ -636,6 +637,20 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_USER_MAPPING,
 		false
 	},
+	{
+		"zstd dictionaries",
+		ZstdDictionariesRelationId,
+		ZstdDictidIndexId,
+		ZSTDDICTIDOID,
+		-1,
+		Anum_pg_zstd_dictionaries_dictid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		OBJECT_ZSTD_DICTIONARY,
+		true
+	},
 };
 
 /*
@@ -831,6 +846,9 @@ static const struct object_type_map
 	},
 	{
 		"statistics object", OBJECT_STATISTIC_EXT
+	},
+	{
+		"zstd dictionary", OBJECT_ZSTD_DICTIONARY
 	}
 };
 
@@ -1127,6 +1145,26 @@ get_object_address(ObjectType objtype, Node *object,
 															 missing_ok);
 				address.objectSubId = 0;
 				break;
+			case OBJECT_ZSTD_DICTIONARY:
+				{
+					Oid			dictid = oidparse(object);
+					HeapTuple	tuple = SearchSysCache1(ZSTDDICTIDOID, ObjectIdGetDatum(dictid));
+
+					if (!HeapTupleIsValid(tuple))
+					{
+						if (!missing_ok)
+							ereport(ERROR,
+									(errcode(ERRCODE_UNDEFINED_OBJECT),
+									 errmsg("zstd dictionary %u does not exist",
+											dictid)));
+					}
+					ReleaseSysCache(tuple);
+
+					address.classId = ZstdDictionariesRelationId;
+					address.objectId = dictid;
+					address.objectSubId = 0;
+					break;
+				}
 				/* no default, to let compiler warn about missing case */
 		}
 
@@ -2172,6 +2210,23 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 					 errmsg("large object OID may not be null")));
 		objnode = (Node *) makeFloat(TextDatumGetCString(elems[0]));
 	}
+	else if (type == OBJECT_ZSTD_DICTIONARY)
+	{
+		Datum	   *elems;
+		bool	   *nulls;
+		int			nelems;
+
+		deconstruct_array_builtin(namearr, TEXTOID, &elems, &nulls, &nelems);
+		if (nelems != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name list length must be exactly %d", 1)));
+		if (nulls[0])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("zstd dictionary OID may not be null")));
+		objnode = (Node *) makeFloat(TextDatumGetCString(elems[0]));
+	}
 	else
 	{
 		name = textarray_to_strvaluelist(namearr);
@@ -2354,6 +2409,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 				break;
 			}
 		case OBJECT_LARGEOBJECT:
+		case OBJECT_ZSTD_DICTIONARY:
 			/* already handled above */
 			break;
 			/* no default, to let compiler warn about missing case */
@@ -2557,6 +2613,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_PUBLICATION_REL:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_ZSTD_DICTIONARY:
 			/* These are currently not supported or don't make sense here. */
 			elog(ERROR, "unsupported object type: %d", (int) objtype);
 			break;
@@ -4067,7 +4124,26 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				ReleaseSysCache(trfTup);
 				break;
 			}
+		case ZstdDictionariesRelationId:
+			{
+				HeapTuple	htup;
+				Form_pg_zstd_dictionaries zstd;
 
+				htup = SearchSysCache1(ZSTDDICTIDOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(htup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "could not find tuple for dictid %u",
+							 object->objectId);
+					break;
+				}
+
+				zstd = (Form_pg_zstd_dictionaries) GETSTRUCT(htup);
+				appendStringInfo(&buffer, _("Dictionary Id %d"), zstd->dictid);
+
+				ReleaseSysCache(htup);
+				break;
+			}
 		default:
 			elog(ERROR, "unsupported object class: %u", object->classId);
 	}
diff --git a/src/backend/catalog/pg_zstd_dictionaries.c b/src/backend/catalog/pg_zstd_dictionaries.c
new file mode 100644
index 00000000000..d5e965c34d0
--- /dev/null
+++ b/src/backend/catalog/pg_zstd_dictionaries.c
@@ -0,0 +1,56 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_zstd_dictionaries.c
+ *	  routines to support manipulation of the pg_zstd_dictionaries relation
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/catalog/pg_zstd_dictionaries.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "catalog/pg_zstd_dictionaries.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
+#include "utils/syscache.h"
+#include "varatt.h"
+
+/*
+ * get_zstd_dict - Fetches the ZSTD dictionary from the catalog
+ *
+ * dictid: The Oid of the dictionary to fetch.
+ *
+ * Returns: A pointer to a bytea containing the dictionary data.
+ */
+bytea *
+get_zstd_dict(Oid dictid)
+{
+	HeapTuple	tuple;
+	Datum		datum;
+	bool		isNull;
+	bytea	   *dict_bytea;
+	Size		bytea_len;
+	bytea	   *result;
+
+	tuple = SearchSysCache1(ZSTDDICTIDOID, ObjectIdGetDatum(dictid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR, (errmsg("Cache lookup failed for dictid %u", dictid)));
+
+	datum = SysCacheGetAttr(ZSTDDICTIDOID, tuple, Anum_pg_zstd_dictionaries_dict, &isNull);
+	if (isNull)
+		ereport(ERROR, (errmsg("Dictionary not found for dictid %u", dictid)));
+
+	dict_bytea = DatumGetByteaP(datum);
+	bytea_len = VARSIZE(dict_bytea);
+
+	result = palloc(bytea_len);
+	memcpy(result, dict_bytea, bytea_len);
+
+	ReleaseSysCache(tuple);
+
+	return result;
+}
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index ceb9a229b63..58bc92d5959 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -508,6 +508,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_PUBLICATION_REL:
 		case OBJECT_TABCONSTRAINT:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_ZSTD_DICTIONARY:
 			/* These are currently not used or needed. */
 			elog(ERROR, "unsupported object type: %d", (int) objtype);
 			break;
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index edc2c988e29..c5c9291c384 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -2186,6 +2186,7 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
 		case OBJECT_VIEW:
+		case OBJECT_ZSTD_DICTIONARY:
 			elog(ERROR, "unsupported object type: %d", (int) objtype);
 	}
 
@@ -2270,6 +2271,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
 		case OBJECT_VIEW:
+		case OBJECT_ZSTD_DICTIONARY:
 			elog(ERROR, "unsupported object type: %d", (int) objtype);
 	}
 
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index cee5d7bbb9c..e4fe6a64b2e 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -92,6 +92,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_TSPARSER:
 		case OBJECT_TSTEMPLATE:
 		case OBJECT_USER_MAPPING:
+		case OBJECT_ZSTD_DICTIONARY:
 			return false;
 
 			/*
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 3e4d5568bde..063780e56dc 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5301,6 +5301,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_COMPRESSION_ID:
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 60b12446a1c..b4b0e44d7ea 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd", TOAST_ZSTD_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 34826d01380..4dd6da32324 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -756,7 +756,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1d08268393e..26951f8f890 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2171,8 +2171,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'z' ? "zstd" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index c916b9299a8..f507f7111c4 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2875,11 +2875,11 @@ match_previous_words(int pattern_id,
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET ( */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "(") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "("))
-		COMPLETE_WITH("n_distinct", "n_distinct_inherited");
+		COMPLETE_WITH("n_distinct", "n_distinct_inherited", "zstd_level", "zstd_dict_size");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION"))
-		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4");
+		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4", "ZSTD");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET EXPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "EXPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "EXPRESSION"))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..fe4fc3db471 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,6 +13,10 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 /*
  * GUC support.
  *
@@ -38,7 +42,8 @@ typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_ZSTD_COMPRESSION_ID = 2,
+	TOAST_INVALID_COMPRESSION_ID = 3
 } ToastCompressionId;
 
 /*
@@ -48,10 +53,24 @@ typedef enum ToastCompressionId
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_COMPRESSION			'z'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
 
+#define InvalidDictId						0
+
+#ifdef USE_ZSTD
+#define DEFAULT_ZSTD_LEVEL					ZSTD_CLEVEL_DEFAULT
+#define MIN_ZSTD_LEVEL						(int)-ZSTD_BLOCKSIZE_MAX
+#define MAX_ZSTD_LEVEL						22
+#define DEFAULT_ZSTD_DICT_SIZE 				(4 * 1024)	/* 4 KB */
+#else
+#define DEFAULT_ZSTD_LEVEL					0
+#define MIN_ZSTD_LEVEL						0
+#define MAX_ZSTD_LEVEL						0
+#define DEFAULT_ZSTD_DICT_SIZE 				0
+#endif
 
 /* pglz compression/decompression routines */
 extern struct varlena *pglz_compress_datum(const struct varlena *value);
@@ -65,6 +84,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd compression/decompression routines */
+extern struct varlena *zstd_compress_datum(const struct varlena *value, Oid dictid, int zstd_level);
+extern struct varlena *zstd_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index e6ab8afffb6..08bf3dfc673 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -33,6 +33,8 @@ typedef struct
 	int32		tai_size;
 	uint8		tai_colflags;
 	char		tai_compression;
+	Oid			dictid;
+	int			zstd_level;
 } ToastAttrInfo;
 
 /*
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..395afe915d8 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -27,25 +27,54 @@ typedef struct toast_compress_header
 								 * external size; see va_extinfo */
 } toast_compress_header;
 
+typedef struct toast_compress_header_ext
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	uint32		tcinfo;			/* 2 bits for compression method and 30 bits
+								 * external size; see va_extinfo */
+	uint32		ext_alg;		/* compression method */
+	Oid			dictid;			/* Dictionary Id */
+}			toast_compress_header_ext;
+
+typedef struct CompressionInfo
+{
+	char		cmethod;
+	Oid			dictid;
+	int			zstd_level;		/* ZSTD compression level */
+}			CompressionInfo;
+
 /*
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
 #define TOAST_COMPRESS_EXTSIZE(ptr) \
 	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
+#define TOAST_COMPRESS_METHOD(PTR) \
+	( ((((toast_compress_header *) (PTR))->tcinfo >> VARLENA_EXTSIZE_BITS) == VARLENA_EXTENDED_COMPRESSION_FLAG ) \
+		? (((toast_compress_header_ext *) (PTR))->ext_alg) \
+		: ( (((toast_compress_header *) (PTR))->tcinfo) >> VARLENA_EXTSIZE_BITS ) )
 
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method, dictid) \
 	do { \
 		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
 		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID || \
+				(cm_method) == TOAST_ZSTD_COMPRESSION_ID); \
+		/* If the compression method is less than TOAST_ZSTD_COMPRESSION_ID, don't use ext_alg */ \
+		if ((cm_method) < TOAST_ZSTD_COMPRESSION_ID) { \
+			((toast_compress_header *) (ptr))->tcinfo = \
+				(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+		} else { \
+			/* For compression methods after lz4, use 'VARLENA_EXTENDED_COMPRESSION_FLAG' \
+				in the top bits of tcinfo to indicate compression algorithm is stored in ext_alg. */ \
+			((toast_compress_header_ext *) (ptr))->tcinfo = \
+			(len) | ((uint32)VARLENA_EXTENDED_COMPRESSION_FLAG << VARLENA_EXTSIZE_BITS); \
+			((toast_compress_header_ext *) (ptr))->ext_alg = (cm_method); \
+			((toast_compress_header_ext *) (ptr))->dictid = (dictid); \
+		} \
 	} while (0)
 
-extern Datum toast_compress_datum(Datum value, char cmethod);
+extern Datum toast_compress_datum(Datum value, CompressionInfo cmp);
 extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
@@ -59,5 +88,6 @@ extern int	toast_open_indexes(Relation toastrel,
 extern void toast_close_indexes(Relation *toastidxs, int num_indexes,
 								LOCKMODE lock);
 extern Snapshot get_toast_snapshot(void);
+extern void setup_compression_info(CompressionInfo * info, Form_pg_attribute att);
 
 #endif							/* TOAST_INTERNALS_H */
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 2bbc7805fe3..1ecd76dd312 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -81,7 +81,8 @@ CATALOG_HEADERS := \
 	pg_publication_namespace.h \
 	pg_publication_rel.h \
 	pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h \
+	pg_zstd_dictionaries.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
 
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index ec1cf467f6f..e9cb6d911cc 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -69,6 +69,7 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_zstd_dictionaries.h',
 ]
 
 # The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_zstd_dictionaries.h b/src/include/catalog/pg_zstd_dictionaries.h
new file mode 100644
index 00000000000..cf847ee2801
--- /dev/null
+++ b/src/include/catalog/pg_zstd_dictionaries.h
@@ -0,0 +1,48 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_zstd_dictionaries.h
+ *	  definition of the "zstd dictionary" system catalog (pg_zstd_dictionaries)
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_zstd_dictionaries.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_ZSTD_DICTIONARIES_H
+#define PG_ZSTD_DICTIONARIES_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
+
+/* ----------------
+ *		pg_zstd_dictionaries definition.  cpp turns this into
+ *		typedef struct FormData_pg_zstd_dictionaries
+ * ----------------
+ */
+CATALOG(pg_zstd_dictionaries,9946,ZstdDictionariesRelationId)
+{
+	Oid			dictid;
+
+	/*
+	 * variable-length fields start here, but we allow direct access to dict
+	 */
+	bytea		dict BKI_FORCE_NOT_NULL;
+} FormData_pg_zstd_dictionaries;
+
+/* Pointer type to a tuple with the format of pg_zstd_dictionaries relation */
+typedef FormData_pg_zstd_dictionaries *Form_pg_zstd_dictionaries;
+
+DECLARE_TOAST_WITH_MACRO(pg_zstd_dictionaries, 9947, 9948, PgZstdDictionariesToastTable, PgZstdDictionariesToastIndex);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_zstd_dictionaries_dictid_index, 9949, ZstdDictidIndexId, pg_zstd_dictionaries, btree(dictid oid_ops));
+
+MAKE_SYSCACHE(ZSTDDICTIDOID, pg_zstd_dictionaries_dictid_index, 128);
+
+extern bytea *get_zstd_dict(Oid dictid);
+
+#endif							/* PG_ZSTD_DICTIONARIES_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4610fc61293..a74c93c020c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2366,6 +2366,7 @@ typedef enum ObjectType
 	OBJECT_TYPE,
 	OBJECT_USER_MAPPING,
 	OBJECT_VIEW,
+	OBJECT_ZSTD_DICTIONARY,
 } ObjectType;
 
 /* ----------------------
diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h
index f684a772af5..ee16bf3a4d4 100644
--- a/src/include/utils/attoptcache.h
+++ b/src/include/utils/attoptcache.h
@@ -21,6 +21,12 @@ typedef struct AttributeOpts
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	float8		n_distinct;
 	float8		n_distinct_inherited;
+	float8		dictid;			/* Oid is a 32-bit unsigned integer, but
+								 * relopt_int is limited to INT_MAX, so it
+								 * cannot represent the full range of Oid
+								 * values. */
+	int			zstd_dict_size;
+	int			zstd_level;
 } AttributeOpts;
 
 extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum);
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 4dd9ee7200d..94495388ade 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -238,10 +238,11 @@ NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd.
+SET default_toast_compression = 'zstd';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 -- test alter compression method
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out
index 7bd7642b4b9..0ce49152176 100644
--- a/src/test/regress/expected/compression_1.out
+++ b/src/test/regress/expected/compression_1.out
@@ -233,6 +233,9 @@ HINT:  Available values: pglz.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
 HINT:  Available values: pglz.
+SET default_toast_compression = 'zstd';
+ERROR:  invalid value for parameter "default_toast_compression": "zstd"
+HINT:  Available values: pglz.
 SET default_toast_compression = 'lz4';
 ERROR:  invalid value for parameter "default_toast_compression": "lz4"
 HINT:  Available values: pglz.
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index 490595fcfb2..e29909558f9 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -102,6 +102,7 @@ CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 SET default_toast_compression = 'I do not exist compression';
+SET default_toast_compression = 'zstd';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d16bc208654..86fb79b2076 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -905,6 +905,7 @@ FormData_pg_ts_parser
 FormData_pg_ts_template
 FormData_pg_type
 FormData_pg_user_mapping
+FormData_pg_zstd_dictionaries
 FormExtraData_pg_attribute
 Form_pg_aggregate
 Form_pg_am
@@ -964,6 +965,7 @@ Form_pg_ts_parser
 Form_pg_ts_template
 Form_pg_type
 Form_pg_user_mapping
+Form_pg_zstd_dictionaries
 FormatNode
 FreeBlockNumberArray
 FreeListData
-- 
2.47.1

v11-0005-generate-and-cleanup-dictionaries-using-vacuum.patchapplication/octet-stream; name=v11-0005-generate-and-cleanup-dictionaries-using-vacuum.patchDownload
From ed65970ccd7953aebc86111333f55f03260885e1 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Mon, 14 Apr 2025 21:51:43 +0000
Subject: [PATCH v11 5/7] generate and cleanup dictionaries using vacuum

---
 src/backend/catalog/pg_zstd_dictionaries.c | 56 ++++++++++++++++++++++
 src/backend/commands/vacuum.c              |  4 ++
 src/include/catalog/pg_zstd_dictionaries.h |  1 +
 3 files changed, 61 insertions(+)

diff --git a/src/backend/catalog/pg_zstd_dictionaries.c b/src/backend/catalog/pg_zstd_dictionaries.c
index 5ae8ed71e48..08a6883ecd4 100644
--- a/src/backend/catalog/pg_zstd_dictionaries.c
+++ b/src/backend/catalog/pg_zstd_dictionaries.c
@@ -568,6 +568,62 @@ cleanup_unused_zstd_dictionaries_internal(void)
 	return dropped_count;
 }
 
+/*
+ * generate_or_cleanup_zstd_dictionaries_for_relation
+ *
+ * Opens the relation identified by relid, iterates over its attributes,
+ * and for each valid (non-dropped, user-defined) attribute, calls
+ * build_zstd_dictionary_internal.
+ */
+void
+generate_or_cleanup_zstd_dictionaries_for_relation(Oid relid)
+{
+	Relation	rel;
+	TupleDesc	tupdesc;
+
+	/* Start a new transaction */
+	StartTransactionCommand();
+
+	/* Push an active snapshot toast want snapshot */
+	PushActiveSnapshot(GetTransactionSnapshot());
+
+	/* Open the relation using table_open (or relation_open) */
+	rel = table_open(relid, AccessShareLock);
+	tupdesc = RelationGetDescr(rel);
+
+	/* Iterate over all attributes of the relation */
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+
+		/* Skip dropped attributes and system columns (attnum <= 0) */
+		if (attr->attisdropped || attr->attnum <= 0)
+			continue;
+
+		/* Call your dictionary-building function for this attribute */
+		build_zstd_dictionary_internal(relid, attr->attnum);
+
+		/*
+		 * If build_zstd_dictionary_internal performs modifications that
+		 * subsequent iterations must see, use CommandCounterIncrement to
+		 * update the visibility of those changes.
+		 */
+		CommandCounterIncrement();
+	}
+
+	/* Close the relation and release the lock */
+	table_close(rel, NoLock);
+
+	/* Cleanup unused zstd dictionaries */
+	cleanup_unused_zstd_dictionaries_internal();
+
+	/* Pop the snapshot to clean up */
+	PopActiveSnapshot();
+
+	/* Commit the transaction */
+	CommitTransactionCommand();
+}
+
 /*
  * get_zstd_dict - Fetches the ZSTD dictionary from the catalog
  *
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index db5da3ce826..87a5708788e 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -37,6 +37,7 @@
 #include "catalog/namespace.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_inherits.h"
+#include "catalog/pg_zstd_dictionaries.h"
 #include "commands/cluster.h"
 #include "commands/defrem.h"
 #include "commands/progress.h"
@@ -2312,6 +2313,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
 		vacuum_rel(toast_relid, NULL, &toast_vacuum_params, bstrategy);
 	}
 
+	/* Generate or cleanup unused ZSTD dictionaries. */
+	generate_or_cleanup_zstd_dictionaries_for_relation(relid);
+
 	/*
 	 * Now release the session-level lock on the main table.
 	 */
diff --git a/src/include/catalog/pg_zstd_dictionaries.h b/src/include/catalog/pg_zstd_dictionaries.h
index cf847ee2801..a1e3948933c 100644
--- a/src/include/catalog/pg_zstd_dictionaries.h
+++ b/src/include/catalog/pg_zstd_dictionaries.h
@@ -44,5 +44,6 @@ DECLARE_UNIQUE_INDEX_PKEY(pg_zstd_dictionaries_dictid_index, 9949, ZstdDictidInd
 MAKE_SYSCACHE(ZSTDDICTIDOID, pg_zstd_dictionaries_dictid_index, 128);
 
 extern bytea *get_zstd_dict(Oid dictid);
+extern void generate_or_cleanup_zstd_dictionaries_for_relation(Oid relid);
 
 #endif							/* PG_ZSTD_DICTIONARIES_H */
-- 
2.47.1

v11-0003-Zstd-dictionary-training-process.patchapplication/octet-stream; name=v11-0003-Zstd-dictionary-training-process.patchDownload
From b63a58c68180872d882b5b3081ea40654e9062b3 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Mon, 14 Apr 2025 21:45:42 +0000
Subject: [PATCH v11 3/7] Zstd dictionary training process

---
 doc/src/sgml/ref/create_type.sgml          |  21 +-
 src/backend/catalog/heap.c                 |  10 +-
 src/backend/catalog/pg_type.c              |  11 +-
 src/backend/catalog/pg_zstd_dictionaries.c | 490 ++++++++++++++++++++-
 src/backend/commands/analyze.c             |   7 +-
 src/backend/commands/typecmds.c            | 107 ++++-
 src/backend/utils/cache/typcache.c         |   2 +
 src/include/catalog/pg_proc.dat            |  35 ++
 src/include/catalog/pg_type.dat            |   4 +-
 src/include/catalog/pg_type.h              |   8 +-
 src/include/parser/analyze.h               |   5 +
 src/include/utils/typcache.h               |   1 +
 src/test/regress/expected/oidjoins.out     |   1 +
 src/tools/pgindent/typedefs.list           |   3 +
 14 files changed, 680 insertions(+), 25 deletions(-)

diff --git a/doc/src/sgml/ref/create_type.sgml b/doc/src/sgml/ref/create_type.sgml
index 994dfc65268..5f9d61db004 100644
--- a/doc/src/sgml/ref/create_type.sgml
+++ b/doc/src/sgml/ref/create_type.sgml
@@ -56,6 +56,7 @@ CREATE TYPE <replaceable class="parameter">name</replaceable> (
     [ , ELEMENT = <replaceable class="parameter">element</replaceable> ]
     [ , DELIMITER = <replaceable class="parameter">delimiter</replaceable> ]
     [ , COLLATABLE = <replaceable class="parameter">collatable</replaceable> ]
+    [ , ZSTD_SAMPLING = <replaceable class="parameter">zstd_sampling_function</replaceable> ]
 )
 
 CREATE TYPE <replaceable class="parameter">name</replaceable>
@@ -211,7 +212,8 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
    <replaceable class="parameter">type_modifier_input_function</replaceable>,
    <replaceable class="parameter">type_modifier_output_function</replaceable>,
    <replaceable class="parameter">analyze_function</replaceable>, and
-   <replaceable class="parameter">subscript_function</replaceable>
+   <replaceable class="parameter">subscript_function</replaceable>, and
+   <replaceable class="parameter">zstd_sampling_function</replaceable>
    are optional.  Generally these functions have to be coded in C
    or another low-level language.
   </para>
@@ -491,6 +493,15 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
    make use of the collation information; this does not happen
    automatically merely by marking the type collatable.
   </para>
+
+  <para>
+    The optional <replaceable class="parameter">zstd_sampling_function</replaceable>
+    performs type-specific sampling for a column of the corresponding data type.
+    By default, for <type>jsonb</type> data type, the function <literal>std_zstd_sampling_for_jsonb</literal> is defined. It attempts to gather samples for a jsonb
+    and returns a sample buffer for zstd dictionary training. The training function must be declared to accept two arguments of type <type>internal</type> and return an <type>internal</type> result.
+    The detailed information for zstd training function is provided in <filename>src/backend/catalog/pg_zstd_dictionaries.c</filename>.
+  </para>
+
   </refsect2>
 
   <refsect2 id="sql-createtype-array" xreflabel="Array Types">
@@ -846,6 +857,14 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
      </para>
     </listitem>
    </varlistentry>
+   <varlistentry>
+    <term><replaceable class="parameter">zstd_sampling</replaceable></term>
+    <listitem>
+        <para>
+        Specifies the name of a function that performs sampling on specific type.
+        </para>
+    </listitem>
+   </varlistentry>
   </variablelist>
  </refsect1>
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fbaed5359ad..9d3a8536e23 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -1071,7 +1071,10 @@ AddNewRelationType(const char *typeName,
 				   -1,			/* typmod */
 				   0,			/* array dimensions for typBaseType */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* rowtypes never have a collation */
+				   InvalidOid,	/* rowtypes never have a collation */
+				   F_COMPOSITE_TYPZSTDSAMPLING	/* generate dictionary
+												 * procedure - default */
+		);
 }
 
 /* --------------------------------
@@ -1394,7 +1397,10 @@ heap_create_with_catalog(const char *relname,
 				   -1,			/* typmod */
 				   0,			/* array dimensions for typBaseType */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* rowtypes never have a collation */
+				   InvalidOid,	/* rowtypes never have a collation */
+				   F_ARRAY_TYPZSTDSAMPLING	/* generate dictionary procedure -
+											 * default */
+			);
 
 		pfree(relarrayname);
 	}
diff --git a/src/backend/catalog/pg_type.c b/src/backend/catalog/pg_type.c
index b36f81afb9d..8ff9b4d327f 100644
--- a/src/backend/catalog/pg_type.c
+++ b/src/backend/catalog/pg_type.c
@@ -120,6 +120,7 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId)
 	values[Anum_pg_type_typtypmod - 1] = Int32GetDatum(-1);
 	values[Anum_pg_type_typndims - 1] = Int32GetDatum(0);
 	values[Anum_pg_type_typcollation - 1] = ObjectIdGetDatum(InvalidOid);
+	values[Anum_pg_type_typzstdsampling - 1] = ObjectIdGetDatum(InvalidOid);
 	nulls[Anum_pg_type_typdefaultbin - 1] = true;
 	nulls[Anum_pg_type_typdefault - 1] = true;
 	nulls[Anum_pg_type_typacl - 1] = true;
@@ -223,7 +224,8 @@ TypeCreate(Oid newTypeOid,
 		   int32 typeMod,
 		   int32 typNDims,		/* Array dimensions for baseType */
 		   bool typeNotNull,
-		   Oid typeCollation)
+		   Oid typeCollation,
+		   Oid zstdSamplingProcedure)
 {
 	Relation	pg_type_desc;
 	Oid			typeObjectId;
@@ -378,6 +380,7 @@ TypeCreate(Oid newTypeOid,
 	values[Anum_pg_type_typtypmod - 1] = Int32GetDatum(typeMod);
 	values[Anum_pg_type_typndims - 1] = Int32GetDatum(typNDims);
 	values[Anum_pg_type_typcollation - 1] = ObjectIdGetDatum(typeCollation);
+	values[Anum_pg_type_typzstdsampling - 1] = ObjectIdGetDatum(zstdSamplingProcedure);
 
 	/*
 	 * initialize the default binary value for this type.  Check for nulls of
@@ -679,6 +682,12 @@ GenerateTypeDependencies(HeapTuple typeTuple,
 		add_exact_object_address(&referenced, addrs_normal);
 	}
 
+	if (OidIsValid(typeForm->typzstdsampling))
+	{
+		ObjectAddressSet(referenced, ProcedureRelationId, typeForm->typzstdsampling);
+		add_exact_object_address(&referenced, addrs_normal);
+	}
+
 	if (OidIsValid(typeForm->typsubscript))
 	{
 		ObjectAddressSet(referenced, ProcedureRelationId, typeForm->typsubscript);
diff --git a/src/backend/catalog/pg_zstd_dictionaries.c b/src/backend/catalog/pg_zstd_dictionaries.c
index d5e965c34d0..58964a600a3 100644
--- a/src/backend/catalog/pg_zstd_dictionaries.c
+++ b/src/backend/catalog/pg_zstd_dictionaries.c
@@ -14,10 +14,498 @@
 #include "postgres.h"
 
 #include "fmgr.h"
+#include "access/table.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class_d.h"
 #include "catalog/pg_zstd_dictionaries.h"
 #include "catalog/pg_zstd_dictionaries_d.h"
+#include "catalog/pg_depend.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_attribute.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
 #include "utils/syscache.h"
-#include "varatt.h"
+#include "access/toast_compression.h"
+#include "utils/attoptcache.h"
+#include "parser/analyze.h"
+#include "nodes/makefuncs.h"
+#include "access/reloptions.h"
+#include "access/genam.h"
+#include "access/htup_details.h"
+#include "access/sdir.h"
+#include "utils/lsyscache.h"
+#include "utils/relcache.h"
+#include "utils/memutils.h"
+#include "utils/varlena.h"
+#include "nodes/pg_list.h"
+#include "utils/array.h"
+#include "utils/rangetypes.h"
+#include "utils/multirangetypes.h"
+#include "utils/snapmgr.h"
+#include "access/xact.h"
+
+#ifdef USE_ZSTD
+#include <zstd.h>
+#include <zdict.h>
+#endif
+
+#define TARGET_ROWS 30000
+
+typedef struct ZstdTrainingData ZstdTrainingData;
+
+struct ZstdTrainingData
+{
+	char	   *sample_buffer;	/* Pointer to the raw sample buffer */
+	size_t	   *sample_sizes;	/* Array of sample sizes */
+	size_t		nitems;			/* Number of sample sizes */
+	size_t		total_size;		/* Running total sample size */
+};
+
+static bool build_zstd_dictionary_internal(Oid relid, AttrNumber attno);
+static Oid	GetNewDictId(Relation relation);
+static bool append_sample(ZstdTrainingData *dict, const char *sample, size_t sample_size);
+static bool sample_varlena_datum(Datum datum, ZstdTrainingData *dict);
+static int	cleanup_unused_zstd_dictionaries_internal(void);
+
+/*
+ * build_zstd_dictionary_internal
+ *   1) Validate that the given (relid, attno) can have a Zstd compression enabled on heap relation
+ *   2) Call the type-specific sampling procedure
+ *   3) Train a dictionary via ZDICT_trainFromBuffer()
+ *   4) Insert dictionary into pg_zstd_dictionaries
+ *   5) Update pg_attribute.attoptions with new dictid
+ */
+pg_attribute_unused()
+static bool
+build_zstd_dictionary_internal(Oid relid, AttrNumber attno)
+{
+#ifndef USE_ZSTD
+	return false;
+#else
+	Relation	rel;
+	Form_pg_attribute att;
+	AttributeOpts *attopt;
+	TypeCacheEntry *typentry;
+	ZstdTrainingData dict = {0};
+	HeapTuple	sample_rows[TARGET_ROWS];
+	int			num_sampled;
+	double		totalrows = 0,
+				totaldeadrows = 0;
+	int			i;
+	size_t		dict_size;
+	void	   *dict_data;
+	Oid			dictid;
+	bytea	   *dict_bytea;
+	Relation	catalogRel,
+				attRel;
+	HeapTuple	tup,
+				atttup,
+				newtuple;
+	Datum		values[Natts_pg_zstd_dictionaries];
+	bool		nulls[Natts_pg_zstd_dictionaries];
+	Datum		attoptionsDatum,
+				newOptions;
+	bool		isnull;
+	Datum		repl_val[Natts_pg_attribute];
+	bool		repl_null[Natts_pg_attribute];
+	bool		repl_repl[Natts_pg_attribute];
+	DefElem    *def;
+	ObjectAddress dictObj,
+				relObj;
+
+	/* Open relation, verify regular table */
+	rel = table_open(relid, AccessShareLock);
+	if (rel->rd_rel->relkind != RELKIND_RELATION)
+		goto fail;
+
+	att = TupleDescAttr(RelationGetDescr(rel), attno - 1);
+	if (att->attcompression != TOAST_ZSTD_COMPRESSION)
+		goto fail;
+
+	/* Check attoptions for user-requested dictionary size, etc. */
+	attopt = get_attribute_options(relid, attno);
+	if (attopt && attopt->zstd_dict_size == 0)
+		goto fail;
+
+	/*
+	 * 2) Look up the type's custom dictionary builder function We'll call it
+	 * to get sample training data.
+	 */
+	typentry = lookup_type_cache(att->atttypid, 0);
+	if (typentry->typlen != -1 || !OidIsValid(typentry->typzstdsampling))
+		goto fail;
+
+	num_sampled = acquire_sample_rows(rel, 0, sample_rows, TARGET_ROWS, &totalrows, &totaldeadrows);
+	if (num_sampled == 0)
+		goto fail;
+
+	for (i = 0; i < num_sampled; i++)
+	{
+		Datum		value;
+
+		value = heap_getattr(sample_rows[i], attno, RelationGetDescr(rel), &isnull);
+
+		if (!isnull && !DatumGetBool(OidFunctionCall2(typentry->typzstdsampling, value, PointerGetDatum(&dict))))
+			break;
+	}
+
+	if (dict.nitems == 0)
+		goto fail;
+
+	/* ZSTD Dictionary training */
+	dict_size = attopt ? attopt->zstd_dict_size : DEFAULT_ZSTD_DICT_SIZE;
+	dict_data = palloc(dict_size);
+
+	dict_size = ZDICT_trainFromBuffer(dict_data, dict_size, dict.sample_buffer, dict.sample_sizes, dict.nitems);
+	if (ZDICT_isError(dict_size))
+	{
+		elog(LOG, "Zstd dictionary training failed: %s", ZDICT_getErrorName(dict_size));
+		goto cleanup_dict;
+	}
+
+	/* Insert dictionary into catalog */
+	dict_bytea = palloc(VARHDRSZ + dict_size);
+	SET_VARSIZE(dict_bytea, VARHDRSZ + dict_size);
+	memcpy(VARDATA(dict_bytea), dict_data, dict_size);
+
+	catalogRel = table_open(ZstdDictionariesRelationId, ShareRowExclusiveLock);
+	dictid = GetNewDictId(catalogRel);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	values[Anum_pg_zstd_dictionaries_dictid - 1] = ObjectIdGetDatum(dictid);
+	values[Anum_pg_zstd_dictionaries_dict - 1] = PointerGetDatum(dict_bytea);
+
+	tup = heap_form_tuple(RelationGetDescr(catalogRel), values, nulls);
+	CatalogTupleInsert(catalogRel, tup);
+
+	heap_freetuple(tup);
+	pfree(dict_bytea);
+	table_close(catalogRel, NoLock);
+
+	/*
+	 * Update pg_attribute.attoptions with "dictid" => dictid so the column
+	 * knows which dictionary to use at compression time.
+	 */
+	attRel = table_open(AttributeRelationId, RowExclusiveLock);
+	atttup = SearchSysCacheAttNum(relid, attno);
+	if (!HeapTupleIsValid(atttup))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("column number %d of relation \"%u\" does not exist",
+						attno, relid)));
+
+	/* Build new attoptions with dictid=... */
+	def = makeDefElem("dictid",
+					  (Node *) makeString(psprintf("%u", dictid)),
+					  -1);
+
+	attoptionsDatum = SysCacheGetAttr(ATTNUM, atttup,
+									  Anum_pg_attribute_attoptions,
+									  &isnull);
+	newOptions = transformRelOptions(isnull ? (Datum) 0 : attoptionsDatum,
+									 list_make1(def),
+									 NULL, NULL,
+									 false, false);
+	/* Validate them (throws error if invalid) */
+	(void) attribute_reloptions(newOptions, true);
+
+	memset(repl_null, false, sizeof(repl_null));
+	memset(repl_repl, false, sizeof(repl_repl));
+
+	repl_val[Anum_pg_attribute_attoptions - 1] = newOptions;
+	repl_repl[Anum_pg_attribute_attoptions - 1] = true;
+
+	newtuple = heap_modify_tuple(atttup, RelationGetDescr(attRel), repl_val, repl_null, repl_repl);
+	CatalogTupleUpdate(attRel, &newtuple->t_self, newtuple);
+
+	heap_freetuple(newtuple);
+	ReleaseSysCache(atttup);
+	table_close(attRel, NoLock);
+
+	/* Record dependency, relation is depended on this dictionary */
+	ObjectAddressSet(dictObj, ZstdDictionariesRelationId, dictid);
+	ObjectAddressSet(relObj, RelationRelationId, relid);
+	recordDependencyOn(&relObj, &dictObj, DEPENDENCY_NORMAL);
+
+	pfree(dict_data);
+	table_close(rel, NoLock);
+	return true;
+
+cleanup_dict:
+	pfree(dict_data);
+fail:
+	table_close(rel, NoLock);
+
+	return false;
+#endif
+}
+
+/*
+ * Acquire a new unique DictId for a relation.
+ *
+ * Assumes the relation is already locked with ShareRowExclusiveLock,
+ * ensuring that concurrent transactions cannot generate duplicate DictIds.
+ */
+pg_attribute_unused()
+static Oid
+GetNewDictId(Relation dictRel)
+{
+	Relation	idxRel;
+	Oid			maxDictId = InvalidOid;
+	Oid			newDictId;
+	SysScanDesc scan;
+	HeapTuple	tuple;
+
+	/*
+	 * Open the index to read existing DictId values.
+	 */
+	idxRel = index_open(ZstdDictidIndexId, AccessShareLock);
+
+	/*
+	 * Retrieve the maximum existing DictId by scanning in reverse order. This
+	 * relies on the index being sorted ascending on dictid, so scanning
+	 * backward finds the largest value first.
+	 */
+	scan = systable_beginscan_ordered(dictRel,
+									  idxRel,
+									  SnapshotSelf,
+									  0, NULL);
+
+	tuple = systable_getnext_ordered(scan, BackwardScanDirection);
+	if (HeapTupleIsValid(tuple))
+	{
+		Datum		value;
+		bool		isNull;
+
+		value = heap_getattr(tuple,
+							 Anum_pg_zstd_dictionaries_dictid,
+							 RelationGetDescr(dictRel),
+							 &isNull);
+		if (!isNull)
+			maxDictId = DatumGetObjectId(value);
+	}
+	systable_endscan_ordered(scan);
+	index_close(idxRel, AccessShareLock);
+
+	/* Propose new DictId one higher than the max found. */
+	newDictId = maxDictId + 1;
+	Assert(newDictId != InvalidDictId);
+
+	if (newDictId <= InvalidDictId || newDictId > UINT32_MAX)
+		ereport(ERROR,
+				(errcode(ERRCODE_INTERNAL_ERROR),
+				 errmsg("dictid is not in expected range")));
+
+	return newDictId;
+}
+
+/*
+ * append_sample
+ *
+ * Given a sample (raw bytes) and its size, append it to the training data.
+ * This function re-allocates (or allocates) the contiguous sample_buffer and
+ * the sample_sizes array. It returns true if the new total allocation does not
+ * exceed MaxAllocSize, false otherwise.
+ */
+static bool
+append_sample(ZstdTrainingData *dict, const char *sample, size_t sample_size)
+{
+	if ((dict->total_size + sample_size) > MaxAllocSize)
+		return false;
+
+	if (dict->sample_buffer == NULL)
+		dict->sample_buffer = palloc(sample_size);
+	else
+		dict->sample_buffer = repalloc(dict->sample_buffer, dict->total_size + sample_size);
+
+	memcpy(dict->sample_buffer + dict->total_size, sample, sample_size);
+	dict->total_size += sample_size;
+
+	if (dict->sample_sizes == NULL)
+		dict->sample_sizes = palloc(sizeof(size_t));
+	else
+		dict->sample_sizes = repalloc(dict->sample_sizes, (dict->nitems + 1) * sizeof(size_t));
+
+	dict->sample_sizes[dict->nitems++] = sample_size;
+
+	return true;
+}
+
+/* Common helper for jsonb and text */
+static bool
+sample_varlena_datum(Datum datum, ZstdTrainingData *dict)
+{
+	struct varlena *attr = (struct varlena *) PG_DETOAST_DATUM(datum);
+
+	return append_sample(dict, VARDATA_ANY(attr), VARSIZE_ANY_EXHDR(attr));
+}
+
+/*
+ * std_zstd_sampling_for_jsonb
+ *
+ * Processes a single jsonb sample.
+ * It detoasts the datum, obtains the raw sample (excluding the header),
+ * and appends it into the provided ZstdTrainingData structure.
+ *
+ * Returns true if the sample was successfully appended, false otherwise.
+ */
+Datum
+std_zstd_sampling_for_jsonb(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_BOOL(sample_varlena_datum(PG_GETARG_DATUM(0), (ZstdTrainingData *) PG_GETARG_POINTER(1)));
+}
+
+/*
+ * std_zstd_sampling_for_text
+ *
+ * Processes a single text sample.
+ * It detoasts the datum, obtains the raw sample (excluding the header),
+ * and appends it into the provided ZstdTrainingData structure.
+ *
+ * Returns true if the sample was successfully appended, false otherwise.
+ */
+Datum
+std_zstd_sampling_for_text(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_BOOL(sample_varlena_datum(PG_GETARG_DATUM(0), (ZstdTrainingData *) PG_GETARG_POINTER(1)));
+}
+
+/*
+ * array_typzstdsampling -- typzstdsampling function for array columns
+ */
+Datum
+array_typzstdsampling(PG_FUNCTION_ARGS)
+{
+	ArrayType  *array = DatumGetArrayTypeP(PG_GETARG_DATUM(0));
+	ZstdTrainingData *dict = (ZstdTrainingData *) PG_GETARG_POINTER(1);
+	Datum	   *elements;
+	bool	   *nulls;
+	int			nelems;
+	TypeCacheEntry *elemCache = lookup_type_cache(ARR_ELEMTYPE(array), 0);
+
+	if (!OidIsValid(elemCache->typzstdsampling))
+		PG_RETURN_BOOL(false);
+
+	deconstruct_array(array, ARR_ELEMTYPE(array), elemCache->typlen, elemCache->typbyval, elemCache->typalign, &elements, &nulls, &nelems);
+
+	for (int j = 0; j < nelems; j++)
+		if (!nulls[j] && !DatumGetBool(OidFunctionCall2(elemCache->typzstdsampling, elements[j], PointerGetDatum(dict))))
+			break;
+
+	pfree(elements);
+	pfree(nulls);
+	PG_RETURN_BOOL(true);
+}
+
+Datum
+range_typzstdsampling(PG_FUNCTION_ARGS)
+{
+	RangeType  *range = DatumGetRangeTypeP(PG_GETARG_DATUM(0));
+	ZstdTrainingData *dict = (ZstdTrainingData *) PG_GETARG_POINTER(1);
+	RangeBound	lower,
+				upper;
+	bool		empty;
+
+	/* Get information about range type; note column might be a domain */
+	TypeCacheEntry *typcache = range_get_typcache(fcinfo, getBaseType(range->rangetypid));
+
+	/* If the type does not supply a builder, skip */
+	if (!OidIsValid(typcache->rngelemtype->typzstdsampling))
+		PG_RETURN_BOOL(false);
+
+	range_deserialize(typcache, range, &lower, &upper, &empty);
+	if (empty)
+		PG_RETURN_BOOL(false);
+
+	OidFunctionCall2(typcache->rngelemtype->typzstdsampling, lower.val, PointerGetDatum(dict));
+
+	OidFunctionCall2(typcache->rngelemtype->typzstdsampling, upper.val, PointerGetDatum(dict));
+
+	PG_RETURN_BOOL(true);
+}
+
+Datum
+multirange_typzstdsampling(PG_FUNCTION_ARGS)
+{
+	MultirangeType *mrange = DatumGetMultirangeTypeP(PG_GETARG_DATUM(0));
+	ZstdTrainingData *dict = (ZstdTrainingData *) PG_GETARG_POINTER(1);
+	int32		rangeCount;
+	RangeType **ranges;
+
+	/* Get information about multirange type; note column might be a domain */
+	TypeCacheEntry *typcache = multirange_get_typcache(fcinfo, getBaseType(mrange->multirangetypid));
+
+	/* If the type does not supply a builder, skip */
+	if (!OidIsValid(typcache->rngtype->typzstdsampling))
+		PG_RETURN_BOOL(false);
+
+	/* Deserialize the multirange into an array of RangeType pointers */
+	multirange_deserialize(typcache->rngtype, mrange, &rangeCount, &ranges);
+
+	for (int j = 0; j < rangeCount; j++)
+		if (!DatumGetBool(OidFunctionCall2(typcache->rngtype->typzstdsampling, RangeTypePGetDatum(ranges[j]), PointerGetDatum(dict))))
+			break;
+
+	PG_RETURN_BOOL(true);
+}
+
+Datum
+composite_typzstdsampling(PG_FUNCTION_ARGS)
+{
+	HeapTupleHeader th = DatumGetHeapTupleHeader(PG_GETARG_DATUM(0));
+	ZstdTrainingData *dict = (ZstdTrainingData *) PG_GETARG_POINTER(1);
+
+	TupleDesc	tupdesc = lookup_rowtype_tupdesc(HeapTupleHeaderGetTypeId(th), HeapTupleHeaderGetTypMod(th));
+	HeapTupleData tuple = {.t_data = th,.t_len = HeapTupleHeaderGetDatumLength(th),.t_tableOid = InvalidOid};
+
+	ItemPointerSetInvalid(&tuple.t_self);
+
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+		bool		isnull;
+		Datum		fieldDatum;
+
+		if (attr->attisdropped || attr->atthasmissing)
+			continue;
+
+		fieldDatum = heap_getattr(&tuple, i + 1, tupdesc, &isnull);
+
+		if (!isnull)
+		{
+			/* Look up the type cache entry for the attribute's type */
+			TypeCacheEntry *typcache = lookup_type_cache(attr->atttypid, 0);
+
+			if (OidIsValid(typcache->typzstdsampling) && !DatumGetBool(OidFunctionCall2(typcache->typzstdsampling, fieldDatum, PointerGetDatum(dict))))
+				break;
+		}
+	}
+
+	ReleaseTupleDesc(tupdesc);
+	PG_RETURN_BOOL(true);
+}
+
+Datum
+build_zstd_dict_for_attribute(PG_FUNCTION_ARGS)
+{
+#ifndef USE_ZSTD
+	PG_RETURN_BOOL(false);
+#else
+	text	   *tablename = PG_GETARG_TEXT_PP(0);
+	AttrNumber	attno = PG_GETARG_INT32(1);
+
+	/* Look up table name. */
+	RangeVar   *tablerel = makeRangeVarFromNameList(textToQualifiedNameList(tablename));
+	Oid			tableoid = RangeVarGetRelid(tablerel, NoLock, false);
+
+	bool		ret = build_zstd_dictionary_internal(tableoid, attno);
+
+	PG_RETURN_BOOL(ret);
+#endif
+}
 
 /*
  * get_zstd_dict - Fetches the ZSTD dictionary from the catalog
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 4fffb76e557..abb92a3a4fe 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -55,7 +55,7 @@
 #include "utils/sortsupport.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
-
+#include "parser/analyze.h"
 
 /* Per-index data for ANALYZE */
 typedef struct AnlIndexData
@@ -85,9 +85,6 @@ static void compute_index_stats(Relation onerel, double totalrows,
 								MemoryContext col_context);
 static VacAttrStats *examine_attribute(Relation onerel, int attnum,
 									   Node *index_expr);
-static int	acquire_sample_rows(Relation onerel, int elevel,
-								HeapTuple *rows, int targrows,
-								double *totalrows, double *totaldeadrows);
 static int	compare_rows(const void *a, const void *b, void *arg);
 static int	acquire_inherited_sample_rows(Relation onerel, int elevel,
 										  HeapTuple *rows, int targrows,
@@ -1195,7 +1192,7 @@ block_sampling_read_stream_next(ReadStream *stream,
  * block.  The previous sampling method put too much credence in the row
  * density near the start of the table.
  */
-static int
+int
 acquire_sample_rows(Relation onerel, int elevel,
 					HeapTuple *rows, int targrows,
 					double *totalrows, double *totaldeadrows)
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 45ae7472ab5..8014ee507a7 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -95,6 +95,7 @@ typedef struct
 	bool		updateTypmodout;
 	bool		updateAnalyze;
 	bool		updateSubscript;
+	bool		updateZstdSampling;
 	/* New values for relevant attributes */
 	char		storage;
 	Oid			receiveOid;
@@ -103,6 +104,7 @@ typedef struct
 	Oid			typmodoutOid;
 	Oid			analyzeOid;
 	Oid			subscriptOid;
+	Oid			zstdSamplingOid;
 } AlterTypeRecurseParams;
 
 /* Potentially set by pg_upgrade_support functions */
@@ -122,6 +124,7 @@ static Oid	findTypeSendFunction(List *procname, Oid typeOid);
 static Oid	findTypeTypmodinFunction(List *procname);
 static Oid	findTypeTypmodoutFunction(List *procname);
 static Oid	findTypeAnalyzeFunction(List *procname, Oid typeOid);
+static Oid	findTypeZstdSamplingFunction(List *procname, Oid typeOid);
 static Oid	findTypeSubscriptingFunction(List *procname, Oid typeOid);
 static Oid	findRangeSubOpclass(List *opcname, Oid subtype);
 static Oid	findRangeCanonicalFunction(List *procname, Oid typeOid);
@@ -162,6 +165,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	List	   *typmodoutName = NIL;
 	List	   *analyzeName = NIL;
 	List	   *subscriptName = NIL;
+	List	   *zstdSamplingName = NIL;
 	char		category = TYPCATEGORY_USER;
 	bool		preferred = false;
 	char		delimiter = DEFAULT_TYPDELIM;
@@ -190,6 +194,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	DefElem    *alignmentEl = NULL;
 	DefElem    *storageEl = NULL;
 	DefElem    *collatableEl = NULL;
+	DefElem    *zstdSamplingEl = NULL;
 	Oid			inputOid;
 	Oid			outputOid;
 	Oid			receiveOid = InvalidOid;
@@ -198,6 +203,7 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	Oid			typmodoutOid = InvalidOid;
 	Oid			analyzeOid = InvalidOid;
 	Oid			subscriptOid = InvalidOid;
+	Oid			zstdSamplingOid = InvalidOid;
 	char	   *array_type;
 	Oid			array_oid;
 	Oid			typoid;
@@ -323,6 +329,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 			defelp = &storageEl;
 		else if (strcmp(defel->defname, "collatable") == 0)
 			defelp = &collatableEl;
+		else if (strcmp(defel->defname, "zstd_sampling") == 0)
+			defelp = &zstdSamplingEl;
 		else
 		{
 			/* WARNING, not ERROR, for historical backwards-compatibility */
@@ -455,6 +463,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	}
 	if (collatableEl)
 		collation = defGetBoolean(collatableEl) ? DEFAULT_COLLATION_OID : InvalidOid;
+	if (zstdSamplingEl)
+		zstdSamplingName = defGetQualifiedName(zstdSamplingEl);
 
 	/*
 	 * make sure we have our required definitions
@@ -516,6 +526,15 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 					 errmsg("element type cannot be specified without a subscripting function")));
 	}
 
+	if (zstdSamplingName)
+	{
+		if (internalLength != -1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("type zstd_sampling function must be specified only if data type is variable length.")));
+		zstdSamplingOid = findTypeZstdSamplingFunction(zstdSamplingName, typoid);
+	}
+
 	/*
 	 * Check permissions on functions.  We choose to require the creator/owner
 	 * of a type to also own the underlying functions.  Since creating a type
@@ -550,6 +569,9 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 	if (analyzeOid && !object_ownercheck(ProcedureRelationId, analyzeOid, GetUserId()))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
 					   NameListToString(analyzeName));
+	if (zstdSamplingOid && !object_ownercheck(ProcedureRelationId, zstdSamplingOid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
+					   NameListToString(zstdSamplingName));
 	if (subscriptOid && !object_ownercheck(ProcedureRelationId, subscriptOid, GetUserId()))
 		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_FUNCTION,
 					   NameListToString(subscriptName));
@@ -601,7 +623,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array Dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   collation);	/* type's collation */
+				   collation,	/* type's collation */
+				   zstdSamplingOid);	/* zstd_sampling procedure */
 	Assert(typoid == address.objectId);
 
 	/*
@@ -643,7 +666,8 @@ DefineType(ParseState *pstate, List *names, List *parameters)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   collation);		/* type's collation */
+			   collation,		/* type's collation */
+			   F_ARRAY_TYPZSTDSAMPLING);	/* zstd_sampling procedure */
 
 	pfree(array_type);
 
@@ -706,6 +730,7 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 	Oid			receiveProcedure;
 	Oid			sendProcedure;
 	Oid			analyzeProcedure;
+	Oid			zstdSamplingOid;
 	bool		byValue;
 	char		category;
 	char		delimiter;
@@ -842,6 +867,9 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 	/* Analysis function */
 	analyzeProcedure = baseType->typanalyze;
 
+	/* Generate dictionary function */
+	zstdSamplingOid = baseType->typzstdsampling;
+
 	/*
 	 * Domains don't need a subscript function, since they are not
 	 * subscriptable on their own.  If the base type is subscriptable, the
@@ -1078,7 +1106,8 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 				   basetypeMod, /* typeMod value */
 				   typNDims,	/* Array dimensions for base type */
 				   typNotNull,	/* Type NOT NULL */
-				   domaincoll); /* type's collation */
+				   domaincoll,	/* type's collation */
+				   zstdSamplingOid);	/* zstd_sampling procedure */
 
 	/*
 	 * Create the array type that goes with it.
@@ -1119,7 +1148,8 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   domaincoll);		/* type's collation */
+			   domaincoll,		/* type's collation */
+			   F_ARRAY_TYPZSTDSAMPLING);	/* zstd_sampling procedure */
 
 	pfree(domainArrayName);
 
@@ -1241,7 +1271,8 @@ DefineEnum(CreateEnumStmt *stmt)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* type's collation */
+				   InvalidOid,	/* type's collation */
+				   InvalidOid); /* generate dictionary procedure - default */
 
 	/* Enter the enum's values into pg_enum */
 	EnumValuesCreate(enumTypeAddr.objectId, stmt->vals);
@@ -1282,7 +1313,8 @@ DefineEnum(CreateEnumStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   InvalidOid);		/* type's collation */
+			   InvalidOid,		/* type's collation */
+			   InvalidOid);		/* generate dictionary procedure - default */
 
 	pfree(enumArrayName);
 
@@ -1583,7 +1615,9 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* type's collation (ranges never have one) */
+				   InvalidOid,	/* type's collation (ranges never have one) */
+				   F_RANGE_TYPZSTDSAMPLING);	/* generate dictionary
+												 * procedure - default */
 	Assert(typoid == InvalidOid || typoid == address.objectId);
 	typoid = address.objectId;
 
@@ -1646,11 +1680,13 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 				   NULL,		/* no binary form available either */
 				   false,		/* never passed by value */
 				   alignment,	/* alignment */
-				   'x',			/* TOAST strategy (always extended) */
+				   TYPSTORAGE_EXTENDED, /* TOAST strategy (always extended) */
 				   -1,			/* typMod (Domains only) */
 				   0,			/* Array dimensions of typbasetype */
 				   false,		/* Type NOT NULL */
-				   InvalidOid); /* type's collation (ranges never have one) */
+				   InvalidOid,	/* type's collation (ranges never have one) */
+				   F_MULTIRANGE_TYPZSTDSAMPLING);	/* generate dictionary
+													 * procedure - default */
 	Assert(multirangeOid == mltrngaddress.objectId);
 
 	/* Create the entry in pg_range */
@@ -1693,7 +1729,9 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   InvalidOid);		/* typcollation */
+			   InvalidOid,		/* typcollation */
+			   F_ARRAY_TYPZSTDSAMPLING);	/* generate dictionary procedure -
+											 * default */
 
 	pfree(rangeArrayName);
 
@@ -1728,11 +1766,13 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt)
 			   NULL,			/* binary default isn't sent either */
 			   false,			/* never passed by value */
 			   alignment,		/* alignment - same as range's */
-			   'x',				/* ARRAY is always toastable */
+			   TYPSTORAGE_EXTENDED, /* ARRAY is always toastable */
 			   -1,				/* typMod (Domains only) */
 			   0,				/* Array dimensions of typbasetype */
 			   false,			/* Type NOT NULL */
-			   InvalidOid);		/* typcollation */
+			   InvalidOid,		/* typcollation */
+			   F_ARRAY_TYPZSTDSAMPLING);	/* generate dictionary procedure -
+											 * default */
 
 	/* And create the constructor functions for this range type */
 	makeRangeConstructors(typeName, typeNamespace, typoid, rangeSubtype);
@@ -2261,6 +2301,31 @@ findTypeAnalyzeFunction(List *procname, Oid typeOid)
 	return procOid;
 }
 
+static Oid
+findTypeZstdSamplingFunction(List *procname, Oid typeOid)
+{
+	Oid			argList[2];
+	Oid			procOid;
+
+	argList[0] = INTERNALOID;
+	argList[1] = INTERNALOID;
+
+	procOid = LookupFuncName(procname, 2, argList, true);
+	if (!OidIsValid(procOid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("function %s does not exist",
+						func_signature_string(procname, 2, NIL, argList))));
+
+	if (get_func_rettype(procOid) != BOOLOID)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+				 errmsg("type build zstd dictionary function %s must return type %s",
+						NameListToString(procname), "internal")));
+
+	return procOid;
+}
+
 static Oid
 findTypeSubscriptingFunction(List *procname, Oid typeOid)
 {
@@ -4444,6 +4509,19 @@ AlterType(AlterTypeStmt *stmt)
 			/* Replacing a subscript function requires superuser. */
 			requireSuper = true;
 		}
+		else if (strcmp(defel->defname, "zstd_sampling") == 0)
+		{
+			if (defel->arg != NULL)
+				atparams.zstdSamplingOid =
+					findTypeZstdSamplingFunction(defGetQualifiedName(defel),
+												 typeOid);
+			else
+				atparams.zstdSamplingOid = InvalidOid;	/* NONE, remove function */
+
+			atparams.updateZstdSampling = true;
+			/* Replacing a canonical function requires superuser. */
+			requireSuper = true;
+		}
 
 		/*
 		 * The rest of the options that CREATE accepts cannot be changed.
@@ -4606,6 +4684,11 @@ AlterTypeRecurse(Oid typeOid, bool isImplicitArray,
 		replaces[Anum_pg_type_typsubscript - 1] = true;
 		values[Anum_pg_type_typsubscript - 1] = ObjectIdGetDatum(atparams->subscriptOid);
 	}
+	if (atparams->updateZstdSampling)
+	{
+		replaces[Anum_pg_type_typzstdsampling - 1] = true;
+		values[Anum_pg_type_typzstdsampling - 1] = ObjectIdGetDatum(atparams->zstdSamplingOid);
+	}
 
 	newtup = heap_modify_tuple(tup, RelationGetDescr(catalog),
 							   values, nulls, replaces);
diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index ae65a1cce06..7cc2d81cf42 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -501,6 +501,7 @@ lookup_type_cache(Oid type_id, int flags)
 		typentry->typelem = typtup->typelem;
 		typentry->typarray = typtup->typarray;
 		typentry->typcollation = typtup->typcollation;
+		typentry->typzstdsampling = typtup->typzstdsampling;
 		typentry->flags |= TCFLAGS_HAVE_PG_TYPE_DATA;
 
 		/* If it's a domain, immediately thread it into the domain cache list */
@@ -547,6 +548,7 @@ lookup_type_cache(Oid type_id, int flags)
 		typentry->typelem = typtup->typelem;
 		typentry->typarray = typtup->typarray;
 		typentry->typcollation = typtup->typcollation;
+		typentry->typzstdsampling = typtup->typzstdsampling;
 		typentry->flags |= TCFLAGS_HAVE_PG_TYPE_DATA;
 
 		ReleaseSysCache(tp);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 62beb71da28..7d2286850dc 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12566,4 +12566,39 @@
   proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+# ZSTD related functions
+{ oid => '9241', descr => 'array typzstdsampling.',
+  proname => 'array_typzstdsampling', provolatile => 'v', prorettype => 'bool',
+  proargtypes => 'internal internal',
+  prosrc => 'array_typzstdsampling' },
+
+{ oid => '9242', descr => 'range typzstdsampling.',
+  proname => 'range_typzstdsampling', provolatile => 'v', prorettype => 'bool',
+  proargtypes => 'internal internal',
+  prosrc => 'range_typzstdsampling' },
+
+{ oid => '9243', descr => 'multirange typzstdsampling.',
+  proname => 'multirange_typzstdsampling', provolatile => 'v', prorettype => 'bool',
+  proargtypes => 'internal internal',
+  prosrc => 'multirange_typzstdsampling' },
+
+{ oid => '9244', descr => 'composite typzstdsampling.',
+  proname => 'composite_typzstdsampling', provolatile => 'v', prorettype => 'bool',
+  proargtypes => 'internal internal',
+  prosrc => 'composite_typzstdsampling' },
+
+{ oid => '9245', descr => 'Build zstd dictionaries for a column.',
+  proname => 'build_zstd_dict_for_attribute', provolatile => 'v', prorettype => 'bool',
+  proargtypes => 'text int4', proparallel => 'u',
+  prosrc => 'build_zstd_dict_for_attribute' },
+
+{ oid => '9247', descr => 'ZSTD standard sampling for jsonb',
+  proname => 'std_zstd_sampling_for_jsonb', provolatile => 'v', prorettype => 'bool',
+  proargtypes => 'internal internal',
+  prosrc => 'std_zstd_sampling_for_jsonb' },
+
+{ oid => '9248', descr => 'ZSTD standard sampling for text',
+  proname => 'std_zstd_sampling_for_text', provolatile => 'v', prorettype => 'bool',
+  proargtypes => 'internal internal',
+  prosrc => 'std_zstd_sampling_for_text' },
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 6dca77e0a22..a151ba33f82 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -83,7 +83,7 @@
   typname => 'text', typlen => '-1', typbyval => 'f', typcategory => 'S',
   typispreferred => 't', typinput => 'textin', typoutput => 'textout',
   typreceive => 'textrecv', typsend => 'textsend', typalign => 'i',
-  typstorage => 'x', typcollation => 'default' },
+  typstorage => 'x', typcollation => 'default', typzstdsampling => 'std_zstd_sampling_for_text' },
 { oid => '26', array_type_oid => '1028',
   descr => 'object identifier(oid), maximum 4 billion',
   typname => 'oid', typlen => '4', typbyval => 't', typcategory => 'N',
@@ -446,7 +446,7 @@
   typname => 'jsonb', typlen => '-1', typbyval => 'f', typcategory => 'U',
   typsubscript => 'jsonb_subscript_handler', typinput => 'jsonb_in',
   typoutput => 'jsonb_out', typreceive => 'jsonb_recv', typsend => 'jsonb_send',
-  typalign => 'i', typstorage => 'x' },
+  typalign => 'i', typstorage => 'x', typzstdsampling => 'std_zstd_sampling_for_jsonb' },
 { oid => '4072', array_type_oid => '4073', descr => 'JSON path',
   typname => 'jsonpath', typlen => '-1', typbyval => 'f', typcategory => 'U',
   typinput => 'jsonpath_in', typoutput => 'jsonpath_out',
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index ff666711a54..6f53b79feda 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -227,6 +227,11 @@ CATALOG(pg_type,1247,TypeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(71,TypeRelati
 	 */
 	Oid			typcollation BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_collation);
 
+	/*
+	 * Custom zstd sampling procedure for the datatype.
+	 */
+	regproc		typzstdsampling BKI_DEFAULT(0) BKI_ARRAY_DEFAULT(array_typzstdsampling) BKI_LOOKUP_OPT(pg_proc);
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 
 	/*
@@ -380,7 +385,8 @@ extern ObjectAddress TypeCreate(Oid newTypeOid,
 								int32 typeMod,
 								int32 typNDims,
 								bool typeNotNull,
-								Oid typeCollation);
+								Oid typeCollation,
+								Oid zstdSamplingProcedure);
 
 extern void GenerateTypeDependencies(HeapTuple typeTuple,
 									 Relation typeCatalog,
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index f29ed03b476..69391e40d0c 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -17,6 +17,7 @@
 #include "nodes/params.h"
 #include "nodes/queryjumble.h"
 #include "parser/parse_node.h"
+#include "access/htup.h"
 
 /* Hook for plugins to get control at end of parse analysis */
 typedef void (*post_parse_analyze_hook_type) (ParseState *pstate,
@@ -65,4 +66,8 @@ extern List *BuildOnConflictExcludedTargetlist(Relation targetrel,
 
 extern SortGroupClause *makeSortGroupClauseForSetOp(Oid rescoltype, bool require_hash);
 
+extern int	acquire_sample_rows(Relation onerel, int elevel,
+								HeapTuple *rows, int targrows,
+								double *totalrows, double *totaldeadrows);
+
 #endif							/* ANALYZE_H */
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 1cb30f1818c..c5bf668e519 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -46,6 +46,7 @@ typedef struct TypeCacheEntry
 	Oid			typelem;
 	Oid			typarray;
 	Oid			typcollation;
+	Oid			typzstdsampling;
 
 	/*
 	 * Information obtained from opfamily entries
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3..09c750d04fa 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -71,6 +71,7 @@ NOTICE:  checking pg_type {typmodout} => pg_proc {oid}
 NOTICE:  checking pg_type {typanalyze} => pg_proc {oid}
 NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
+NOTICE:  checking pg_type {typzstdsampling} => pg_proc {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 86fb79b2076..06a36903a83 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2641,6 +2641,8 @@ STARTUPINFO
 STRLEN
 SV
 SYNCHRONIZATION_BARRIER
+SampleCollector
+SampleEntry
 SampleScan
 SampleScanGetSampleSize_function
 SampleScanState
@@ -3370,6 +3372,7 @@ ZSTD_cParameter
 ZSTD_inBuffer
 ZSTD_outBuffer
 ZstdCompressorState
+ZstdTrainingData
 _SPI_connection
 _SPI_plan
 __m128i
-- 
2.47.1

v11-0007-Some-tests-related-to-zstd-dictionary-based-comp.patchapplication/octet-stream; name=v11-0007-Some-tests-related-to-zstd-dictionary-based-comp.patchDownload
From b98458770e75dd413d1a720bbd8ebf7ca38378d7 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Mon, 14 Apr 2025 21:53:39 +0000
Subject: [PATCH v11 7/7] Some tests related to zstd dictionary based
 compression and decompression

---
 .../zstd-dictionary-build-cleanup.out         | 661 ++++++++++++++++
 .../zstd-dictionary-build-cleanup_2.out       | 709 ++++++++++++++++++
 .../zstd-dictionary-tblcpy-cleanup.out        | 199 +++++
 .../zstd-dictionary-tblcpy-cleanup_2.out      | 161 ++++
 src/test/isolation/isolation_schedule         |   2 +
 .../specs/zstd-dictionary-build-cleanup.spec  |  40 +
 .../specs/zstd-dictionary-tblcpy-cleanup.spec |  46 ++
 .../regress/expected/compression_zstd.out     | 158 ++++
 .../regress/expected/compression_zstd_1.out   | 251 +++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression_zstd.sql     | 106 +++
 11 files changed, 2334 insertions(+), 1 deletion(-)
 create mode 100644 src/test/isolation/expected/zstd-dictionary-build-cleanup.out
 create mode 100644 src/test/isolation/expected/zstd-dictionary-build-cleanup_2.out
 create mode 100644 src/test/isolation/expected/zstd-dictionary-tblcpy-cleanup.out
 create mode 100644 src/test/isolation/expected/zstd-dictionary-tblcpy-cleanup_2.out
 create mode 100644 src/test/isolation/specs/zstd-dictionary-build-cleanup.spec
 create mode 100644 src/test/isolation/specs/zstd-dictionary-tblcpy-cleanup.spec
 create mode 100644 src/test/regress/expected/compression_zstd.out
 create mode 100644 src/test/regress/expected/compression_zstd_1.out
 create mode 100644 src/test/regress/sql/compression_zstd.sql

diff --git a/src/test/isolation/expected/zstd-dictionary-build-cleanup.out b/src/test/isolation/expected/zstd-dictionary-build-cleanup.out
new file mode 100644
index 00000000000..f68c27e3713
--- /dev/null
+++ b/src/test/isolation/expected/zstd-dictionary-build-cleanup.out
@@ -0,0 +1,661 @@
+Parsed test spec with 4 sessions
+
+starting permutation: insert_more build run_cleanup run_droptbl
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+
+starting permutation: insert_more build run_droptbl run_cleanup
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: insert_more run_cleanup build run_droptbl
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+
+starting permutation: insert_more run_cleanup run_droptbl build
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: insert_more run_droptbl build run_cleanup
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: insert_more run_droptbl run_cleanup build
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: build insert_more run_cleanup run_droptbl
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+
+starting permutation: build insert_more run_droptbl run_cleanup
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: build run_cleanup insert_more run_droptbl
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+
+starting permutation: build run_cleanup run_droptbl insert_more
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+
+starting permutation: build run_droptbl insert_more run_cleanup
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: build run_droptbl run_cleanup insert_more
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_cleanup insert_more build run_droptbl
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+
+starting permutation: run_cleanup insert_more run_droptbl build
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_cleanup build insert_more run_droptbl
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+
+starting permutation: run_cleanup build run_droptbl insert_more
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               1
+(1 row)
+
+
+starting permutation: run_cleanup run_droptbl insert_more build
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_cleanup run_droptbl build insert_more
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl insert_more build run_cleanup
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl insert_more run_cleanup build
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl build insert_more run_cleanup
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl build run_cleanup insert_more
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl run_cleanup insert_more build
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl run_cleanup build insert_more
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+ERROR:  relation "messages" does not exist
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
diff --git a/src/test/isolation/expected/zstd-dictionary-build-cleanup_2.out b/src/test/isolation/expected/zstd-dictionary-build-cleanup_2.out
new file mode 100644
index 00000000000..b4d5705b969
--- /dev/null
+++ b/src/test/isolation/expected/zstd-dictionary-build-cleanup_2.out
@@ -0,0 +1,709 @@
+Parsed test spec with 4 sessions
+
+starting permutation: insert_more build run_cleanup run_droptbl
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: insert_more build run_droptbl run_cleanup
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: insert_more run_cleanup build run_droptbl
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: insert_more run_cleanup run_droptbl build
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: insert_more run_droptbl build run_cleanup
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: insert_more run_droptbl run_cleanup build
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: build insert_more run_cleanup run_droptbl
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: build insert_more run_droptbl run_cleanup
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: build run_cleanup insert_more run_droptbl
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: build run_cleanup run_droptbl insert_more
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: build run_droptbl insert_more run_cleanup
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: build run_droptbl run_cleanup insert_more
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_cleanup insert_more build run_droptbl
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_cleanup insert_more run_droptbl build
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_cleanup build insert_more run_droptbl
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_cleanup build run_droptbl insert_more
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_cleanup run_droptbl insert_more build
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_cleanup run_droptbl build insert_more
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl insert_more build run_cleanup
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl insert_more run_cleanup build
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl build insert_more run_cleanup
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl build run_cleanup insert_more
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl run_cleanup insert_more build
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: run_droptbl run_cleanup build insert_more
+step run_droptbl: 
+  DROP TABLE IF EXISTS messages;
+
+step run_cleanup: 
+  SELECT cleanup_unused_zstd_dictionaries();
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+step build: 
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+step insert_more: 
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+
+ERROR:  relation "messages" does not exist
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
diff --git a/src/test/isolation/expected/zstd-dictionary-tblcpy-cleanup.out b/src/test/isolation/expected/zstd-dictionary-tblcpy-cleanup.out
new file mode 100644
index 00000000000..b0cef50c280
--- /dev/null
+++ b/src/test/isolation/expected/zstd-dictionary-tblcpy-cleanup.out
@@ -0,0 +1,199 @@
+Parsed test spec with 2 sessions
+
+starting permutation: build_dict_and_insert insert_into_othertbl build_dict_and_insert insert_into_othertbl
+step build_dict_and_insert: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  SELECT build_zstd_dict_for_attribute('messages_temp', 1);
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+objid        |refobjid
+-------------+--------
+messages_temp|       1
+(1 row)
+
+step insert_into_othertbl: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  INSERT INTO messages_temp_cpy SELECT * from messages_temp;
+  DROP TABLE IF EXISTS messages_temp_ctas; create table messages_temp_ctas as SELECT * from messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas1; create table messages_temp_ctas1 as SELECT * from messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas2; create table messages_temp_ctas2 as SELECT * from messages_temp_ctas1;
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid        |refobjid
+-------------+--------
+messages_temp|       1
+(1 row)
+
+datum_leak: NOTICE:  table "messages_temp_ctas" does not exist, skipping
+datum_leak: NOTICE:  table "messages_temp_ctas1" does not exist, skipping
+datum_leak: NOTICE:  table "messages_temp_ctas2" does not exist, skipping
+objid              |refobjid
+-------------------+--------
+messages_temp      |       1
+messages_temp_cpy  |       1
+messages_temp_ctas |       1
+messages_temp_ctas1|       1
+messages_temp_ctas2|       1
+(5 rows)
+
+step build_dict_and_insert: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  SELECT build_zstd_dict_for_attribute('messages_temp', 1);
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid              |refobjid
+-------------------+--------
+messages_temp      |       1
+messages_temp_cpy  |       1
+messages_temp_ctas |       1
+messages_temp_ctas1|       1
+messages_temp_ctas2|       1
+(5 rows)
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+objid              |refobjid
+-------------------+--------
+messages_temp      |       1
+messages_temp      |       2
+messages_temp_cpy  |       1
+messages_temp_ctas |       1
+messages_temp_ctas1|       1
+messages_temp_ctas2|       1
+(6 rows)
+
+step insert_into_othertbl: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  INSERT INTO messages_temp_cpy SELECT * from messages_temp;
+  DROP TABLE IF EXISTS messages_temp_ctas; create table messages_temp_ctas as SELECT * from messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas1; create table messages_temp_ctas1 as SELECT * from messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas2; create table messages_temp_ctas2 as SELECT * from messages_temp_ctas1;
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid              |refobjid
+-------------------+--------
+messages_temp      |       1
+messages_temp      |       2
+messages_temp_cpy  |       1
+messages_temp_ctas |       1
+messages_temp_ctas1|       1
+messages_temp_ctas2|       1
+(6 rows)
+
+objid              |refobjid
+-------------------+--------
+messages_temp      |       1
+messages_temp      |       2
+messages_temp_cpy  |       1
+messages_temp_cpy  |       2
+messages_temp_ctas |       1
+messages_temp_ctas |       2
+messages_temp_ctas1|       1
+messages_temp_ctas1|       2
+messages_temp_ctas2|       1
+messages_temp_ctas2|       2
+(10 rows)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               2
+(1 row)
+
+
+starting permutation: insert_into_othertbl insert_into_othertbl build_dict_and_insert build_dict_and_insert
+step insert_into_othertbl: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  INSERT INTO messages_temp_cpy SELECT * from messages_temp;
+  DROP TABLE IF EXISTS messages_temp_ctas; create table messages_temp_ctas as SELECT * from messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas1; create table messages_temp_ctas1 as SELECT * from messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas2; create table messages_temp_ctas2 as SELECT * from messages_temp_ctas1;
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+datum_leak: NOTICE:  table "messages_temp_ctas" does not exist, skipping
+datum_leak: NOTICE:  table "messages_temp_ctas1" does not exist, skipping
+datum_leak: NOTICE:  table "messages_temp_ctas2" does not exist, skipping
+objid|refobjid
+-----+--------
+(0 rows)
+
+step insert_into_othertbl: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  INSERT INTO messages_temp_cpy SELECT * from messages_temp;
+  DROP TABLE IF EXISTS messages_temp_ctas; create table messages_temp_ctas as SELECT * from messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas1; create table messages_temp_ctas1 as SELECT * from messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas2; create table messages_temp_ctas2 as SELECT * from messages_temp_ctas1;
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+step build_dict_and_insert: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  SELECT build_zstd_dict_for_attribute('messages_temp', 1);
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+objid        |refobjid
+-------------+--------
+messages_temp|       1
+(1 row)
+
+step build_dict_and_insert: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  SELECT build_zstd_dict_for_attribute('messages_temp', 1);
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid        |refobjid
+-------------+--------
+messages_temp|       1
+(1 row)
+
+build_zstd_dict_for_attribute
+-----------------------------
+t                            
+(1 row)
+
+objid        |refobjid
+-------------+--------
+messages_temp|       1
+messages_temp|       2
+(2 rows)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               2
+(1 row)
+
diff --git a/src/test/isolation/expected/zstd-dictionary-tblcpy-cleanup_2.out b/src/test/isolation/expected/zstd-dictionary-tblcpy-cleanup_2.out
new file mode 100644
index 00000000000..8229150eb64
--- /dev/null
+++ b/src/test/isolation/expected/zstd-dictionary-tblcpy-cleanup_2.out
@@ -0,0 +1,161 @@
+Parsed test spec with 2 sessions
+
+starting permutation: build_dict_and_insert insert_into_othertbl build_dict_and_insert insert_into_othertbl
+step build_dict_and_insert: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  SELECT build_zstd_dict_for_attribute('messages_temp', 1);
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+step insert_into_othertbl: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  INSERT INTO messages_temp_cpy SELECT * from messages_temp;
+  DROP TABLE IF EXISTS messages_temp_ctas; create table messages_temp_ctas as SELECT * from messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas1; create table messages_temp_ctas1 as SELECT * from messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas2; create table messages_temp_ctas2 as SELECT * from messages_temp_ctas1;
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+datum_leak: NOTICE:  table "messages_temp_ctas" does not exist, skipping
+datum_leak: NOTICE:  table "messages_temp_ctas1" does not exist, skipping
+datum_leak: NOTICE:  table "messages_temp_ctas2" does not exist, skipping
+objid|refobjid
+-----+--------
+(0 rows)
+
+step build_dict_and_insert: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  SELECT build_zstd_dict_for_attribute('messages_temp', 1);
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+step insert_into_othertbl: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  INSERT INTO messages_temp_cpy SELECT * from messages_temp;
+  DROP TABLE IF EXISTS messages_temp_ctas; create table messages_temp_ctas as SELECT * from messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas1; create table messages_temp_ctas1 as SELECT * from messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas2; create table messages_temp_ctas2 as SELECT * from messages_temp_ctas1;
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
+
+starting permutation: insert_into_othertbl insert_into_othertbl build_dict_and_insert build_dict_and_insert
+step insert_into_othertbl: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  INSERT INTO messages_temp_cpy SELECT * from messages_temp;
+  DROP TABLE IF EXISTS messages_temp_ctas; create table messages_temp_ctas as SELECT * from messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas1; create table messages_temp_ctas1 as SELECT * from messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas2; create table messages_temp_ctas2 as SELECT * from messages_temp_ctas1;
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+datum_leak: NOTICE:  table "messages_temp_ctas" does not exist, skipping
+datum_leak: NOTICE:  table "messages_temp_ctas1" does not exist, skipping
+datum_leak: NOTICE:  table "messages_temp_ctas2" does not exist, skipping
+objid|refobjid
+-----+--------
+(0 rows)
+
+step insert_into_othertbl: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  INSERT INTO messages_temp_cpy SELECT * from messages_temp;
+  DROP TABLE IF EXISTS messages_temp_ctas; create table messages_temp_ctas as SELECT * from messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas1; create table messages_temp_ctas1 as SELECT * from messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas2; create table messages_temp_ctas2 as SELECT * from messages_temp_ctas1;
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+step build_dict_and_insert: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  SELECT build_zstd_dict_for_attribute('messages_temp', 1);
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+step build_dict_and_insert: 
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  SELECT build_zstd_dict_for_attribute('messages_temp', 1);
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+build_zstd_dict_for_attribute
+-----------------------------
+f                            
+(1 row)
+
+objid|refobjid
+-----+--------
+(0 rows)
+
+cleanup_unused_zstd_dictionaries
+--------------------------------
+                               0
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index e3c669a29c7..fd446e8f357 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -116,3 +116,5 @@ test: serializable-parallel-2
 test: serializable-parallel-3
 test: matview-write-skew
 test: lock-nowait
+test: zstd-dictionary-build-cleanup
+test: zstd-dictionary-tblcpy-cleanup
\ No newline at end of file
diff --git a/src/test/isolation/specs/zstd-dictionary-build-cleanup.spec b/src/test/isolation/specs/zstd-dictionary-build-cleanup.spec
new file mode 100644
index 00000000000..f526ba79d72
--- /dev/null
+++ b/src/test/isolation/specs/zstd-dictionary-build-cleanup.spec
@@ -0,0 +1,40 @@
+# Zstd compression race test for dictionary build and cleanup methods
+
+setup
+{
+  CREATE TABLE messages (content text);
+  DO $$
+    BEGIN
+      ALTER TABLE messages ALTER COLUMN content SET COMPRESSION ZSTD;
+    EXCEPTION WHEN others THEN
+      RAISE WARNING 'Ignoring zstd compression: this build does not support it.';
+    END
+	$$;
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 10);
+}
+
+teardown
+{
+  DROP TABLE IF EXISTS messages;
+  SELECT cleanup_unused_zstd_dictionaries();
+}
+
+session "insert"
+step "insert_more" {
+  INSERT INTO messages SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+}
+
+session "build_dict"
+step "build" {
+  SELECT build_zstd_dict_for_attribute('messages', 1);
+}
+
+session "cleanup"
+step "run_cleanup" {
+  SELECT cleanup_unused_zstd_dictionaries();
+}
+
+session "droptbls"
+step "run_droptbl" {
+  DROP TABLE IF EXISTS messages;
+}
diff --git a/src/test/isolation/specs/zstd-dictionary-tblcpy-cleanup.spec b/src/test/isolation/specs/zstd-dictionary-tblcpy-cleanup.spec
new file mode 100644
index 00000000000..57aabdf1cf7
--- /dev/null
+++ b/src/test/isolation/specs/zstd-dictionary-tblcpy-cleanup.spec
@@ -0,0 +1,46 @@
+# Zstd compression test for dictionary build, zstd table copy and cleanup method
+
+setup
+{
+  CREATE TABLE messages_temp (content text);
+  DO $$
+    BEGIN
+      ALTER TABLE messages_temp ALTER COLUMN content SET COMPRESSION ZSTD;
+    EXCEPTION WHEN others THEN
+      RAISE WARNING 'Ignoring zstd compression: this build does not support it.';
+    END
+	$$;
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 10);
+  CREATE TABLE messages_temp_cpy (like messages_temp INCLUDING ALL);
+}
+
+teardown
+{
+  DROP TABLE IF EXISTS messages_temp;
+  DROP TABLE IF EXISTS messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas1;
+  DROP TABLE IF EXISTS messages_temp_ctas2;
+  SELECT cleanup_unused_zstd_dictionaries();
+}
+
+session "build_dict_and_insert"
+step "build_dict_and_insert" {
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  SELECT build_zstd_dict_for_attribute('messages_temp', 1);
+  INSERT INTO messages_temp SELECT repeat('Random_', 500) FROM generate_series(1, 5);
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+}
+
+session "datum_leak"
+step "insert_into_othertbl" {
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+  INSERT INTO messages_temp_cpy SELECT * from messages_temp;
+  DROP TABLE IF EXISTS messages_temp_ctas; create table messages_temp_ctas as SELECT * from messages_temp_cpy;
+  DROP TABLE IF EXISTS messages_temp_ctas1; create table messages_temp_ctas1 as SELECT * from messages_temp_ctas;
+  DROP TABLE IF EXISTS messages_temp_ctas2; create table messages_temp_ctas2 as SELECT * from messages_temp_ctas1;
+  SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946 order by objid::regclass, refobjid;
+}
+
+permutation build_dict_and_insert insert_into_othertbl build_dict_and_insert insert_into_othertbl
+permutation insert_into_othertbl insert_into_othertbl build_dict_and_insert build_dict_and_insert
diff --git a/src/test/regress/expected/compression_zstd.out b/src/test/regress/expected/compression_zstd.out
new file mode 100644
index 00000000000..8fee9961abe
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd.out
@@ -0,0 +1,158 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd CASCADE;
+NOTICE:  table "cmdata_zstd" does not exist, skipping
+CREATE TABLE cmdata_zstd (
+    f1 TEXT COMPRESSION zstd
+);
+ERROR:  compression method zstd not supported
+DETAIL:  This functionality requires the server to be built with zstd support.
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 10...
+                    ^
+QUERY:  INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004))
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
+-- Train dictionary for f1 column and insert more.
+SELECT build_zstd_dict_for_attribute('cmdata_zstd', 1);
+ build_zstd_dict_for_attribute 
+-------------------------------
+ f
+(1 row)
+
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 10...
+                    ^
+QUERY:  INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004))
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
+SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
+ objid | refobjid 
+-------+----------
+(0 rows)
+
+select dictid from pg_zstd_dictionaries;
+ dictid 
+--------
+(0 rows)
+
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd;
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd
+GROUP BY pg_column_compression(f1);
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 3: FROM cmdata_zstd
+             ^
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 2: FROM cmdata_zstd;
+             ^
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_2;
+NOTICE:  table "cmdata_zstd_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPR...
+                                         ^
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_2;
+DROP TABLE cmdata_zstd_2;
+ERROR:  table "cmdata_zstd_2" does not exist
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd;
+NOTICE:  materialized view "compressmv_zstd" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd AS
+  SELECT f1 FROM cmdata_zstd;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 2:   SELECT f1 FROM cmdata_zstd;
+                         ^
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd;
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd;
+ERROR:  relation "compressmv_zstd" does not exist
+LINE 2: FROM compressmv_zstd;
+             ^
+SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
+ objid | refobjid 
+-------+----------
+(0 rows)
+
+select dictid from pg_zstd_dictionaries;
+ dictid 
+--------
+(0 rows)
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd
+SET f1 = f1 || ' UPDATED';
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 1: UPDATE cmdata_zstd
+               ^
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd;
+ERROR:  relation "cmdata_zstd" does not exist
+LINE 2: FROM cmdata_zstd;
+             ^
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd;
+ERROR:  materialized view "compressmv_zstd" does not exist
+DROP TABLE cmdata_zstd;
+ERROR:  table "cmdata_zstd" does not exist
+--cleanup dictionary
+select cleanup_unused_zstd_dictionaries();
+ cleanup_unused_zstd_dictionaries 
+----------------------------------
+                                0
+(1 row)
+
+SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
+ objid | refobjid 
+-------+----------
+(0 rows)
+
+select dictid from pg_zstd_dictionaries;
+ dictid 
+--------
+(0 rows)
+
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_zstd_1.out b/src/test/regress/expected/compression_zstd_1.out
new file mode 100644
index 00000000000..c1a7936e574
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_1.out
@@ -0,0 +1,251 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd CASCADE;
+NOTICE:  table "cmdata_zstd" does not exist, skipping
+CREATE TABLE cmdata_zstd (
+    f1 TEXT COMPRESSION zstd
+);
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+-- Train dictionary for f1 column and insert more.
+SELECT build_zstd_dict_for_attribute('cmdata_zstd', 1);
+ build_zstd_dict_for_attribute 
+-------------------------------
+ t
+(1 row)
+
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
+    objid    | refobjid 
+-------------+----------
+ cmdata_zstd |        1
+(1 row)
+
+select dictid from pg_zstd_dictionaries;
+ dictid 
+--------
+      1
+(1 row)
+
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd;
+                                      Table "public.cmdata_zstd"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd
+GROUP BY pg_column_compression(f1);
+ compression_method | row_count 
+--------------------+-----------
+ zstd               |        30
+(1 row)
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd;
+                     data_slice                     
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+(30 rows)
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_2;
+NOTICE:  table "cmdata_zstd_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_2;
+                                     Table "public.cmdata_zstd_2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+DROP TABLE cmdata_zstd_2;
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd;
+NOTICE:  materialized view "compressmv_zstd" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd AS
+  SELECT f1 FROM cmdata_zstd;
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd;
+                              Materialized view "public.compressmv_zstd"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1
+   FROM cmdata_zstd;
+
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd;
+ mv_compression 
+----------------
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+(30 rows)
+
+SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
+    objid    | refobjid 
+-------------+----------
+ cmdata_zstd |        1
+(1 row)
+
+select dictid from pg_zstd_dictionaries;
+ dictid 
+--------
+      1
+(1 row)
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd
+SET f1 = f1 || ' UPDATED';
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd;
+ preview 
+---------
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+(30 rows)
+
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd;
+DROP TABLE cmdata_zstd;
+--cleanup dictionary
+select cleanup_unused_zstd_dictionaries();
+ cleanup_unused_zstd_dictionaries 
+----------------------------------
+                                1
+(1 row)
+
+SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
+ objid | refobjid 
+-------+----------
+(0 rows)
+
+select dictid from pg_zstd_dictionaries;
+ dictid 
+--------
+(0 rows)
+
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 0f38caa0d24..4df32357d01 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_zstd memoize stats predicate numa
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression_zstd.sql b/src/test/regress/sql/compression_zstd.sql
new file mode 100644
index 00000000000..894e72da0cb
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd.sql
@@ -0,0 +1,106 @@
+\set HIDE_TOAST_COMPRESSION false
+
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd CASCADE;
+CREATE TABLE cmdata_zstd (
+    f1 TEXT COMPRESSION zstd
+);
+
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+
+-- Train dictionary for f1 column and insert more.
+SELECT build_zstd_dict_for_attribute('cmdata_zstd', 1);
+
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+
+SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
+
+select dictid from pg_zstd_dictionaries;
+
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd;
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd
+GROUP BY pg_column_compression(f1);
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd;
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_2;
+CREATE TABLE cmdata_zstd_2 (LIKE cmdata_zstd INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_2;
+DROP TABLE cmdata_zstd_2;
+
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd;
+CREATE MATERIALIZED VIEW compressmv_zstd AS
+  SELECT f1 FROM cmdata_zstd;
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd;
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd;
+
+SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
+
+select dictid from pg_zstd_dictionaries;
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd
+SET f1 = f1 || ' UPDATED';
+
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd;
+
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd;
+DROP TABLE cmdata_zstd;
+
+--cleanup dictionary
+select cleanup_unused_zstd_dictionaries();
+
+SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
+
+select dictid from pg_zstd_dictionaries;
+
+\set HIDE_TOAST_COMPRESSION true
-- 
2.47.1

v11-0006-pg_dump-pg_upgrade-needed-changes-to-support-new.patchapplication/octet-stream; name=v11-0006-pg_dump-pg_upgrade-needed-changes-to-support-new.patchDownload
From 60625c15c53b0e5e0090f98b2d692f03f9d617cd Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Mon, 14 Apr 2025 21:53:02 +0000
Subject: [PATCH v11 6/7] pg_dump, pg_upgrade needed changes to support new
 zstd catalog

---
 src/bin/pg_dump/pg_dump.c  | 210 ++++++++++++++++++++++++++++++++-----
 src/bin/pg_upgrade/check.c |  30 ++++--
 src/bin/pg_upgrade/info.c  |   2 +-
 3 files changed, 209 insertions(+), 33 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c6e6d3b2b86..539e046cf22 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -53,6 +53,7 @@
 #include "catalog/pg_publication_d.h"
 #include "catalog/pg_subscription_d.h"
 #include "catalog/pg_type_d.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
 #include "common/connect.h"
 #include "common/int.h"
 #include "common/relpath.h"
@@ -3646,6 +3647,144 @@ dumpDatabase(Archive *fout)
 		destroyPQExpBuffer(loOutQry);
 	}
 
+	if (dopt->binary_upgrade && fout->remoteVersion >= 180000)
+	{
+		/**
+		 *  --- Process pg_zstd_dictionaries related operations ---
+		 */
+		{
+			PGresult   *zstd_res = NULL;
+			PQExpBuffer zstdFrozenQry = createPQExpBuffer();
+			PQExpBuffer zstdOutQry = createPQExpBuffer();
+			PQExpBuffer zstdHorizonQry = createPQExpBuffer();
+			int			ii_relfrozenxid,
+						ii_relfilenode,
+						ii_oid,
+						ii_relminmxid;
+
+			/* Build query to fetch pg_class information */
+			appendPQExpBuffer(zstdFrozenQry,
+							  "SELECT relfrozenxid, relminmxid, relfilenode, oid\n"
+							  "FROM pg_catalog.pg_class\n"
+							  "WHERE oid IN (%u, %u, %u, %u);\n",
+							  ZstdDictionariesRelationId,
+							  PgZstdDictionariesToastTable, /* toast table */
+							  PgZstdDictionariesToastIndex, /* toast table index */
+							  ZstdDictidIndexId);	/* index */
+
+			zstd_res = ExecuteSqlQuery(fout, zstdFrozenQry->data, PGRES_TUPLES_OK);
+
+			ii_relfrozenxid = PQfnumber(zstd_res, "relfrozenxid");
+			ii_relminmxid = PQfnumber(zstd_res, "relminmxid");
+			ii_relfilenode = PQfnumber(zstd_res, "relfilenode");
+			ii_oid = PQfnumber(zstd_res, "oid");
+
+			appendPQExpBufferStr(zstdHorizonQry,
+								 "\n-- For binary upgrade, set pg_zstd_dictionaries relfilenode, relfrozenxid, and relminmxid\n");
+			appendPQExpBufferStr(zstdOutQry,
+								 "\n-- For binary upgrade, preserve pg_zstd_dictionaries and related relfilenodes\n");
+
+			/* Loop over each result row and build update statements */
+			for (int i = 0; i < PQntuples(zstd_res); ++i)
+			{
+				Oid			oid = atooid(PQgetvalue(zstd_res, i, ii_oid));
+				Oid			relfilenumber = atooid(PQgetvalue(zstd_res, i, ii_relfilenode));
+
+				appendPQExpBuffer(zstdHorizonQry,
+								  "UPDATE pg_catalog.pg_class\n"
+								  "SET relfrozenxid = '%u', relminmxid = '%u', relfilenode = %u\n"
+								  "WHERE oid = %u;\n",
+								  atooid(PQgetvalue(zstd_res, i, ii_relfrozenxid)),
+								  atooid(PQgetvalue(zstd_res, i, ii_relminmxid)),
+								  relfilenumber,
+								  oid);
+
+				if (oid == ZstdDictionariesRelationId || oid == PgZstdDictionariesToastTable)
+					appendPQExpBuffer(zstdOutQry,
+									  "SELECT pg_catalog.binary_upgrade_set_next_heap_relfilenode('%u'::pg_catalog.oid);\n",
+									  relfilenumber);
+				else if (oid == PgZstdDictionariesToastIndex || oid == ZstdDictidIndexId)
+					appendPQExpBuffer(zstdOutQry,
+									  "SELECT pg_catalog.binary_upgrade_set_next_index_relfilenode('%u'::pg_catalog.oid);\n",
+									  relfilenumber);
+			}
+
+			appendPQExpBufferStr(zstdOutQry, zstdHorizonQry->data);
+
+			ArchiveEntry(fout, nilCatalogId, createDumpId(),
+						 ARCHIVE_OPTS(.tag = "pg_zstd_dictionaries",
+									  .description = "pg_zstd_dictionaries",
+									  .section = SECTION_PRE_DATA,
+									  .createStmt = zstdOutQry->data));
+
+			PQclear(zstd_res);
+			destroyPQExpBuffer(zstdFrozenQry);
+			destroyPQExpBuffer(zstdHorizonQry);
+			destroyPQExpBuffer(zstdOutQry);
+		}
+
+		/**
+		 * --- Process pg_depend related operations ---
+		 */
+		{
+			PGresult   *pgdep_res = NULL;
+			PQExpBuffer pgdepQry = createPQExpBuffer();
+			PQExpBuffer pgdepBuf = createPQExpBuffer();
+			int			i_classid,
+						i_objid,
+						i_objsubid,
+						i_refclassid,
+						i_refobjid,
+						i_refobjsubid,
+						i_deptype;
+
+			/*
+			 * Build query to fetch all dependency rows where refclassid
+			 * equals ZstdDictionariesRelationId
+			 */
+			appendPQExpBuffer(pgdepQry,
+							  "SELECT classid, objid, objsubid, refclassid, refobjid, refobjsubid, deptype\n"
+							  "FROM pg_catalog.pg_depend\n"
+							  "WHERE refclassid = %u;\n",
+							  ZstdDictionariesRelationId);
+
+			pgdep_res = ExecuteSqlQuery(fout, pgdepQry->data, PGRES_TUPLES_OK);
+
+			i_classid = PQfnumber(pgdep_res, "classid");
+			i_objid = PQfnumber(pgdep_res, "objid");
+			i_objsubid = PQfnumber(pgdep_res, "objsubid");
+			i_refclassid = PQfnumber(pgdep_res, "refclassid");
+			i_refobjid = PQfnumber(pgdep_res, "refobjid");
+			i_refobjsubid = PQfnumber(pgdep_res, "refobjsubid");
+			i_deptype = PQfnumber(pgdep_res, "deptype");
+
+			/* Loop over dependency rows and generate INSERT statements */
+			for (int i = 0; i < PQntuples(pgdep_res); i++)
+			{
+				appendPQExpBuffer(pgdepBuf,
+								  "INSERT INTO pg_catalog.pg_depend (classid, objid, objsubid, refclassid, refobjid, refobjsubid, deptype) "
+								  "VALUES (%s, %s, %s, %s, %s, %s, '%s');\n",
+								  PQgetvalue(pgdep_res, i, i_classid),
+								  PQgetvalue(pgdep_res, i, i_objid),
+								  PQgetvalue(pgdep_res, i, i_objsubid),
+								  PQgetvalue(pgdep_res, i, i_refclassid),
+								  PQgetvalue(pgdep_res, i, i_refobjid),
+								  PQgetvalue(pgdep_res, i, i_refobjsubid),
+								  PQgetvalue(pgdep_res, i, i_deptype));
+			}
+
+			ArchiveEntry(fout, nilCatalogId, createDumpId(),
+						 ARCHIVE_OPTS(.tag = "pg_depend_refclassid_9946",
+									  .description = "pg_depend rows for refclassid 9946",
+									  .section = SECTION_DATA,
+									  .createStmt = pgdepBuf->data));
+
+			PQclear(pgdep_res);
+			destroyPQExpBuffer(pgdepQry);
+			destroyPQExpBuffer(pgdepBuf);
+		}
+	}
+
 	PQclear(res);
 
 	free(qdatname);
@@ -9061,29 +9200,32 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
-						 "a.attrelid,\n"
-						 "a.attnum,\n"
-						 "a.attname,\n"
-						 "a.attstattarget,\n"
-						 "a.attstorage,\n"
-						 "t.typstorage,\n"
-						 "a.atthasdef,\n"
-						 "a.attisdropped,\n"
-						 "a.attlen,\n"
-						 "a.attalign,\n"
-						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
-						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
-						 "CASE WHEN a.attcollation <> t.typcollation "
-						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
-						 "pg_catalog.array_to_string(ARRAY("
-						 "SELECT pg_catalog.quote_ident(option_name) || "
-						 "' ' || pg_catalog.quote_literal(option_value) "
-						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
-						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+	appendPQExpBuffer(q,
+					  "SELECT\n"
+					  "  a.attrelid,\n"
+					  "  a.attnum,\n"
+					  "  a.attname,\n"
+					  "  a.attstattarget,\n"
+					  "  a.attstorage,\n"
+					  "  t.typstorage,\n"
+					  "  a.atthasdef,\n"
+					  "  a.attisdropped,\n"
+					  "  a.attlen,\n"
+					  "  a.attalign,\n"
+					  "  a.attislocal,\n"
+					  "  pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+					  "  array_to_string(ARRAY(\n"
+					  "    SELECT x FROM unnest(a.attoptions) AS x %s\n"
+					  "  ), ', ') AS attoptions,\n"
+					  "  CASE WHEN a.attcollation <> t.typcollation"
+					  "       THEN a.attcollation ELSE 0 END AS attcollation,\n"
+					  "  pg_catalog.array_to_string(ARRAY("
+					  "    SELECT pg_catalog.quote_ident(option_name) || ' ' || "
+					  "           pg_catalog.quote_literal(option_value)"
+					  "    FROM pg_catalog.pg_options_to_table(attfdwoptions)"
+					  "    ORDER BY option_name"
+					  "  ), E',\n    ') AS attfdwoptions,\n",
+					  dopt->binary_upgrade ? "" : "WHERE x NOT LIKE 'dictid=%'");
 
 	/*
 	 * Find out any NOT NULL markings for each column.  In 18 and up we read
@@ -12158,12 +12300,14 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 	char	   *typmodout;
 	char	   *typanalyze;
 	char	   *typsubscript;
+	char	   *typzstdsampling;
 	Oid			typreceiveoid;
 	Oid			typsendoid;
 	Oid			typmodinoid;
 	Oid			typmodoutoid;
 	Oid			typanalyzeoid;
 	Oid			typsubscriptoid;
+	Oid			typzstdsamplingoid;
 	char	   *typcategory;
 	char	   *typispreferred;
 	char	   *typdelim;
@@ -12196,10 +12340,18 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 		if (fout->remoteVersion >= 140000)
 			appendPQExpBufferStr(query,
 								 "typsubscript, "
-								 "typsubscript::pg_catalog.oid AS typsubscriptoid ");
+								 "typsubscript::pg_catalog.oid AS typsubscriptoid, ");
+		else
+			appendPQExpBufferStr(query,
+								 "'-' AS typsubscript, 0 AS typsubscriptoid, ");
+
+		if (fout->remoteVersion >= 180000)
+			appendPQExpBufferStr(query,
+								 "typzstdsampling, "
+								 "typzstdsampling::pg_catalog.oid AS typzstdsamplingoid ");
 		else
 			appendPQExpBufferStr(query,
-								 "'-' AS typsubscript, 0 AS typsubscriptoid ");
+								 "'-' AS typzstdsampling, 0 AS typzstdsamplingoid ");
 
 		appendPQExpBufferStr(query, "FROM pg_catalog.pg_type "
 							 "WHERE oid = $1");
@@ -12224,12 +12376,14 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 	typmodout = PQgetvalue(res, 0, PQfnumber(res, "typmodout"));
 	typanalyze = PQgetvalue(res, 0, PQfnumber(res, "typanalyze"));
 	typsubscript = PQgetvalue(res, 0, PQfnumber(res, "typsubscript"));
+	typzstdsampling = PQgetvalue(res, 0, PQfnumber(res, "typzstdsampling"));
 	typreceiveoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typreceiveoid")));
 	typsendoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typsendoid")));
 	typmodinoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typmodinoid")));
 	typmodoutoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typmodoutoid")));
 	typanalyzeoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typanalyzeoid")));
 	typsubscriptoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typsubscriptoid")));
+	typzstdsamplingoid = atooid(PQgetvalue(res, 0, PQfnumber(res, "typzstdsamplingoid")));
 	typcategory = PQgetvalue(res, 0, PQfnumber(res, "typcategory"));
 	typispreferred = PQgetvalue(res, 0, PQfnumber(res, "typispreferred"));
 	typdelim = PQgetvalue(res, 0, PQfnumber(res, "typdelim"));
@@ -12285,7 +12439,8 @@ dumpBaseType(Archive *fout, const TypeInfo *tyinfo)
 		appendPQExpBuffer(q, ",\n    TYPMOD_OUT = %s", typmodout);
 	if (OidIsValid(typanalyzeoid))
 		appendPQExpBuffer(q, ",\n    ANALYZE = %s", typanalyze);
-
+	if (OidIsValid(typzstdsamplingoid))
+		appendPQExpBuffer(q, ",\n    ZSTD_SAMPLING = %s", typzstdsampling);
 	if (strcmp(typcollatable, "t") == 0)
 		appendPQExpBufferStr(q, ",\n    COLLATABLE = true");
 
@@ -17542,6 +17697,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 					case 'l':
 						cmname = "lz4";
 						break;
+					case 'z':
+						cmname = "zstd";
+						break;
 					default:
 						cmname = NULL;
 						break;
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 18c2d652bb6..38cae3ab0bd 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -14,6 +14,7 @@
 #include "fe_utils/string_utils.h"
 #include "pg_upgrade.h"
 #include "common/unicode_version.h"
+#include "catalog/pg_zstd_dictionaries_d.h"
 
 static void check_new_cluster_is_empty(void);
 static void check_is_install_user(ClusterInfo *cluster);
@@ -916,12 +917,29 @@ check_new_cluster_is_empty(void)
 		for (relnum = 0; relnum < rel_arr->nrels;
 			 relnum++)
 		{
-			/* pg_largeobject and its index should be skipped */
-			if (strcmp(rel_arr->rels[relnum].nspname, "pg_catalog") != 0)
-				pg_fatal("New cluster database \"%s\" is not empty: found relation \"%s.%s\"",
-						 new_cluster.dbarr.dbs[dbnum].db_name,
-						 rel_arr->rels[relnum].nspname,
-						 rel_arr->rels[relnum].relname);
+			const char *nspname = rel_arr->rels[relnum].nspname;
+			const char *relname = rel_arr->rels[relnum].relname;
+			Oid			relOid = rel_arr->rels[relnum].reloid;
+
+			/**
+			 * Allow all objects in pg_catalog
+			 * pg_largeobject, pg_zstd_dictionaries and its index should be skipped.
+			 */
+
+			if (strcmp(nspname, "pg_catalog") == 0)
+				continue;
+
+			/**
+			 * Allow the specific toast objects for pg_zstd_dictionaries:
+			 * fixed OIDs should be 9947 (toast table) or 9948 (toast index).
+			 */
+			if (strcmp(nspname, "pg_toast") == 0 && (relOid == PgZstdDictionariesToastTable || relOid == PgZstdDictionariesToastIndex))
+				continue;
+
+			pg_fatal("New cluster database \"%s\" is not empty: found relation \"%s.%s\"",
+					 new_cluster.dbarr.dbs[dbnum].db_name,
+					 nspname,
+					 relname);
 		}
 	}
 }
diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c
index 4b7a56f5b3b..b38c125c77a 100644
--- a/src/bin/pg_upgrade/info.c
+++ b/src/bin/pg_upgrade/info.c
@@ -498,7 +498,7 @@ get_rel_infos_query(void)
 					  "                        'binary_upgrade', 'pg_toast') AND "
 					  "      c.oid >= %u::pg_catalog.oid) OR "
 					  "     (n.nspname = 'pg_catalog' AND "
-					  "      relname IN ('pg_largeobject') ))), ",
+					  "      relname IN ('pg_largeobject', 'pg_zstd_dictionaries') ))), ",
 					  (user_opts.transfer_mode == TRANSFER_MODE_SWAP) ?
 					  ", " CppAsString2(RELKIND_SEQUENCE) : "",
 					  FirstNormalObjectId);
-- 
2.47.1

v11-0004-Dependency-tracking-mechanism-to-track-compresse.patchapplication/octet-stream; name=v11-0004-Dependency-tracking-mechanism-to-track-compresse.patchDownload
From 371865589fe3e456114caff5e1109e273d18c25f Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Mon, 14 Apr 2025 21:50:07 +0000
Subject: [PATCH v11 4/7] Dependency tracking mechanism to track compressed
 datum leaks to unrelated tables

---
 src/backend/catalog/dependency.c           | 105 +++++++++++++++++++++
 src/backend/catalog/pg_zstd_dictionaries.c |  61 ++++++++++++
 src/backend/commands/createas.c            |  16 +---
 src/backend/commands/prepare.c             |  10 +-
 src/backend/executor/nodeModifyTable.c     |   5 +-
 src/include/catalog/dependency.h           |   2 +
 src/include/catalog/pg_proc.dat            |   5 +
 src/include/commands/createas.h            |  12 +++
 8 files changed, 202 insertions(+), 14 deletions(-)

diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 0ea61ed1dae..c9a647d62ca 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -2838,3 +2838,108 @@ DeleteInitPrivs(const ObjectAddress *object)
 
 	table_close(relation, RowExclusiveLock);
 }
+
+/*
+ * inheritZstdDictionaryDependencies - Record dictionary dependencies for a destination table.
+ *
+ * This function receives a list of relation OIDs. For each relation OID, it scans
+ * pg_depend to collect all associated dictids and then creates
+ * equivalent dependency entries for the destination table.
+ */
+void
+inheritZstdDictionaryDependencies(List *relationOids, Oid destRelid)
+{
+	List	   *relids;
+	List	   *src_dictids = NIL;
+	List	   *dest_dictids = NIL;
+	List	   *new_dictids;
+	ListCell   *lc;
+	Relation	depRel;
+	ScanKeyData skey[2];
+	SysScanDesc scan;
+	HeapTuple	tup;
+
+	/* Build and deduplicate the list of all relation OIDs including destRelid */
+	relids = list_copy(relationOids);
+	relids = lappend_oid(relids, destRelid);
+	list_sort(relids, list_oid_cmp);
+	list_deduplicate_oid(relids);
+
+	/** Early exit if only destination relation is present
+	 * During pg upgrade, dictionaries in the database are copied explicitly and their dependencies too.
+	 */
+	if (list_length(relids) == 1 || IsBinaryUpgrade)
+	{
+		list_free(relids);
+		return;
+	}
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+
+	/* Collect source and destination dictionaries from dependency table */
+	foreach(lc, relids)
+	{
+		Oid			relOid = lfirst_oid(lc);
+
+		/* Initialize scan keys for current relation */
+		ScanKeyInit(&skey[0], Anum_pg_depend_classid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(RelationRelationId));
+		ScanKeyInit(&skey[1], Anum_pg_depend_objid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(relOid));
+
+		scan = systable_beginscan(depRel, DependDependerIndexId, true,
+								  NULL, 2, skey);
+
+		while (HeapTupleIsValid(tup = systable_getnext(scan)))
+		{
+			Form_pg_depend dep = (Form_pg_depend) GETSTRUCT(tup);
+
+			/* Interested only in Zstd dictionary dependencies */
+			if (dep->refclassid != ZstdDictionariesRelationId)
+				continue;
+
+			if (dep->objid == destRelid)
+				dest_dictids = list_append_unique_oid(dest_dictids, dep->refobjid);
+			else
+				src_dictids = list_append_unique_oid(src_dictids, dep->refobjid);
+		}
+
+		systable_endscan(scan);
+	}
+
+	table_close(depRel, AccessShareLock);
+
+	/*
+	 * Identify dictionaries from sources not already referenced by
+	 * destination
+	 */
+	new_dictids = list_difference_oid(src_dictids, dest_dictids);
+
+	/* Add new dictionary dependencies to the destination table if necessary */
+	if (new_dictids)
+	{
+		ObjectAddress depender;
+		ObjectAddresses *referenced = new_object_addresses();
+
+		ObjectAddressSet(depender, RelationRelationId, destRelid);
+
+		foreach(lc, new_dictids)
+		{
+			ObjectAddress dictObj;
+
+			ObjectAddressSet(dictObj, ZstdDictionariesRelationId, lfirst_oid(lc));
+			add_exact_object_address(&dictObj, referenced);
+		}
+
+		record_object_address_dependencies(&depender, referenced, DEPENDENCY_NORMAL);
+		free_object_addresses(referenced);
+	}
+
+	/* Clean up temporary lists */
+	list_free(relids);
+	list_free(src_dictids);
+	list_free(dest_dictids);
+	list_free(new_dictids);
+}
diff --git a/src/backend/catalog/pg_zstd_dictionaries.c b/src/backend/catalog/pg_zstd_dictionaries.c
index 58964a600a3..5ae8ed71e48 100644
--- a/src/backend/catalog/pg_zstd_dictionaries.c
+++ b/src/backend/catalog/pg_zstd_dictionaries.c
@@ -507,6 +507,67 @@ build_zstd_dict_for_attribute(PG_FUNCTION_ARGS)
 #endif
 }
 
+Datum
+cleanup_unused_zstd_dictionaries(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32(cleanup_unused_zstd_dictionaries_internal());
+}
+
+static int
+cleanup_unused_zstd_dictionaries_internal(void)
+{
+	Relation	dictRel,
+				depRel;
+	SysScanDesc dictScan,
+				depScan;
+	HeapTuple	tuple;
+	List	   *used_dictids = NIL;
+	int			dropped_count = 0;
+	ScanKeyData depKey;
+
+	/* Open necessary catalog relations */
+	dictRel = table_open(ZstdDictionariesRelationId, ShareRowExclusiveLock);
+	depRel = table_open(DependRelationId, AccessShareLock);
+
+	/* Find dictionary OIDs with dependencies */
+	ScanKeyInit(&depKey,
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(ZstdDictionariesRelationId));
+
+	depScan = systable_beginscan(depRel, DependReferenceIndexId, true, NULL, 1, &depKey);
+	while ((tuple = systable_getnext(depScan)) != NULL)
+	{
+		Form_pg_depend dep = (Form_pg_depend) GETSTRUCT(tuple);
+
+		used_dictids = list_append_unique_oid(used_dictids, dep->refobjid);
+	}
+	systable_endscan(depScan);
+
+	/* Drop unused dictionaries */
+	dictScan = systable_beginscan(dictRel, InvalidOid, false, NULL, 0, NULL);
+	while ((tuple = systable_getnext(dictScan)) != NULL)
+	{
+		Oid			dictid = ((Form_pg_zstd_dictionaries) GETSTRUCT(tuple))->dictid;
+
+		if (!list_member_oid(used_dictids, dictid))
+		{
+			ObjectAddress dictAddr;
+
+			ObjectAddressSet(dictAddr, ZstdDictionariesRelationId, dictid);
+			performDeletion(&dictAddr, DROP_RESTRICT, 0);
+			dropped_count++;
+		}
+	}
+	systable_endscan(dictScan);
+
+	/* Close catalog relations */
+	table_close(depRel, NoLock);
+	table_close(dictRel, NoLock);
+
+	return dropped_count;
+}
+
 /*
  * get_zstd_dict - Fetches the ZSTD dictionary from the catalog
  *
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 0a4155773eb..2361ad77db4 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -47,18 +47,7 @@
 #include "utils/lsyscache.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
-
-typedef struct
-{
-	DestReceiver pub;			/* publicly-known function pointers */
-	IntoClause *into;			/* target relation specification */
-	/* These fields are filled by intorel_startup: */
-	Relation	rel;			/* relation to write to */
-	ObjectAddress reladdr;		/* address of rel, for ExecCreateTableAs */
-	CommandId	output_cid;		/* cmin to insert in output tuples */
-	int			ti_options;		/* table_tuple_insert performance options */
-	BulkInsertState bistate;	/* bulk insert state */
-} DR_intorel;
+#include "catalog/dependency.h"
 
 /* utility functions for CTAS definition creation */
 static ObjectAddress create_ctas_internal(List *attrList, IntoClause *into);
@@ -352,6 +341,9 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 		/* get object address that intorel_startup saved for us */
 		address = ((DR_intorel *) dest)->reladdr;
 
+		/* Inherit zstd dictionary dependencies */
+		inheritZstdDictionaryDependencies(plan->relationOids, address.objectId);
+
 		/* and clean up */
 		ExecutorFinish(queryDesc);
 		ExecutorEnd(queryDesc);
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index bf7d2b2309f..a867f4d7711 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -36,7 +36,7 @@
 #include "utils/builtins.h"
 #include "utils/snapmgr.h"
 #include "utils/timestamp.h"
-
+#include "catalog/dependency.h"
 
 /*
  * The hash table in which prepared queries are stored. This is
@@ -161,6 +161,7 @@ ExecuteQuery(ParseState *pstate,
 	char	   *query_string;
 	int			eflags;
 	long		count;
+	List	   *relationOids = NIL;
 
 	/* Look it up in the hash table */
 	entry = FetchPreparedStatement(stmt->name, true);
@@ -242,7 +243,10 @@ ExecuteQuery(ParseState *pstate,
 		if (intoClause->skipData)
 			count = 0;
 		else
+		{
 			count = FETCH_ALL;
+			relationOids = pstmt->relationOids;
+		}
 	}
 	else
 	{
@@ -258,6 +262,10 @@ ExecuteQuery(ParseState *pstate,
 
 	(void) PortalRun(portal, count, false, dest, dest, qc);
 
+	/* Inherit zstd dictionary dependencies */
+	if (intoClause && count != 0)
+		inheritZstdDictionaryDependencies(relationOids, ((DR_intorel *) dest)->reladdr.objectId);
+
 	PortalDrop(portal, false);
 
 	if (estate)
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 333cbf78343..e6847fd31c7 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -69,7 +69,7 @@
 #include "utils/datum.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
-
+#include "catalog/dependency.h"
 
 typedef struct MTTargetRelLookup
 {
@@ -4416,6 +4416,9 @@ ExecModifyTable(PlanState *pstate)
 	if (estate->es_insert_pending_result_relations != NIL)
 		ExecPendingInserts(estate);
 
+	/* Inherit zstd dictionary dependencies */
+	inheritZstdDictionaryDependencies(estate->es_plannedstmt->relationOids, RelationGetRelid(resultRelInfo->ri_RelationDesc));
+
 	/*
 	 * We're done, but fire AFTER STATEMENT triggers before exiting.
 	 */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 0ea7ccf5243..a1e91f2c8f1 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -225,4 +225,6 @@ extern void shdepDropOwned(List *roleids, DropBehavior behavior);
 
 extern void shdepReassignOwned(List *roleids, Oid newrole);
 
+extern void inheritZstdDictionaryDependencies(List *relationOids, Oid destRelid);
+
 #endif							/* DEPENDENCY_H */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7d2286850dc..c98e9dca653 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12592,6 +12592,11 @@
   proargtypes => 'text int4', proparallel => 'u',
   prosrc => 'build_zstd_dict_for_attribute' },
 
+{ oid => '9246', descr => 'cleanup unused dictionaries.',
+  proname => 'cleanup_unused_zstd_dictionaries', provolatile => 'v', prorettype => 'int4',
+  proargtypes => '', proparallel => 'u',
+  prosrc => 'cleanup_unused_zstd_dictionaries' },
+  
 { oid => '9247', descr => 'ZSTD standard sampling for jsonb',
   proname => 'std_zstd_sampling_for_jsonb', provolatile => 'v', prorettype => 'bool',
   proargtypes => 'internal internal',
diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h
index 90612ebbb0e..2ee78652f28 100644
--- a/src/include/commands/createas.h
+++ b/src/include/commands/createas.h
@@ -19,7 +19,19 @@
 #include "parser/parse_node.h"
 #include "tcop/dest.h"
 #include "utils/queryenvironment.h"
+#include "access/heapam.h"
 
+typedef struct
+{
+	DestReceiver pub;			/* publicly-known function pointers */
+	IntoClause *into;			/* target relation specification */
+	/* These fields are filled by intorel_startup: */
+	Relation	rel;			/* relation to write to */
+	ObjectAddress reladdr;		/* address of rel, for ExecCreateTableAs */
+	CommandId	output_cid;		/* cmin to insert in output tuples */
+	int			ti_options;		/* table_tuple_insert performance options */
+	BulkInsertState bistate;	/* bulk insert state */
+} DR_intorel;
 
 extern ObjectAddress ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt,
 									   ParamListInfo params, QueryEnvironment *queryEnv,
-- 
2.47.1

#19Robert Haas
robertmhaas@gmail.com
In reply to: Nikhil Kumar Veldanda (#18)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Tue, Apr 15, 2025 at 2:13 PM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Addressing Compressed Datum Leaks problem (via CTAS, INSERT INTO ... SELECT ...)

As compressed datums can be copied to other unrelated tables via CTAS,
INSERT INTO ... SELECT, or CREATE TABLE ... EXECUTE, I’ve introduced a
method inheritZstdDictionaryDependencies. This method is invoked at
the end of such statements and ensures that any dictionary
dependencies from source tables are copied to the destination table.
We determine the set of source tables using the relationOids field in
PlannedStmt.

With the disclaimer that I haven't opened the patch or thought
terribly deeply about this issue, at least not yet, my fairly strong
suspicion is that this design is not going to work out, for multiple
reasons. In no particular order:

1. I don't think users will like it if dependencies on a zstd
dictionary spread like kudzu across all of their tables. I don't think
they'd like it even if it were 100% accurate, but presumably this is
going to add dependencies any time there MIGHT be a real dependency
rather than only when there actually is one.

2. Inserting into a table or updating it only takes RowExclusiveLock,
which is not even self-exclusive. I doubt that it's possible to change
system catalogs in a concurrency-safe way with such a weak lock. For
instance, if two sessions tried to do the same thing in concurrent
transactions, they could both try to add the same dependency at the
same time.

3. I'm not sure that CTAS, INSERT INTO...SELECT, and CREATE
TABLE...EXECUTE are the only ways that datums can creep from one table
into another. For example, what if I create a plpgsql function that
gets a value from one table and stores it in a variable, and then use
that variable to drive an INSERT into another table? I seem to recall
there are complex cases involving records and range types and arrays,
too, where the compressed object gets wrapped inside of another
object; though maybe that wouldn't matter to your implementation if
INSERT INTO ... SELECT uses a sufficiently aggressive strategy for
adding dependencies.

When Dilip and I were working on lz4 TOAST compression, my first
instinct was to not let LZ4-compressed datums leak out of a table by
forcing them to be decompressed (and then possibly recompressed). We
spent a long time trying to make that work before giving up. I think
this is approximately where things started to unravel, and I'd suggest
you read both this message and some of the discussion before and
after:

/messages/by-id/20210316185455.5gp3c5zvvvq66iyj@alap3.anarazel.de

I think we could add plain-old zstd compression without really
tackling this issue, but if we are going to add dictionaries then I
think we might need to revisit the idea of preventing things from
leaking out of tables. What I can't quite remember at the moment is
how much of the problem was that it was going to be slow to force the
recompression, and how much of it was that we weren't sure we could
even find all the places in the code that might need such handling.

I'm now also curious to know whether Andres would agree that it's bad
if zstd dictionaries are un-droppable. After all, I thought it would
be bad if there was no way to eliminate a dependency on a compression
method, and he disagreed. So maybe he would also think undroppable
dictionaries are fine. But maybe not. It seems even worse to me than
undroppable compression methods, because you'll probably not have that
many compression methods ever, but you could have a large number of
dictionaries eventually.

--
Robert Haas
EDB: http://www.enterprisedb.com

#20Michael Paquier
michael@paquier.xyz
In reply to: Robert Haas (#19)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Fri, Apr 18, 2025 at 12:22:18PM -0400, Robert Haas wrote:

I think we could add plain-old zstd compression without really
tackling this issue, but if we are going to add dictionaries then I
think we might need to revisit the idea of preventing things from
leaking out of tables. What I can't quite remember at the moment is
how much of the problem was that it was going to be slow to force the
recompression, and how much of it was that we weren't sure we could
even find all the places in the code that might need such handling.

FWIW, this point resonates here. There is one thing that we have to
do anyway: we just have one bit left in the varlena headers as lz4 is
using the one before last. So we have to make it extensible, even if
it means that any compression method other than LZ4 and pglz would
consume one more byte in its header by default. And I think that this
has to happen at some point if we want flexibility in this area.

+    struct
+    {
+        uint32        va_header;
+        uint32        va_tcinfo;
+        uint32        va_cmp_alg;
+        uint32        va_cmp_dictid;
+        char        va_data[FLEXIBLE_ARRAY_MEMBER];
+    }            va_compressed_ext;

Speaking of which, I am confused by this abstraction choice in
varatt.h in the first patch. Are we sure that we are always going to
have a dictionary attached to a compressed data set or even a
va_cmp_alg? It seems to me that this could lead to a waste of data in
some cases because these fields may not be required depending on the
compression method used, as some fields may not care about these
details. This kind of data should be made optional, on a per-field
basis.

One thing that I've been wondering is how it would be possible to make
the area around varattrib_4b more readable while dealing with more
extensibility. It would be a good occasion to improve that, even if
I'm hand-waving here currently and that the majority of this code is
old enough to vote, with few modifications across the years.

The second thing that I'd love to see on top of the addition of the
extensibility is adding plain compression support for zstd, with
nothing fancy, just the compression and decompression bits. I've done
quite a few benchmarks with the two, and results kind of point in the
direction that zstd is more efficient than lz4 overall. Don't take me
wrong: lz4 can be better in some workloads as it can consume less CPU
than zstd while compressing less. However, a comparison of ratios
like (compression rate / cpu used) has always led me to see zstd as
superior in a large number of cases. lz4 is still very good if you
are CPU-bound and don't care about the extra space required. Both are
three classes better than pglz.

Once we have these three points incrementally built-in together (the
last bit extensibility, the potential varatt.h refactoring and the
zstd support), there may be a point in having support for more
advanced options with the compression methods in the shape of dicts or
more requirements linked to other compression methods, but I think the
topic is complex enough that we should make sure that these basics are
implemented in a way sane enough so as we'd be able to extend them
with all the use cases in mind.
--
Michael

#21Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Robert Haas (#19)
1 attachment(s)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Robert,

Thank you for your feedback on the patch. You’re right that my
proposed design will introduce more dictionary dependencies as
dictionaries grow, I chose this path specifically to avoid changing
existing system behavior and prevent perf regressions in CTAS and
related commands.

After reviewing the email thread you attached on previous response, I
identified a natural choke point for both inserts and updates: the
call to "heap_toast_insert_or_update" inside
heap_prepare_insert/heap_update. In the current master branch, that
function only runs when HeapTupleHasExternal is true; my patch extends
it to HeapTupleHasVarWidth tuples as well. By decompressing every
nested compressed datum at this point—no matter how deeply nested—we
can prevent any leaked datum from propagating into unrelated tables.
This mirrors the existing inlining logic in toast_tuple_init for
external toasted datum, but takes it one step further to fully flatten
datum(decompress datum, not just top level at every level).

On the performance side, my basic benchmarks show almost no regression
for simple INSERT … VALUES workloads. CTAS, however, does regress
noticeably: a CTAS completes in about 4 seconds before this patch, but
with this patch it takes roughly 24 seconds. (For reference, a normal
insert into the source table took about 58 seconds when using zstd
dictionary compression), I suspect the extra cost comes from the added
zstd decompression and PGLZ compression on the destination table.

I’ve attached v13-0008-initial-draft-to-address-datum-leak-problem.patch,
which implements this “flatten_datum” method.

I’d love to know your thoughts on this. Am I on the right track for
solving the problem?

Best regards,
Nikhil Veldanda

Show quoted text

On Fri, Apr 18, 2025 at 9:22 AM Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Apr 15, 2025 at 2:13 PM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Addressing Compressed Datum Leaks problem (via CTAS, INSERT INTO ... SELECT ...)

As compressed datums can be copied to other unrelated tables via CTAS,
INSERT INTO ... SELECT, or CREATE TABLE ... EXECUTE, I’ve introduced a
method inheritZstdDictionaryDependencies. This method is invoked at
the end of such statements and ensures that any dictionary
dependencies from source tables are copied to the destination table.
We determine the set of source tables using the relationOids field in
PlannedStmt.

With the disclaimer that I haven't opened the patch or thought
terribly deeply about this issue, at least not yet, my fairly strong
suspicion is that this design is not going to work out, for multiple
reasons. In no particular order:

1. I don't think users will like it if dependencies on a zstd
dictionary spread like kudzu across all of their tables. I don't think
they'd like it even if it were 100% accurate, but presumably this is
going to add dependencies any time there MIGHT be a real dependency
rather than only when there actually is one.

2. Inserting into a table or updating it only takes RowExclusiveLock,
which is not even self-exclusive. I doubt that it's possible to change
system catalogs in a concurrency-safe way with such a weak lock. For
instance, if two sessions tried to do the same thing in concurrent
transactions, they could both try to add the same dependency at the
same time.

3. I'm not sure that CTAS, INSERT INTO...SELECT, and CREATE
TABLE...EXECUTE are the only ways that datums can creep from one table
into another. For example, what if I create a plpgsql function that
gets a value from one table and stores it in a variable, and then use
that variable to drive an INSERT into another table? I seem to recall
there are complex cases involving records and range types and arrays,
too, where the compressed object gets wrapped inside of another
object; though maybe that wouldn't matter to your implementation if
INSERT INTO ... SELECT uses a sufficiently aggressive strategy for
adding dependencies.

When Dilip and I were working on lz4 TOAST compression, my first
instinct was to not let LZ4-compressed datums leak out of a table by
forcing them to be decompressed (and then possibly recompressed). We
spent a long time trying to make that work before giving up. I think
this is approximately where things started to unravel, and I'd suggest
you read both this message and some of the discussion before and
after:

/messages/by-id/20210316185455.5gp3c5zvvvq66iyj@alap3.anarazel.de

I think we could add plain-old zstd compression without really
tackling this issue, but if we are going to add dictionaries then I
think we might need to revisit the idea of preventing things from
leaking out of tables. What I can't quite remember at the moment is
how much of the problem was that it was going to be slow to force the
recompression, and how much of it was that we weren't sure we could
even find all the places in the code that might need such handling.

I'm now also curious to know whether Andres would agree that it's bad
if zstd dictionaries are un-droppable. After all, I thought it would
be bad if there was no way to eliminate a dependency on a compression
method, and he disagreed. So maybe he would also think undroppable
dictionaries are fine. But maybe not. It seems even worse to me than
undroppable compression methods, because you'll probably not have that
many compression methods ever, but you could have a large number of
dictionaries eventually.

--
Robert Haas
EDB: http://www.enterprisedb.com

Attachments:

v13-0008-initial-draft-to-address-datum-leak-problem.patchapplication/octet-stream; name=v13-0008-initial-draft-to-address-datum-leak-problem.patchDownload
From 79d501b31c967e2e01424ba10cd0d0e14d175c08 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Mon, 21 Apr 2025 13:12:30 +0000
Subject: [PATCH v13 8/8] initial draft to address datum leak problem

---
 src/backend/access/heap/heapam.c              |   3 +-
 src/backend/access/table/toast_helper.c       | 227 +++++++++++++++++-
 src/backend/catalog/pg_zstd_dictionaries.c    |   4 +-
 src/test/regress/expected/compression.out     |   8 +-
 .../regress/expected/compression_zstd_1.out   |  60 ++---
 5 files changed, 264 insertions(+), 38 deletions(-)

diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index c1a4de14a59..0348161432a 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -2278,7 +2278,7 @@ heap_prepare_insert(Relation relation, HeapTuple tup, TransactionId xid,
 		Assert(!HeapTupleHasExternal(tup));
 		return tup;
 	}
-	else if (HeapTupleHasExternal(tup) || tup->t_len > TOAST_TUPLE_THRESHOLD)
+	else if (HeapTupleHasExternal(tup) || HeapTupleHasVarWidth(tup) || tup->t_len > TOAST_TUPLE_THRESHOLD)
 		return heap_toast_insert_or_update(relation, tup, NULL, options);
 	else
 		return tup;
@@ -3776,6 +3776,7 @@ l2:
 	else
 		need_toast = (HeapTupleHasExternal(&oldtup) ||
 					  HeapTupleHasExternal(newtup) ||
+					  HeapTupleHasVarWidth(newtup) ||
 					  newtup->t_len > TOAST_TUPLE_THRESHOLD);
 
 	pagefree = PageGetHeapFreeSpace(page);
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index f4b1cbe494e..2a90ebf77c9 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -19,7 +19,17 @@
 #include "access/toast_internals.h"
 #include "catalog/pg_type_d.h"
 #include "varatt.h"
-
+#include "utils/lsyscache.h"
+#include "access/htup_details.h"
+#include "catalog/pg_type.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/rangetypes.h"
+#include "utils/multirangetypes.h"
+#include "utils/typcache.h"
+#include "miscadmin.h"
+
+static Datum flatten_datum(Datum value, Oid typid);
 
 /*
  * Prepare to TOAST a tuple.
@@ -151,6 +161,25 @@ toast_tuple_init(ToastTupleContext *ttc)
 				ttc->ttc_flags |= (TOAST_NEEDS_CHANGE | TOAST_NEEDS_FREE);
 			}
 
+			if (!(IsCatalogNamespace(ttc->ttc_rel->rd_rel->relnamespace) || IsToastNamespace(ttc->ttc_rel->rd_rel->relnamespace)))
+			{
+				if (!VARATT_IS_SHORT(new_value))
+				{
+					Datum		oldd = PointerGetDatum(new_value);
+					Datum		clean = flatten_datum(oldd, att->atttypid);
+
+					if (DatumGetPointer(clean) != DatumGetPointer(oldd))
+					{
+						if (ttc->ttc_attr[i].tai_oldexternal != NULL)
+							pfree(new_value);
+						new_value = (struct varlena *) DatumGetPointer(clean);
+						ttc->ttc_values[i] = clean;
+						ttc->ttc_attr[i].tai_colflags |= TOASTCOL_NEEDS_FREE;
+						ttc->ttc_flags |= (TOAST_NEEDS_CHANGE | TOAST_NEEDS_FREE);
+					}
+				}
+			}
+
 			/*
 			 * Remember the size of this attribute
 			 */
@@ -346,3 +375,199 @@ toast_delete_external(Relation rel, const Datum *values, const bool *isnull,
 		}
 	}
 }
+
+static Datum
+flatten_datum(Datum value, Oid typid)
+{
+	Oid			basetypid;
+	char		typtype;
+
+	/* initialize at top of this block */
+	basetypid = getBaseType(typid); /* Get basetype for Domain. */
+	typtype = get_typtype(basetypid);
+
+	/* ---------- BASE / ENUM / PSEUDO ----------------------- */
+	if (typtype == TYPTYPE_BASE ||
+		typtype == TYPTYPE_ENUM ||
+		typtype == TYPTYPE_PSEUDO)
+	{
+		if (get_typlen(basetypid) > 0 ||
+			get_typbyval(basetypid))
+			return value;		/* fixed‑len or pass‑by‑val */
+
+		/* ---------- ARRAY ------------------------------------------------ */
+		if (type_is_array(typid))
+		{
+			ArrayType  *arr;
+			TypeCacheEntry *elemCache;
+			Datum	   *elems;
+			bool	   *nulls;
+			int			nitems;
+			int			i;
+			ArrayType  *new_arr;
+
+			arr = DatumGetArrayTypeP(value);
+			elemCache = lookup_type_cache(ARR_ELEMTYPE(arr), 0);
+
+			if (elemCache->typbyval || elemCache->typlen > 0)
+				return PointerGetDatum(arr);
+
+			deconstruct_array(arr,
+							  ARR_ELEMTYPE(arr),
+							  elemCache->typlen,
+							  elemCache->typbyval,
+							  elemCache->typalign,
+							  &elems, &nulls, &nitems);
+
+			if (nitems == 0)
+				return PointerGetDatum(arr);
+
+			for (i = 0; i < nitems; i++)
+			{
+				if (!nulls[i])
+					elems[i] = flatten_datum(elems[i],
+											 ARR_ELEMTYPE(arr));
+			}
+
+			new_arr = construct_md_array(elems, nulls,
+										 ARR_NDIM(arr),
+										 ARR_DIMS(arr),
+										 ARR_LBOUND(arr),
+										 ARR_ELEMTYPE(arr),
+										 elemCache->typlen,
+										 elemCache->typbyval,
+										 elemCache->typalign);
+
+			pfree(elems);
+			pfree(nulls);
+
+			return PointerGetDatum(new_arr);
+		}
+
+		return PointerGetDatum(PG_DETOAST_DATUM(value));
+	}
+
+	/* ---------- COMPOSITE ------------------------------------------- */
+	if (typtype == TYPTYPE_COMPOSITE)
+	{
+		HeapTupleHeader hdr;
+		TupleDesc	td;
+		int			natts;
+		Datum	   *vals;
+		bool	   *nulls;
+		HeapTupleData t;
+		int			i;
+		Form_pg_attribute att;
+		HeapTuple	newt;
+		HeapTupleHeader copy;
+
+		hdr = DatumGetHeapTupleHeader(value);
+
+		if (!(hdr->t_infomask & HEAP_HASVARWIDTH))
+			return PointerGetDatum(hdr);
+
+		td = lookup_rowtype_tupdesc(HeapTupleHeaderGetTypeId(hdr),
+									HeapTupleHeaderGetTypMod(hdr));
+		natts = td->natts;
+
+		vals = palloc(sizeof(Datum) * natts);
+		nulls = palloc(sizeof(bool) * natts);
+
+		t.t_len = VARSIZE(hdr); /* ⚑ portable */
+		t.t_tableOid = InvalidOid;
+		t.t_data = hdr;
+		ItemPointerSetInvalid(&t.t_self);
+
+		heap_deform_tuple(&t, td, vals, nulls);
+
+		for (i = 0; i < natts; i++)
+		{
+			att = TupleDescAttr(td, i);
+			if (att->attisdropped || att->atthasmissing)
+				continue;
+			if (!nulls[i] && att->attlen == -1)
+				vals[i] = flatten_datum(vals[i], att->atttypid);
+		}
+
+		newt = heap_form_tuple(td, vals, nulls);
+		copy = (HeapTupleHeader) palloc(newt->t_len);
+		memcpy(copy, newt->t_data, newt->t_len);
+
+		ReleaseTupleDesc(td);
+		pfree(vals);
+		pfree(nulls);
+		heap_freetuple(newt);
+
+		return PointerGetDatum(copy);
+	}
+
+	/* ---------- RANGE ----------------------------------------------- */
+	if (typtype == TYPTYPE_RANGE)
+	{
+		RangeType  *r;
+		TypeCacheEntry *tc;
+		RangeBound	l;
+		RangeBound	u;
+		bool		empty;
+
+		r = DatumGetRangeTypeP(value);
+		tc = lookup_type_cache(basetypid, TYPECACHE_RANGE_INFO);
+
+		range_deserialize(tc, r, &l, &u, &empty);
+
+		if (!empty &&
+			!(tc->rngelemtype->typbyval ||
+			  tc->rngelemtype->typlen > 0))
+		{
+			if (!l.infinite)
+				l.val = flatten_datum(l.val,
+									  tc->rngelemtype->type_id);
+			if (!u.infinite)
+				u.val = flatten_datum(u.val,
+									  tc->rngelemtype->type_id);
+			return PointerGetDatum(make_range(tc, &l, &u, empty, NULL));
+		}
+
+		return PointerGetDatum(r);
+	}
+
+	/* ---------- MULTIRANGE ---------------------------- */
+	if (typtype == TYPTYPE_MULTIRANGE)
+	{
+		MultirangeType *mr;
+		TypeCacheEntry *tc;
+		int32		rangeCount;
+		RangeType **ranges;
+		RangeType **out;
+		MultirangeType *newmr;
+		int			i;
+
+		mr = DatumGetMultirangeTypeP(value);
+		tc = lookup_type_cache(basetypid,
+							   TYPECACHE_MULTIRANGE_INFO);
+
+		multirange_deserialize(tc->rngtype, mr,
+							   &rangeCount, &ranges);
+
+		out = palloc(sizeof(RangeType *) * rangeCount);
+
+		for (i = 0; i < rangeCount; i++)
+		{
+			out[i] = DatumGetRangeTypeP(
+										flatten_datum(PointerGetDatum(ranges[i]),
+													  RangeTypeGetOid(ranges[i])));
+		}
+
+		newmr = make_multirange(MultirangeTypeGetOid(mr),
+								tc->rngtype,
+								rangeCount, out);
+		pfree(out);
+
+		return PointerGetDatum(newmr);
+	}
+
+	elog(ERROR, "flatten_datum: unsupported type %u", typid);
+	pg_unreachable();
+	/* keep compiler happy: */
+	return (Datum) 0;
+}
diff --git a/src/backend/catalog/pg_zstd_dictionaries.c b/src/backend/catalog/pg_zstd_dictionaries.c
index 08a6883ecd4..63c2a34190a 100644
--- a/src/backend/catalog/pg_zstd_dictionaries.c
+++ b/src/backend/catalog/pg_zstd_dictionaries.c
@@ -410,7 +410,7 @@ range_typzstdsampling(PG_FUNCTION_ARGS)
 	bool		empty;
 
 	/* Get information about range type; note column might be a domain */
-	TypeCacheEntry *typcache = range_get_typcache(fcinfo, getBaseType(range->rangetypid));
+	TypeCacheEntry *typcache = range_get_typcache(fcinfo, RangeTypeGetOid(range));
 
 	/* If the type does not supply a builder, skip */
 	if (!OidIsValid(typcache->rngelemtype->typzstdsampling))
@@ -436,7 +436,7 @@ multirange_typzstdsampling(PG_FUNCTION_ARGS)
 	RangeType **ranges;
 
 	/* Get information about multirange type; note column might be a domain */
-	TypeCacheEntry *typcache = multirange_get_typcache(fcinfo, getBaseType(mrange->multirangetypid));
+	TypeCacheEntry *typcache = multirange_get_typcache(fcinfo, MultirangeTypeGetOid(mrange));
 
 	/* If the type does not supply a builder, skip */
 	if (!OidIsValid(typcache->rngtype->typzstdsampling))
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 94495388ade..29ef885e516 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -69,7 +69,7 @@ SELECT pg_column_compression(f1) FROM cmmove3;
  pg_column_compression 
 -----------------------
  pglz
- lz4
+ pglz
 (2 rows)
 
 -- test LIKE INCLUDING COMPRESSION
@@ -97,7 +97,7 @@ UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
 SELECT pg_column_compression(f1) FROM cmmove2;
  pg_column_compression 
 -----------------------
- lz4
+ pglz
 (1 row)
 
 -- test externally stored compressed data
@@ -200,8 +200,8 @@ SELECT pg_column_compression(f1) FROM cmdata1;
 SELECT pg_column_compression(x) FROM compressmv;
  pg_column_compression 
 -----------------------
- lz4
- lz4
+ pglz
+ pglz
 (2 rows)
 
 -- test compression with partition
diff --git a/src/test/regress/expected/compression_zstd_1.out b/src/test/regress/expected/compression_zstd_1.out
index c1a7936e574..260b9fd2b17 100644
--- a/src/test/regress/expected/compression_zstd_1.out
+++ b/src/test/regress/expected/compression_zstd_1.out
@@ -139,36 +139,36 @@ SELECT pg_column_compression(f1) AS mv_compression
 FROM compressmv_zstd;
  mv_compression 
 ----------------
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
- zstd
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
+ pglz
 (30 rows)
 
 SELECT objid::regclass, refobjid from pg_depend where refclassid = 9946;
-- 
2.47.1

#22Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#20)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Michael,

Thanks for the feedback and the suggested patch sequence. I completely
agree—we must minimize storage overhead when dictionaries aren’t used,
while ensuring varattrib_4b remains extensible enough to handle future
compression metadata beyond dictionary ID (for other algorithms). I’ll
explore design options that satisfy both goals and share my proposal.

Best regards,
Nikhil Veldanda

Show quoted text

On Mon, Apr 21, 2025 at 12:02 AM Michael Paquier <michael@paquier.xyz> wrote:

On Fri, Apr 18, 2025 at 12:22:18PM -0400, Robert Haas wrote:

I think we could add plain-old zstd compression without really
tackling this issue, but if we are going to add dictionaries then I
think we might need to revisit the idea of preventing things from
leaking out of tables. What I can't quite remember at the moment is
how much of the problem was that it was going to be slow to force the
recompression, and how much of it was that we weren't sure we could
even find all the places in the code that might need such handling.

FWIW, this point resonates here. There is one thing that we have to
do anyway: we just have one bit left in the varlena headers as lz4 is
using the one before last. So we have to make it extensible, even if
it means that any compression method other than LZ4 and pglz would
consume one more byte in its header by default. And I think that this
has to happen at some point if we want flexibility in this area.

+    struct
+    {
+        uint32        va_header;
+        uint32        va_tcinfo;
+        uint32        va_cmp_alg;
+        uint32        va_cmp_dictid;
+        char        va_data[FLEXIBLE_ARRAY_MEMBER];
+    }            va_compressed_ext;

Speaking of which, I am confused by this abstraction choice in
varatt.h in the first patch. Are we sure that we are always going to
have a dictionary attached to a compressed data set or even a
va_cmp_alg? It seems to me that this could lead to a waste of data in
some cases because these fields may not be required depending on the
compression method used, as some fields may not care about these
details. This kind of data should be made optional, on a per-field
basis.

One thing that I've been wondering is how it would be possible to make
the area around varattrib_4b more readable while dealing with more
extensibility. It would be a good occasion to improve that, even if
I'm hand-waving here currently and that the majority of this code is
old enough to vote, with few modifications across the years.

The second thing that I'd love to see on top of the addition of the
extensibility is adding plain compression support for zstd, with
nothing fancy, just the compression and decompression bits. I've done
quite a few benchmarks with the two, and results kind of point in the
direction that zstd is more efficient than lz4 overall. Don't take me
wrong: lz4 can be better in some workloads as it can consume less CPU
than zstd while compressing less. However, a comparison of ratios
like (compression rate / cpu used) has always led me to see zstd as
superior in a large number of cases. lz4 is still very good if you
are CPU-bound and don't care about the extra space required. Both are
three classes better than pglz.

Once we have these three points incrementally built-in together (the
last bit extensibility, the potential varatt.h refactoring and the
zstd support), there may be a point in having support for more
advanced options with the compression methods in the shape of dicts or
more requirements linked to other compression methods, but I think the
topic is complex enough that we should make sure that these basics are
implemented in a way sane enough so as we'd be able to extend them
with all the use cases in mind.
--
Michael

#23Andres Freund
andres@anarazel.de
In reply to: Robert Haas (#19)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi,

On 2025-04-18 12:22:18 -0400, Robert Haas wrote:

On Tue, Apr 15, 2025 at 2:13 PM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Addressing Compressed Datum Leaks problem (via CTAS, INSERT INTO ... SELECT ...)

As compressed datums can be copied to other unrelated tables via CTAS,
INSERT INTO ... SELECT, or CREATE TABLE ... EXECUTE, I’ve introduced a
method inheritZstdDictionaryDependencies. This method is invoked at
the end of such statements and ensures that any dictionary
dependencies from source tables are copied to the destination table.
We determine the set of source tables using the relationOids field in
PlannedStmt.

With the disclaimer that I haven't opened the patch or thought
terribly deeply about this issue, at least not yet, my fairly strong
suspicion is that this design is not going to work out, for multiple
reasons. In no particular order:

1. I don't think users will like it if dependencies on a zstd
dictionary spread like kudzu across all of their tables. I don't think
they'd like it even if it were 100% accurate, but presumably this is
going to add dependencies any time there MIGHT be a real dependency
rather than only when there actually is one.

2. Inserting into a table or updating it only takes RowExclusiveLock,
which is not even self-exclusive. I doubt that it's possible to change
system catalogs in a concurrency-safe way with such a weak lock. For
instance, if two sessions tried to do the same thing in concurrent
transactions, they could both try to add the same dependency at the
same time.

3. I'm not sure that CTAS, INSERT INTO...SELECT, and CREATE
TABLE...EXECUTE are the only ways that datums can creep from one table
into another. For example, what if I create a plpgsql function that
gets a value from one table and stores it in a variable, and then use
that variable to drive an INSERT into another table? I seem to recall
there are complex cases involving records and range types and arrays,
too, where the compressed object gets wrapped inside of another
object; though maybe that wouldn't matter to your implementation if
INSERT INTO ... SELECT uses a sufficiently aggressive strategy for
adding dependencies.

+1 to all of these.

I think we could add plain-old zstd compression without really
tackling this issue

+1

I'm now also curious to know whether Andres would agree that it's bad
if zstd dictionaries are un-droppable. After all, I thought it would
be bad if there was no way to eliminate a dependency on a compression
method, and he disagreed.

I still am not too worried about that aspect. However:

So maybe he would also think undroppable dictionaries are fine.

I'm much less sanguine about this. Imagine a schema based multi-tenancy setup,
where tenants come and go, and where a few of the tables use custom
dictionaries. Whereas not being able to get rid of lz4 at all has basically no
cost whatsoever, collecting more and more unusable dictionaries can imply a
fair amount of space usage after a while. I don't see any argument why that
would be ok, really.

But maybe not. It seems even worse to me than undroppable compression
methods, because you'll probably not have that many compression methods
ever, but you could have a large number of dictionaries eventually.

Agreed on the latter.

Greetings,

Andres Freund

#24Robert Haas
robertmhaas@gmail.com
In reply to: Nikhil Kumar Veldanda (#21)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Mon, Apr 21, 2025 at 8:52 PM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

After reviewing the email thread you attached on previous response, I
identified a natural choke point for both inserts and updates: the
call to "heap_toast_insert_or_update" inside
heap_prepare_insert/heap_update. In the current master branch, that
function only runs when HeapTupleHasExternal is true; my patch extends
it to HeapTupleHasVarWidth tuples as well.

Isn't that basically all tuples, though? I think that's where this gets painful.

On the performance side, my basic benchmarks show almost no regression
for simple INSERT … VALUES workloads. CTAS, however, does regress
noticeably: a CTAS completes in about 4 seconds before this patch, but
with this patch it takes roughly 24 seconds. (For reference, a normal
insert into the source table took about 58 seconds when using zstd
dictionary compression), I suspect the extra cost comes from the added
zstd decompression and PGLZ compression on the destination table.

That's nice to know, but I think the key question is not so much what
the feature costs when it is used but what it costs when it isn't
used. If we implement a system where we don't let
dictionary-compressed zstd datums leak out of tables, that's bound to
slow down a CTAS from a table where this feature is used, but that's
kind of OK: the feature has pros and cons, and if you don't like those
tradeoffs, you don't have to use it. However, it sounds like this
could also slow down inserts and updates in some cases even for users
who are not making use of the feature, and that's going to be a major
problem unless it can be shown that there is no case where the impact
is at all significant. Users hate paying for features that they aren't
using.

I wonder if there's a possible design where we only allow
dictionary-compressed datums to exist as top-level attributes in
designated tables to which those dictionaries are attached; and any
time you try to bury that Datum inside a container object (row, range,
array, whatever) detoasting is forced. If there's a clean and
inexpensive way to implement that, then you could avoid having
heap_toast_insert_or_update care about HeapTupleHasExternal(), which
seems like it might be a key point.

--
Robert Haas
EDB: http://www.enterprisedb.com

#25Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#24)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Wed, Apr 23, 2025 at 11:59 AM Robert Haas <robertmhaas@gmail.com> wrote:

heap_toast_insert_or_update care about HeapTupleHasExternal(), which
seems like it might be a key point.

Care about HeapTupleHasVarWidth, rather.

--
Robert Haas
EDB: http://www.enterprisedb.com

#26Michael Paquier
michael@paquier.xyz
In reply to: Robert Haas (#24)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Wed, Apr 23, 2025 at 11:59:26AM -0400, Robert Haas wrote:

That's nice to know, but I think the key question is not so much what
the feature costs when it is used but what it costs when it isn't
used. If we implement a system where we don't let
dictionary-compressed zstd datums leak out of tables, that's bound to
slow down a CTAS from a table where this feature is used, but that's
kind of OK: the feature has pros and cons, and if you don't like those
tradeoffs, you don't have to use it. However, it sounds like this
could also slow down inserts and updates in some cases even for users
who are not making use of the feature, and that's going to be a major
problem unless it can be shown that there is no case where the impact
is at all significant. Users hate paying for features that they aren't
using.

The cost of digesting a dictionnary when decompressing sets of values
is also something I think we should worry about, FWIW (see [1]https://facebook.github.io/zstd/zstd_manual.html#Chapter10 -- Michael), as
the digesting cost is documented as costly, so I think that there is
also an argument in making the feature efficient if used. That would
hurt if a sequential scan needs to detoast multiple blobs with the
same dict. If we attach that on a per-value value, wouldn't it imply
that we need to digest the dictionnary every time a blob is
decompressed? This information could be cached, but it seems a bit
weird to me to invent a new level of relation caching for would could
be attached as a relation attribute option in the relcache. If a
dictionnary gets trained with a new sample of values, we could rely on
the invalidation to pass the new information.

Based on what I'm reading and I know very little about the topic so I
may be wrong, but does it even make sense to allow multiple
dictionnaries to be used in a single attribute? Of course that may
depend on the JSON blob patterns a single attribute is dealing with,
but I'm not sure that this is worth the extra complexity this creates.

I wonder if there's a possible design where we only allow
dictionary-compressed datums to exist as top-level attributes in
designated tables to which those dictionaries are attached; and any
time you try to bury that Datum inside a container object (row, range,
array, whatever) detoasting is forced. If there's a clean and
inexpensive way to implement that, then you could avoid having
heap_toast_insert_or_update care about HeapTupleHasExternal(), which
seems like it might be a key point.

Interesting, not sure.

FWIW, I'd still try to focus on making varatt more extensible with
plain zstd support first, because diving in all these details. We are
going to need it anyway.

[1]: https://facebook.github.io/zstd/zstd_manual.html#Chapter10 -- Michael
--
Michael

#27Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#20)
2 attachment(s)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Michael,

Thanks for the suggestions. I agree that we should first solve the
“last–free-bit” problem in varattrib_4b compression bits before
layering on any features. Below is the approach I’ve prototyped to
keep the header compact yet fully extensible, followed by a sketch of
the plain-ZSTD(no dict) patch that sits cleanly on top of it.

1. Minimal but extensible header

/* varatt_cmp_extended follows va_tcinfo when the upper two bits of
* va_tcinfo are 11. Compressed data starts immediately after
* ext_data. ext_hdr encodes both the compression algorithm and the
* byte-length of the algorithm-specific metadata.
*/
typedef struct varatt_cmp_extended
{
uint32 ext_hdr; /* [ meta_size:24 | cmpr_id:8 ] */
char ext_data[FLEXIBLE_ARRAY_MEMBER]; /* optional metadata */
} varatt_cmp_extended;

a. 24 bits for length → per-datum compression algorithm metadata is
capped at 16 MB, which is far more than any realistic compression
header.
b. 8 bits for algorithm id → up to 256 algorithms.
c. Zero-overhead when unused if an algorithm needs no per-datum
metadata (e.g., ZSTD-nodict),

2. Algorithm registry
/*
* TOAST compression methods enumeration.
*
* Each entry defines:
* - NAME : identifier for the compression algorithm
* - VALUE : numeric enum value
* - METADATA type: struct type holding extra info (void when none)
*
* The INVALID entry is a sentinel and must remain last.
*/
#define TOAST_COMPRESSION_LIST \
X(PGLZ, 0, void) /* existing */ \
X(LZ4, 1, void) /* existing */ \
X(ZSTD_NODICT, 2, void) /* new, no metadata */ \
X(ZSTD_DICT, 3, zstd_dict_meta) /* new, needs dict_id */ \
X(INVALID, 4, void) /* sentinel */

typedef enum ToastCompressionId
{
#define X(name,val,meta) TOAST_##name##_COMPRESSION_ID = val,
TOAST_COMPRESSION_LIST
#undef X
} ToastCompressionId;

/* Example of an algorithm-specific metadata block */
typedef struct
{
uint32 dict_id; /* dictionary Oid */
} zstd_dict_meta;

3. Resulting on-disk layouts for zstd

ZSTD no dict: datum ondisk layout:
+----------------------------------+
| va_header (uint32) |
+----------------------------------+
| va_tcinfo (uint32) | (11 in top two bits specify extended)
+----------------------------------+
| ext_hdr (uint32) | <-- [ meta size:24 bits |
compression id:8 bits ]
+----------------------------------+
| Compressed bytes … | <-- zstd (no dictionary)
+----------------------------------+

ZSTD dict: datum ondisk layout
+----------------------------------+
| va_header (uint32) |
+----------------------------------+
| va_tcinfo (uint32) |
+----------------------------------+
| ext_hdr (uint32) | <-- [ meta size:24 bits |
compression id:8 bits ]
+----------------------------------+
| dict_id (uint32) | <-- zstd_dict_meta
+----------------------------------+
| Compressed bytes … | <-- zstd (dictionary)
+----------------------------------+

4. How does this fit?

Flexibility: Each new algorithm that needs extra metadata simply
defines its own struct and allocates varatt_cmp_extended in
setup_compression_info.
Storage: Everything in varatt_cmp_extended is copied to the datum,
immediately followed by the compressed payload.
Optional, pay-as-you-go metadata – only algorithms that need it pay for it.
Future-proof – new compression algorithms, requires any kind of
metadata like dictid or any other slot into the same ext_data
mechanism.

I’ve split the work into two patches for review:
v19-0001-varattrib_4b-design-proposal-to-make-it-extended.patch:
varattrib_4b extensibility – adds varatt_cmp_extended, enum plumbing,
and macros; behaviour unchanged.
v19-0002-zstd-nodict-support.patch: Plain ZSTD (non dict) support.

Please share your thoughts—and I’d love to hear feedback on the design. Thanks!

On Mon, Apr 21, 2025 at 12:02 AM Michael Paquier <michael@paquier.xyz> wrote:

On Fri, Apr 18, 2025 at 12:22:18PM -0400, Robert Haas wrote:

I think we could add plain-old zstd compression without really
tackling this issue, but if we are going to add dictionaries then I
think we might need to revisit the idea of preventing things from
leaking out of tables. What I can't quite remember at the moment is
how much of the problem was that it was going to be slow to force the
recompression, and how much of it was that we weren't sure we could
even find all the places in the code that might need such handling.

FWIW, this point resonates here. There is one thing that we have to
do anyway: we just have one bit left in the varlena headers as lz4 is
using the one before last. So we have to make it extensible, even if
it means that any compression method other than LZ4 and pglz would
consume one more byte in its header by default. And I think that this
has to happen at some point if we want flexibility in this area.

+    struct
+    {
+        uint32        va_header;
+        uint32        va_tcinfo;
+        uint32        va_cmp_alg;
+        uint32        va_cmp_dictid;
+        char        va_data[FLEXIBLE_ARRAY_MEMBER];
+    }            va_compressed_ext;

Speaking of which, I am confused by this abstraction choice in
varatt.h in the first patch. Are we sure that we are always going to
have a dictionary attached to a compressed data set or even a
va_cmp_alg? It seems to me that this could lead to a waste of data in
some cases because these fields may not be required depending on the
compression method used, as some fields may not care about these
details. This kind of data should be made optional, on a per-field
basis.

One thing that I've been wondering is how it would be possible to make
the area around varattrib_4b more readable while dealing with more
extensibility. It would be a good occasion to improve that, even if
I'm hand-waving here currently and that the majority of this code is
old enough to vote, with few modifications across the years.

The second thing that I'd love to see on top of the addition of the
extensibility is adding plain compression support for zstd, with
nothing fancy, just the compression and decompression bits. I've done
quite a few benchmarks with the two, and results kind of point in the
direction that zstd is more efficient than lz4 overall. Don't take me
wrong: lz4 can be better in some workloads as it can consume less CPU
than zstd while compressing less. However, a comparison of ratios
like (compression rate / cpu used) has always led me to see zstd as
superior in a large number of cases. lz4 is still very good if you
are CPU-bound and don't care about the extra space required. Both are
three classes better than pglz.

Once we have these three points incrementally built-in together (the
last bit extensibility, the potential varatt.h refactoring and the
zstd support), there may be a point in having support for more
advanced options with the compression methods in the shape of dicts or
more requirements linked to other compression methods, but I think the
topic is complex enough that we should make sure that these basics are
implemented in a way sane enough so as we'd be able to extend them
with all the use cases in mind.
--
Michael

--
Nikhil Veldanda

--
Nikhil Veldanda

Attachments:

v19-0002-zstd-nodict-support.patchapplication/octet-stream; name=v19-0002-zstd-nodict-support.patchDownload
From 2594a2b4d22a671dc4776ad0a7ffc214dc3ddf71 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Fri, 25 Apr 2025 06:00:37 +0000
Subject: [PATCH v19 2/2] zstd nodict support.

---
 contrib/amcheck/verify_heapam.c               |   1 +
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/reloptions.c        |  14 +-
 src/backend/access/common/toast_compression.c | 223 +++++++++++++++++-
 src/backend/access/common/toast_internals.c   |   4 +
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/psql/describe.c                       |   5 +-
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/access/toast_compression.h        |  31 ++-
 src/include/access/toast_internals.h          |   3 +-
 src/include/utils/attoptcache.h               |   1 +
 src/include/varatt.h                          |   3 +-
 src/test/regress/expected/compression.out     |   5 +-
 src/test/regress/expected/compression_1.out   |   3 +
 src/test/regress/sql/compression.sql          |   1 +
 17 files changed, 292 insertions(+), 26 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 3c3faf59579..f1b7e77a322 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1793,6 +1793,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_NODICT_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 01419d1c65f..72b0a2c5672 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -246,10 +246,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
 				TOAST_PGLZ_COMPRESSION_ID)
@@ -485,6 +485,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_NODICT_COMPRESSION_ID:
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +530,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_NODICT_COMPRESSION_ID:
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 46c1dce222d..1267668a242 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -24,6 +24,7 @@
 #include "access/nbtree.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
+#include "access/toast_compression.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
@@ -381,7 +382,15 @@ static relopt_int intRelOpts[] =
 		},
 		-1, 0, 1024
 	},
-
+	{
+		{
+			"zstd_level",
+			"Set column's ZSTD compression level",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_LEVEL, MIN_ZSTD_LEVEL, MAX_ZSTD_LEVEL
+	},
 	/* list terminator */
 	{{NULL}}
 };
@@ -2097,7 +2106,8 @@ attribute_reloptions(Datum reloptions, bool validate)
 {
 	static const relopt_parse_elt tab[] = {
 		{"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)},
-		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}
+		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)},
+		{"zstd_level", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_level)},
 	};
 
 	return (bytea *) build_reloptions(reloptions, validate,
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index de31a8dc591..ec1bbffaaf0 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,19 +17,36 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
+#include "utils/attoptcache.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
 
-#define NO_LZ4_SUPPORT() \
+#ifdef USE_ZSTD
+static ZSTD_CCtx *ZstdCompressionCtx = NULL;
+
+static ZSTD_DCtx *ZstdDecompressionCtx = NULL;
+
+#define ZSTD_CHECK_ERROR(zstd_ret, msg) \
+	do { \
+		if (ZSTD_isError(zstd_ret)) \
+			ereport(ERROR, (errmsg("%s: %s", (msg), ZSTD_getErrorName(zstd_ret)))); \
+	} while (0)
+#endif
+
+#define COMPRESSION_METHOD_NOT_SUPPORTED(method) \
 	ereport(ERROR, \
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
-			 errmsg("compression method lz4 not supported"), \
-			 errdetail("This functionality requires the server to be built with lz4 support.")))
+			 errmsg("compression method %s not supported", method), \
+			 errdetail("This functionality requires the server to be built with %s support.", method)))
 
 /*
  * Compress a varlena using PGLZ.
@@ -139,7 +156,7 @@ struct varlena *
 lz4_compress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		valsize;
@@ -182,7 +199,7 @@ struct varlena *
 lz4_decompress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -215,7 +232,7 @@ struct varlena *
 lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -245,6 +262,167 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 #endif
 }
 
+/* Compress datum using ZSTD with optional dictionary (using cdict) */
+struct varlena *
+zstd_compress_datum(const struct varlena *value, CompressionInfo cmp)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	void	   *dest;
+	size_t		cmp_size,
+				ret;
+
+	/* Create the session CCtx if it hasn't been yet */
+	if (ZstdCompressionCtx == NULL)
+	{
+		ZstdCompressionCtx = ZSTD_createCCtx();
+		if (!ZstdCompressionCtx)
+			ereport(ERROR, (errmsg("could not create ZSTD_CCtx")));
+	}
+
+	/* Reset the context to clear any prior state */
+	ret = ZSTD_CCtx_reset(ZstdCompressionCtx, ZSTD_reset_session_only);
+	ZSTD_CHECK_ERROR(ret, "failed to reset ZSTD CCtx");
+
+	/* Set compression level */
+	ret = ZSTD_CCtx_setParameter(ZstdCompressionCtx, ZSTD_c_compressionLevel, cmp.zstd_level);
+	ZSTD_CHECK_ERROR(ret, "failed to set ZSTD compression level");
+
+	/* Allocate space for the compressed varlena (header + data) */
+	compressed = (struct varlena *) palloc(max_size + VARATT_4BCE_HDRSZ(cmp.cmp_ext));
+	dest = (char *) compressed + VARATT_4BCE_HDRSZ(cmp.cmp_ext);
+
+	cmp_size = ZSTD_compress2(ZstdCompressionCtx,
+							  dest,
+							  max_size,
+							  VARDATA_ANY(value),
+							  valsize);
+
+	if (ZSTD_isError(cmp_size))
+	{
+		pfree(compressed);
+		ZSTD_CHECK_ERROR(cmp_size, "ZSTD compression failed");
+	}
+
+	/*
+	 * If compression did not reduce size, return NULL so that the
+	 * uncompressed data is stored
+	 */
+	if (cmp_size > valsize)
+	{
+		pfree(compressed);
+		return NULL;
+	}
+
+	/* Set the compressed size in the varlena header */
+	SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARATT_4BCE_HDRSZ(cmp.cmp_ext));
+	return compressed;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+	return NULL;
+#endif
+}
+
+/* Decompression routine */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		cmp_size_exhdr = VARATT_4BCE_DATA_SIZE(value);
+	struct varlena *result;
+	size_t		uncmp_size,
+				ret;
+
+	if (ZstdDecompressionCtx == NULL)
+	{
+		ZstdDecompressionCtx = ZSTD_createDCtx();
+		if (!ZstdDecompressionCtx)
+			ereport(ERROR, (errmsg("could not create ZSTD_DCtx")));
+	}
+
+	/* Reset the context to clear any prior state */
+	ret = ZSTD_DCtx_reset(ZstdDecompressionCtx, ZSTD_reset_session_only);
+	ZSTD_CHECK_ERROR(ret, "failed to reset ZSTD DCtx");
+
+	/* Allocate space for the uncompressed data */
+	result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+	uncmp_size = ZSTD_decompressDCtx(ZstdDecompressionCtx,
+									 VARDATA(result),
+									 actual_size_exhdr,
+									 VARATT_4BCE_DATA_PTR(value),
+									 cmp_size_exhdr);
+
+	if (ZSTD_isError(uncmp_size))
+	{
+		pfree(result);
+		ZSTD_CHECK_ERROR(uncmp_size, "ZSTD decompression failed");
+	}
+
+	/* Set final size in the varlena header */
+	SET_VARSIZE(result, uncmp_size + VARHDRSZ);
+	return result;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum using the streaming API and optional dictionary */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	size_t		ret;
+
+	if (ZstdDecompressionCtx == NULL)
+	{
+		ZstdDecompressionCtx = ZSTD_createDCtx();
+		if (!ZstdDecompressionCtx)
+			elog(ERROR, "could not create ZSTD_DCtx");
+	}
+
+	/* Reset the context to clear any prior state */
+	ret = ZSTD_DCtx_reset(ZstdDecompressionCtx, ZSTD_reset_session_only);
+	ZSTD_CHECK_ERROR(ret, "failed to reset ZSTD_DCtx");
+
+	inBuf.src = VARATT_4BCE_DATA_PTR(value);
+	inBuf.size = VARATT_4BCE_DATA_SIZE(value);
+	inBuf.pos = 0;
+
+	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+	outBuf.dst = (char *) result + VARHDRSZ;
+	outBuf.size = slicelength;
+	outBuf.pos = 0;
+
+	/* Common decompression loop */
+	while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+	{
+		ret = ZSTD_decompressStream(ZstdDecompressionCtx, &outBuf, &inBuf);
+		if (ZSTD_isError(ret))
+		{
+			pfree(result);
+			ZSTD_CHECK_ERROR(ret, "zstd decompression failed");
+		}
+	}
+
+	Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+	SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+	return result;
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+	return NULL;
+#endif
+}
+
 /*
  * Extract compression ID from a varlena.
  *
@@ -291,10 +469,17 @@ CompressionNameToMethod(const char *compression)
 	else if (strcmp(compression, "lz4") == 0)
 	{
 #ifndef USE_LZ4
-		NO_LZ4_SUPPORT();
+		COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd_nodict") == 0)
+	{
+#ifndef USE_ZSTD
+		COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+#endif
+		return TOAST_ZSTD_NODICT_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -311,6 +496,8 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_NODICT_COMPRESSION:
+			return "zstd_nodict";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
@@ -324,11 +511,33 @@ setup_compression_info(char cmethod, Form_pg_attribute att)
 
 	/* initialize from the attribute’s default settings */
 	info.cmethod = cmethod;
+	info.zstd_level = DEFAULT_ZSTD_LEVEL;
 	info.cmp_ext = NULL;
 
 	if (!CompressionMethodIsValid(cmethod))
 		info.cmethod = default_toast_compression;
 
+	switch (info.cmethod)
+	{
+		case TOAST_PGLZ_COMPRESSION:
+		case TOAST_LZ4_COMPRESSION:
+			break;
+		case TOAST_ZSTD_NODICT_COMPRESSION:
+			{
+				AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+				if (aopt != NULL)
+					info.zstd_level = aopt->zstd_level;
+
+				info.cmp_ext = palloc(sizeof(varatt_cmp_extended));
+
+				VARATT_4BCE_SET_HDR(info.cmp_ext->ext_hdr, TOAST_ZSTD_NODICT_COMPRESSION_ID, 0);
+			}
+			break;
+		default:
+			elog(ERROR, "invalid compression method %c", info.cmethod);
+	}
+
 	return info;
 }
 
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 21139f20de3..7cc2c2bf8ac 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -68,6 +68,10 @@ toast_compress_datum(Datum value, CompressionInfo cmp)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_NODICT_COMPRESSION:
+			tmp = zstd_compress_datum((const struct varlena *) value, cmp);
+			cmid = TOAST_ZSTD_NODICT_COMPRESSION_ID;
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 3e4d5568bde..5b9151c7e16 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5301,6 +5301,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_NODICT_COMPRESSION_ID:
+			result = "zstd_nodict";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 60b12446a1c..432bb1bd3ab 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd_nodict", TOAST_ZSTD_NODICT_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 34826d01380..f2d2ca39514 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -756,7 +756,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd_nodict'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1d08268393e..3831a7fab03 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2171,8 +2171,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'n' ? "zstd_nodict" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index c916b9299a8..2441acf41ce 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2875,11 +2875,11 @@ match_previous_words(int pattern_id,
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET ( */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "(") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "("))
-		COMPLETE_WITH("n_distinct", "n_distinct_inherited");
+		COMPLETE_WITH("n_distinct", "n_distinct_inherited", "zstd_level");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION"))
-		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4");
+		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4", "ZSTD_NODICT");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET EXPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "EXPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "EXPRESSION"))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 8e9d5b44752..9af4a14ebed 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -17,6 +17,10 @@
 #include "toast_compression.h"
 #include "catalog/pg_attribute.h"
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 /*
  * GUC support.
  *
@@ -36,10 +40,11 @@ extern PGDLLIMPORT int default_toast_compression;
  *
  * The INVALID entry is a sentinel and must remain last.
  */
-#define TOAST_COMPRESSION_LIST                                 \
-    X(PGLZ,         0,    void)    /* PostgreSQL LZ-based */   \
-    X(LZ4,          1,    void)    /* LZ4 algorithm */         \
-    X(INVALID,      2,    void) /* sentinel, must be last */
+#define TOAST_COMPRESSION_LIST                                 			\
+    X(PGLZ,         0,    void)    /* PostgreSQL LZ-based */   			\
+    X(LZ4,          1,    void)    /* LZ4 algorithm */         			\
+	X(ZSTD_NODICT,  2, 	  void)	   /* ZSTD algorithm, no dictionary */ 	\
+	X(INVALID, 		3, 	  void) /* sentinel, must be last */
 
 
 typedef enum ToastCompressionId
@@ -54,6 +59,7 @@ typedef enum ToastCompressionId
 typedef struct CompressionInfo
 {
 	char		cmethod;
+	int			zstd_level;
 	/* Extended compression meta info */
 	varatt_cmp_extended *cmp_ext;
 } CompressionInfo;
@@ -66,10 +72,22 @@ typedef struct CompressionInfo
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_NODICT_COMPRESSION	'n'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
 
+#define InvalidDictId						0
+
+#ifdef USE_ZSTD
+#define DEFAULT_ZSTD_LEVEL					ZSTD_CLEVEL_DEFAULT
+#define MIN_ZSTD_LEVEL						(int)-ZSTD_BLOCKSIZE_MAX
+#define MAX_ZSTD_LEVEL						22
+#else
+#define DEFAULT_ZSTD_LEVEL					0
+#define MIN_ZSTD_LEVEL						0
+#define MAX_ZSTD_LEVEL						0
+#endif
 
 /* pglz compression/decompression routines */
 extern struct varlena *pglz_compress_datum(const struct varlena *value);
@@ -83,6 +101,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd compression/decompression routines */
+extern struct varlena *zstd_compress_datum(const struct varlena *value, CompressionInfo cmp);
+extern struct varlena *zstd_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index e672766f91a..7ae69792596 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -35,7 +35,8 @@ typedef struct toast_compress_header
 	do { 																						\
 		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); 									\
 		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || 										\
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); 										\
+			   (cm_method) == TOAST_LZ4_COMPRESSION_ID  ||										\
+			   (cm_method) == TOAST_ZSTD_NODICT_COMPRESSION_ID); 								\
 		if ((cm_method) <= TOAST_LAST_COMPRESSION_ID_BEFORE_EXT) { 								\
 			((toast_compress_header *) (ptr))->tcinfo = 										\
 				(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); 						\
diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h
index f684a772af5..51d65ebd646 100644
--- a/src/include/utils/attoptcache.h
+++ b/src/include/utils/attoptcache.h
@@ -21,6 +21,7 @@ typedef struct AttributeOpts
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	float8		n_distinct;
 	float8		n_distinct_inherited;
+	int			zstd_level;
 } AttributeOpts;
 
 extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 72a49c2322e..4e60706744b 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -340,7 +340,8 @@ typedef struct
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
 	do { \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
+			   (cm) == TOAST_LZ4_COMPRESSION_ID  || 	  \
+			   (cm) == TOAST_ZSTD_NODICT_COMPRESSION_ID); \
 		if ((cm) <= TOAST_LAST_COMPRESSION_ID_BEFORE_EXT) \
 		{ \
 			/* Store the actual method in va_extinfo */ \
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 4dd9ee7200d..c7e108a0f52 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -238,10 +238,11 @@ NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd_nodict.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd_nodict.
+SET default_toast_compression = 'zstd_nodict';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 -- test alter compression method
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out
index 7bd7642b4b9..5b10d8c5259 100644
--- a/src/test/regress/expected/compression_1.out
+++ b/src/test/regress/expected/compression_1.out
@@ -233,6 +233,9 @@ HINT:  Available values: pglz.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
 HINT:  Available values: pglz.
+SET default_toast_compression = 'zstd_nodict';
+ERROR:  invalid value for parameter "default_toast_compression": "zstd_nodict"
+HINT:  Available values: pglz.
 SET default_toast_compression = 'lz4';
 ERROR:  invalid value for parameter "default_toast_compression": "lz4"
 HINT:  Available values: pglz.
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index 490595fcfb2..27979eb7997 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -102,6 +102,7 @@ CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 SET default_toast_compression = 'I do not exist compression';
+SET default_toast_compression = 'zstd_nodict';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 
-- 
2.47.1

v19-0001-varattrib_4b-design-proposal-to-make-it-extended.patchapplication/octet-stream; name=v19-0001-varattrib_4b-design-proposal-to-make-it-extended.patchDownload
From 8d0b75a2c1ae1fb8401f3d9c3f44cf3df429f141 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Fri, 25 Apr 2025 04:38:01 +0000
Subject: [PATCH v19 1/2] varattrib_4b design proposal to make it extended to
 support multiple compression algorithms.

---
 contrib/amcheck/verify_heapam.c               |  3 +-
 src/backend/access/brin/brin_tuple.c          |  4 +-
 src/backend/access/common/detoast.c           |  6 +-
 src/backend/access/common/indextuple.c        |  5 +-
 src/backend/access/common/toast_compression.c | 26 ++++++-
 src/backend/access/common/toast_internals.c   | 18 +++--
 src/backend/access/table/toast_helper.c       |  4 +-
 src/include/access/toast_compression.h        | 44 ++++++++---
 src/include/access/toast_internals.h          | 31 ++++----
 src/include/varatt.h                          | 73 ++++++++++++++++++-
 src/tools/pgindent/typedefs.list              |  2 +
 11 files changed, 171 insertions(+), 45 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index aa9cccd1da4..3c3faf59579 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1786,7 +1786,8 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_get_compression_id(attr);
+
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..9c1e22e98c6 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -223,6 +223,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			{
 				Datum		cvalue;
 				char		compression;
+				CompressionInfo cmp;
 				Form_pg_attribute att = TupleDescAttr(brdesc->bd_tupdesc,
 													  keyno);
 
@@ -237,7 +238,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 				else
 					compression = InvalidCompressionMethod;
 
-				cvalue = toast_compress_datum(value, compression);
+				cmp = setup_compression_info(compression, att);
+				cvalue = toast_compress_datum(value, cmp);
 
 				if (DatumGetPointer(cvalue) != NULL)
 				{
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..01419d1c65f 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -478,7 +478,7 @@ toast_decompress_datum(struct varlena *attr)
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
@@ -514,14 +514,14 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 	 * have been seen to give wrong results if passed an output size that is
 	 * more than the data's true decompressed size.
 	 */
-	if ((uint32) slicelength >= TOAST_COMPRESS_EXTSIZE(attr))
+	if ((uint32) slicelength >= VARDATA_COMPRESSED_GET_EXTSIZE(attr))
 		return toast_decompress_datum(attr);
 
 	/*
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data slice using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 1986b943a28..0386f5a1491 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -123,9 +123,10 @@ index_form_tuple_context(TupleDesc tupleDescriptor,
 			 att->attstorage == TYPSTORAGE_MAIN))
 		{
 			Datum		cvalue;
+			CompressionInfo cmp;
 
-			cvalue = toast_compress_datum(untoasted_values[i],
-										  att->attcompression);
+			cmp = setup_compression_info(att->attcompression, att);
+			cvalue = toast_compress_datum(untoasted_values[i], cmp);
 
 			if (DatumGetPointer(cvalue) != NULL)
 			{
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97e..de31a8dc591 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -266,7 +266,9 @@ toast_get_compression_id(struct varlena *attr)
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) > TOAST_LAST_COMPRESSION_ID_BEFORE_EXT)
+			cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(detoast_external_attr(attr));
+		else
 			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 	}
 	else if (VARATT_IS_COMPRESSED(attr))
@@ -314,3 +316,25 @@ GetCompressionMethodName(char method)
 			return NULL;		/* keep compiler quiet */
 	}
 }
+
+CompressionInfo
+setup_compression_info(char cmethod, Form_pg_attribute att)
+{
+	CompressionInfo info;
+
+	/* initialize from the attribute’s default settings */
+	info.cmethod = cmethod;
+	info.cmp_ext = NULL;
+
+	if (!CompressionMethodIsValid(cmethod))
+		info.cmethod = default_toast_compression;
+
+	return info;
+}
+
+void
+free_compression_info(CompressionInfo *info)
+{
+	if (info->cmp_ext != NULL)
+		pfree(info->cmp_ext);
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346ce..21139f20de3 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -43,25 +43,22 @@ static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
  * ----------
  */
 Datum
-toast_compress_datum(Datum value, char cmethod)
+toast_compress_datum(Datum value, CompressionInfo cmp)
 {
 	struct varlena *tmp = NULL;
 	int32		valsize;
 	ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID;
+	varatt_cmp_extended *cmp_ext = cmp.cmp_ext;
 
 	Assert(!VARATT_IS_EXTERNAL(DatumGetPointer(value)));
 	Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value)));
 
 	valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(value));
 
-	/* If the compression method is not valid, use the current default */
-	if (!CompressionMethodIsValid(cmethod))
-		cmethod = default_toast_compression;
-
 	/*
 	 * Call appropriate compression routine for the compression method.
 	 */
-	switch (cmethod)
+	switch (cmp.cmethod)
 	{
 		case TOAST_PGLZ_COMPRESSION:
 			tmp = pglz_compress_datum((const struct varlena *) value);
@@ -72,11 +69,14 @@ toast_compress_datum(Datum value, char cmethod)
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
 		default:
-			elog(ERROR, "invalid compression method %c", cmethod);
+			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
 
 	if (tmp == NULL)
+	{
+		free_compression_info(&cmp);
 		return PointerGetDatum(NULL);
+	}
 
 	/*
 	 * We recheck the actual size even if compression reports success, because
@@ -92,13 +92,15 @@ toast_compress_datum(Datum value, char cmethod)
 	{
 		/* successful compression */
 		Assert(cmid != TOAST_INVALID_COMPRESSION_ID);
-		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid);
+		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid, cmp_ext);
+		free_compression_info(&cmp);
 		return PointerGetDatum(tmp);
 	}
 	else
 	{
 		/* incompressible data */
 		pfree(tmp);
+		free_compression_info(&cmp);
 		return PointerGetDatum(NULL);
 	}
 }
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d2..ba5af5db404 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -229,8 +229,10 @@ toast_tuple_try_compression(ToastTupleContext *ttc, int attribute)
 	Datum	   *value = &ttc->ttc_values[attribute];
 	Datum		new_value;
 	ToastAttrInfo *attr = &ttc->ttc_attr[attribute];
+	Form_pg_attribute att = TupleDescAttr(ttc->ttc_rel->rd_att, attribute);
+	CompressionInfo cmp = setup_compression_info(attr->tai_compression, att);
 
-	new_value = toast_compress_datum(*value, attr->tai_compression);
+	new_value = toast_compress_datum(*value, cmp);
 
 	if (DatumGetPointer(new_value) != NULL)
 	{
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..8e9d5b44752 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,6 +13,10 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#include "varatt.h"
+#include "toast_compression.h"
+#include "catalog/pg_attribute.h"
+
 /*
  * GUC support.
  *
@@ -23,24 +27,38 @@
 extern PGDLLIMPORT int default_toast_compression;
 
 /*
- * Built-in compression method ID.  The toast compression header will store
- * this in the first 2 bits of the raw length.  These built-in compression
- * method IDs are directly mapped to the built-in compression methods.
+ * TOAST compression methods enumeration.
+ *
+ * Each entry defines:
+ *   - NAME         : identifier for the compression algorithm
+ *   - VALUE        : numeric enum value
+ *   - METADATA type: struct type holding extra info (void when none)
  *
- * Don't use these values for anything other than understanding the meaning
- * of the raw bits from a varlena; in particular, if the goal is to identify
- * a compression method, use the constants TOAST_PGLZ_COMPRESSION, etc.
- * below. We might someday support more than 4 compression methods, but
- * we can never have more than 4 values in this enum, because there are
- * only 2 bits available in the places where this is stored.
+ * The INVALID entry is a sentinel and must remain last.
  */
+#define TOAST_COMPRESSION_LIST                                 \
+    X(PGLZ,         0,    void)    /* PostgreSQL LZ-based */   \
+    X(LZ4,          1,    void)    /* LZ4 algorithm */         \
+    X(INVALID,      2,    void) /* sentinel, must be last */
+
+
 typedef enum ToastCompressionId
 {
-	TOAST_PGLZ_COMPRESSION_ID = 0,
-	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+#define X(name,val,struct) TOAST_##name##_COMPRESSION_ID = (val),
+	TOAST_COMPRESSION_LIST
+#undef X
 } ToastCompressionId;
 
+#define TOAST_LAST_COMPRESSION_ID_BEFORE_EXT  TOAST_LZ4_COMPRESSION_ID
+
+typedef struct CompressionInfo
+{
+	char		cmethod;
+	/* Extended compression meta info */
+	varatt_cmp_extended *cmp_ext;
+} CompressionInfo;
+
+
 /*
  * Built-in compression methods.  pg_attribute will store these in the
  * attcompression column.  In attcompression, InvalidCompressionMethod
@@ -69,5 +87,7 @@ extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
 extern const char *GetCompressionMethodName(char method);
+extern CompressionInfo setup_compression_info(char cmethod, Form_pg_attribute att);
+extern void free_compression_info(CompressionInfo *info);
 
 #endif							/* TOAST_COMPRESSION_H */
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..e672766f91a 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -31,21 +31,26 @@ typedef struct toast_compress_header
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
-#define TOAST_COMPRESS_EXTSIZE(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
-
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
-	do { \
-		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
-		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method, cmp_ext) 				\
+	do { 																						\
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); 									\
+		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || 										\
+			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); 										\
+		if ((cm_method) <= TOAST_LAST_COMPRESSION_ID_BEFORE_EXT) { 								\
+			((toast_compress_header *) (ptr))->tcinfo = 										\
+				(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); 						\
+		} else { 																				\
+			/* For compression methods after lz4, use 11 in the top bits of tcinfo 				\
+				to indicate compression algorithm is stored in extended format. */ 				\
+			((toast_compress_header *) (ptr))->tcinfo = 										\
+				(len) | ((uint32) (VARATT_4BCE_MASK) << VARLENA_EXTSIZE_BITS);					\
+			Assert((cmp_ext) != NULL);															\
+			memcpy(VARATT_4BCE_HDR_PTR(ptr), (cmp_ext), 										\
+					sizeof(varatt_cmp_extended) + VARATT_4BCE_META_SIZE( cmp_ext->ext_hdr ));	\
+		} 																						\
 	} while (0)
 
-extern Datum toast_compress_datum(Datum value, char cmethod);
+extern Datum toast_compress_datum(Datum value, CompressionInfo cmp);
 extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d4998..72a49c2322e 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -328,7 +328,8 @@ typedef struct
 #define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+	( (VARATT_IS_4BCE(PTR)) ? (VARATT_4BCE_CMP_METHOD(VARATT_4BCE_HDR_PTR(PTR)->ext_hdr)) \
+	: (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS))
 
 /* Same for external Datums; but note argument is a struct varatt_external */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
@@ -340,8 +341,17 @@ typedef struct
 	do { \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
 			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
+		if ((cm) <= TOAST_LAST_COMPRESSION_ID_BEFORE_EXT) \
+		{ \
+			/* Store the actual method in va_extinfo */ \
+			((toast_pointer).va_extinfo = \
+				(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
+		} \
+		else \
+		{ \
+			/* Store 11 in the top bits, meaning "extended" method. */ 				\
+			(toast_pointer).va_extinfo = (uint32)(len) | (VARATT_4BCE_MASK << VARLENA_EXTSIZE_BITS ); \
+		} \
 	} while (0)
 
 /*
@@ -355,4 +365,61 @@ typedef struct
 	(VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
 	 (toast_pointer).va_rawsize - VARHDRSZ)
 
+typedef struct varatt_cmp_extended
+{
+	uint32		ext_hdr;		/* [ size:24 | type:8 ] */
+	char		ext_data[FLEXIBLE_ARRAY_MEMBER];	/* algorithm-specific meta */
+} varatt_cmp_extended;
+
+/*--------------------------------------------------------------------*/
+/* 1) Detect the extended compression					              */
+/*    (top-2 mode bits of va_tcinfo are 0b11)                         */
+#define VARATT_4BCE_MASK   0x0003
+
+#define VARATT_IS_4BCE(ptr)                                            			\
+	((((varattrib_4b*)(ptr))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) 	\
+		== VARATT_4BCE_MASK)
+
+/*--------------------------------------------------------------------*/
+/* 2) Pointer to varatt_cmp_extended header (just after the 8-byte varlena head) */
+#define VARATT_4BCE_HDR_PTR(ptr) ((varatt_cmp_extended*)(((char*)(ptr)) + VARHDRSZ_COMPRESSED))
+#define VARATT_4BCE_GET_HDR(ptr) ((uint32)(VARATT_4BCE_HDR_PTR(ptr)->ext_hdr))
+
+/*--------------------------------------------------------------------*/
+/* 3) The 32-bit ext_hdr */
+/*    Layout:   [ meta size:24 bits | type:8 bits ] */
+#define VARATT_4BCE_TYPE_MASK  0x000000FF	/* low-order 8 bits  */
+#define VARATT_4BCE_SIZE_MASK  0xFFFFFF00	/* high-order 24 bits */
+
+#define VARATT_4BCE_SET_HDR(hdr, type, size24)                                  \
+	do {                                                                        \
+		Assert((uint32)(type)   <= VARATT_4BCE_TYPE_MASK);      /* 8 bits  */   \
+		Assert((uint32)(size24) <= (VARATT_4BCE_SIZE_MASK >> 8)); 				\
+		(hdr) = ( ((uint32)(type)) ) | ( ((uint32)(size24) << 8) );   			\
+	} while (0)
+
+#define VARATT_4BCE_CMP_METHOD(hdr)   ( (uint8)  ((hdr) & VARATT_4BCE_TYPE_MASK) )
+#define VARATT_4BCE_META_SIZE(hdr)   ( ((hdr) & VARATT_4BCE_SIZE_MASK) >> 8)
+
+/*--------------------------------------------------------------------*/
+/* 4) Derived helpers to jump inside the extension block */
+
+/* -> metadata begins immediately after the 4-byte ext header */
+#define VARATT_4BCE_META_PTR(ptr)    ( (void*) VARATT_4BCE_HDR_PTR(ptr)->ext_data )
+
+/* -> compressed bytes begins after metadata */
+#define VARATT_4BCE_DATA_PTR(ptr)                                      \
+	( (void*)( (char*)VARATT_4BCE_META_PTR(ptr)                        \
+			   + VARATT_4BCE_META_SIZE(VARATT_4BCE_HDR_PTR(ptr)->ext_hdr) ) )
+
+/* -> payload byte count */
+#define VARATT_4BCE_DATA_SIZE(ptr)                                    \
+	( VARSIZE_4B(ptr)                                                 \
+	  - VARHDRSZ_COMPRESSED                                           \
+	  - sizeof(varatt_cmp_extended)                                   \
+	  - VARATT_4BCE_META_SIZE(VARATT_4BCE_HDR_PTR(ptr)->ext_hdr) )
+
+/* Expects varatt_cmp_extended pointer */
+#define	VARATT_4BCE_HDRSZ(ptr) (VARHDRSZ_COMPRESSED + sizeof(varatt_cmp_extended) + VARATT_4BCE_META_SIZE((ptr)->ext_hdr))
+
 #endif
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e5879e00dff..ea28675e0c9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -482,6 +482,7 @@ CompositeIOData
 CompositeTypeStmt
 CompoundAffixFlag
 CompressFileHandle
+CompressionInfo
 CompressionLocation
 CompressorState
 ComputeXidHorizonsResult
@@ -4153,6 +4154,7 @@ uuid_t
 va_list
 vacuumingOptions
 validate_string_relopt
+varatt_cmp_extended
 varatt_expanded
 varattrib_1b
 varattrib_1b_e

base-commit: 0787646e1dce966395f211fb9475dcab32daae70
-- 
2.47.1

#28Robert Haas
robertmhaas@gmail.com
In reply to: Nikhil Kumar Veldanda (#27)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Fri, Apr 25, 2025 at 11:15 AM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

a. 24 bits for length → per-datum compression algorithm metadata is
capped at 16 MB, which is far more than any realistic compression
header.
b. 8 bits for algorithm id → up to 256 algorithms.
c. Zero-overhead when unused if an algorithm needs no per-datum
metadata (e.g., ZSTD-nodict),

I don't understand why we need to spend 24 bits on a length header
here. I agree with the idea of adding a 1-byte quantity for algorithm
here, but I don't see why we need anything more than that. If the
compression method is zstd-with-a-dict, then the payload data
presumably needs to start with the OID of the dictionary, but it seems
like in your schema every single datum would use these 3 bytes to
store the fact that sizeof(Oid) = 4. The code that interprets
zstd-with-dict datums should already know the header length. Even if
generic code that works with all types of compression needs to be able
to obtain the header length on a per-compression-type basis, there can
be some kind of callback or table for that, rather than storing it in
every single datum.

--
Robert Haas
EDB: http://www.enterprisedb.com

#29Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Robert Haas (#28)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Robert,

Thanks for raising that question. The idea behind including a 24-bit
length field alongside the 1-byte algorithm ID is to ensure that each
compressed datum self-describes its metadata size. This allows any
compression algorithm to embed variable-length metadata (up to 16 MB)
without the need for hard-coding header sizes. For instance, an
algorithm in feature might require different metadata lengths for each
datum, and a fixed header size table wouldn’t work. By storing the
length in the header, we maintain a generic and future-proof design. I
would greatly appreciate any feedback on this design. Thanks!

On Mon, Apr 28, 2025 at 7:50 AM Robert Haas <robertmhaas@gmail.com> wrote:

On Fri, Apr 25, 2025 at 11:15 AM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

a. 24 bits for length → per-datum compression algorithm metadata is
capped at 16 MB, which is far more than any realistic compression
header.
b. 8 bits for algorithm id → up to 256 algorithms.
c. Zero-overhead when unused if an algorithm needs no per-datum
metadata (e.g., ZSTD-nodict),

I don't understand why we need to spend 24 bits on a length header
here. I agree with the idea of adding a 1-byte quantity for algorithm
here, but I don't see why we need anything more than that. If the
compression method is zstd-with-a-dict, then the payload data
presumably needs to start with the OID of the dictionary, but it seems
like in your schema every single datum would use these 3 bytes to
store the fact that sizeof(Oid) = 4. The code that interprets
zstd-with-dict datums should already know the header length. Even if
generic code that works with all types of compression needs to be able
to obtain the header length on a per-compression-type basis, there can
be some kind of callback or table for that, rather than storing it in
every single datum.

--
Robert Haas
EDB: http://www.enterprisedb.com

--
Nikhil Veldanda

#30Nikita Malakhov
hukutoc@gmail.com
In reply to: Nikhil Kumar Veldanda (#29)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi,

Nikhil, please consider existing discussions on using dictionaries
(mentioned above by Aleksander) and extending the TOAST pointer [1]/messages/by-id/CAN-LCVMq2X=fhx7KLxfeDyb3P+BXuCkHC0g=9GF+JD4izfVa0Q@mail.gmail.com,
it seems you did not check them.

The same question Robert asked above - it's unclear why the header
wastes so much space. You mentioned metadata length - what metadata
do you mean there?

Also Robert pointed out very questionable approaches in your solution -
new dependencies crawling around user tables, new catalog table
with very unclear lifecycle (and, having new catalog table, immediately
having questions with pg_upgrade).
Currently I'm looking through the patch and could share my thoughts
later.

While reading this thread I've thought about storing a dictionary within
the table it is used for - IIUC on dictionary is used for just one
attribute,
so it does not make sense to make it global.

Also, I have a question regarding the Zstd implementation you propose -
does it provide a possibility for partial decompression?

Thanks!

[1]: /messages/by-id/CAN-LCVMq2X=fhx7KLxfeDyb3P+BXuCkHC0g=9GF+JD4izfVa0Q@mail.gmail.com
/messages/by-id/CAN-LCVMq2X=fhx7KLxfeDyb3P+BXuCkHC0g=9GF+JD4izfVa0Q@mail.gmail.com

--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#31Robert Haas
robertmhaas@gmail.com
In reply to: Nikhil Kumar Veldanda (#29)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Mon, Apr 28, 2025 at 5:32 PM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

Thanks for raising that question. The idea behind including a 24-bit
length field alongside the 1-byte algorithm ID is to ensure that each
compressed datum self-describes its metadata size. This allows any
compression algorithm to embed variable-length metadata (up to 16 MB)
without the need for hard-coding header sizes. For instance, an
algorithm in feature might require different metadata lengths for each
datum, and a fixed header size table wouldn’t work. By storing the
length in the header, we maintain a generic and future-proof design. I
would greatly appreciate any feedback on this design. Thanks!

I feel like I gave you some feedback on the design already, which was
that it seems like a waste of 3 bytes to me.

Don't get me wrong: I'm quite impressed by the way you're working on
this problem and I hope you stick around and keep working on it and
figure something out. But I don't quite understand the point of this
response: it seems like you're just restating what the design does
without really justifying it. The question here isn't whether a 3-byte
header can describe a length up to 16MB; I think we all know our
powers of two well enough to agree on the answer to that question. The
question is whether it's a good use of 3 bytes, and I don't think it
is.

I did consider the fact that future compression algorithms might want
to use variable-length headers; but I couldn't see a reason why we
shouldn't let each of those compression algorithms decide for
themselves how to encode whatever information they need. If a
compression algorithm needs a variable-length header, then it just
needs to make that header self-describing. Worst case scenario, it can
make the first byte of that variable-length header a length byte, and
then go from there; but it's probably possible to be even smarter and
use less than a full byte. Say for example we store a dictionary ID
that in concept is a 32-bit quantity but we use a variable-length
integer representation for it. It's easy to see that we shouldn't ever
need more than 3 bits for that so a full length byte is overkill and,
in fact, would undermine the value of a variable-length representation
rather severely. (I suspect it's a bad idea anyway, but it's a worse
idea if you burn a full byte on a length header.)

But there's an even larger question here too, which is why we're
having some kind of discussion about generalized metadata when the
current project seemingly only requires a 4-byte dictionary OID. If
you have some other use of this space in mind, I don't think you've
told us what it is. If you don't, then I'm not sure why we're
designing around an up-to-16MB variable-length quantity when what we
have before us is a 4-byte fixed-length quantity.

Moreover, even if you do have some (undisclosed) idea about what else
might be stored in this metadata area, why would it be important or
even desirable to have the length of that area represented in some
uniform way across compression methods? There's no obvious need for
any code outside the compression method itself to be able to decompose
the Datum into a metadata portion and a payload portion. After all,
the metadata portion could be anything so there's no way for anything
but the compression method to interpret it usefully. If we do want to
have outside code be able to ask questions, we could design some kind
of callback interface - e.g. if we end up with multiple compression
methods that store dictionary OIDs and they maybe do it in different
ways, each could provide an
"extract-the-dictionary-OID-from-this-datum" callback and each
compression method can implement that however it likes.

Maybe you can argue that we will eventually end up with various
compression method callbacks each of which is capable of working on
the metadata, and so then we might want to take an initial slice of a
toasted datum that is just big enough to allow that to work. But that
is pretty hypothetical, and in practice the first chunk of the TOAST
value (~2k) seems like it'd probably work well for most cases.

So, again, if you want us to take seriously the idea of dedicating 3
bytes per Datum to something, you need to give us a really good reason
for so doing. The fact a 24-bit metadata length can describe a
metadata header of up to 2^24 bits isn't a reason, good or bad. It's
just math.

--
Robert Haas
EDB: http://www.enterprisedb.com

#32Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Robert Haas (#31)
2 attachment(s)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Robert

But I don't quite understand the point of this
response: it seems like you're just restating what the design does
without really justifying it. The question here isn't whether a 3-byte
header can describe a length up to 16MB; I think we all know our
powers of two well enough to agree on the answer to that question. The
question is whether it's a good use of 3 bytes, and I don't think it
is.

My initial decision to include a 3‑byte length field was driven by two goals:
1. Avoid introducing separate callbacks for each algorithm.
2. Provide a single, algorithm-agnostic mechanism for handling
metadata length.

After re-evaluating based on your feedback, I agree that the fixed
overhead of a 3-byte length field outweighs its benefit; per-algorithm
callbacks deliver the same functionality while saving three bytes per
datum.

I did consider the fact that future compression algorithms might want
to use variable-length headers; but I couldn't see a reason why we
shouldn't let each of those compression algorithms decide for
themselves how to encode whatever information they need. If a
compression algorithm needs a variable-length header, then it just
needs to make that header self-describing. Worst case scenario, it can
make the first byte of that variable-length header a length byte, and
then go from there; but it's probably possible to be even smarter and
use less than a full byte. Say for example we store a dictionary ID
that in concept is a 32-bit quantity but we use a variable-length
integer representation for it. It's easy to see that we shouldn't ever
need more than 3 bits for that so a full length byte is overkill and,
in fact, would undermine the value of a variable-length representation
rather severely. (I suspect it's a bad idea anyway, but it's a worse
idea if you burn a full byte on a length header.)

I agree. Each compression algorithm can decide its own metadata size
overhead. Callbacks can provide this information as well rather than
storing in fixed length bytes(3 bytes). The revised patch introduces a
"toast_cmpid_meta_size(const varatt_cmp_extended *hdr)", which
calculates the metadata size.

But there's an even larger question here too, which is why we're
having some kind of discussion about generalized metadata when the
current project seemingly only requires a 4-byte dictionary OID. If
you have some other use of this space in mind, I don't think you've
told us what it is. If you don't, then I'm not sure why we're
designing around an up-to-16MB variable-length quantity when what we
have before us is a 4-byte fixed-length quantity.

This project only requires 4 bytes of fixed-size metadata to store the
dictionary ID.

Updated design for extending varattrib_4b compression

1. extensible header

/*
* varatt_cmp_extended: an optional per‐datum header for extended
compression method.
* Only used when va_tcinfo's top two bits are "11".
*/
typedef struct varatt_cmp_extended
{
uint8 cmp_alg;
char cmp_meta[FLEXIBLE_ARRAY_MEMBER]; /*
algorithm‐specific metadata */
} varatt_cmp_extended;

2. Algorithm registry and metadata size dispatch

static inline uint32
unsupported_meta_size(const varatt_cmp_extended *hdr)
{
elog(ERROR, "toast_cmpid_meta_size called for unsupported
compression algorithm");
return 0; /* unreachable */
}

/* no metadata for plain-ZSTD */
static inline uint32
zstd_nodict_meta_size(const varatt_cmp_extended *hdr)
{
return 0;
}

static inline uint32
zstd_dict_meta_size(const varatt_cmp_extended *hdr)
{
return sizeof(Oid);
}

/*
* TOAST compression methods enumeration.
*
* NAME : algorithm identifier
* VALUE : enum value
* META-SIZE-FN : Calculates algorithm metadata size.
*/
#define TOAST_COMPRESSION_LIST \
X(PGLZ, 0, unsupported_meta_size) \
X(LZ4, 1, unsupported_meta_size) \
X(ZSTD_NODICT, 2, zstd_nodict_meta_size) \
X(ZSTD_DICT, 3, zstd_dict_meta_size) \
X(INVALID, 4, unsupported_meta_size) /* sentinel */

/* Compression algorithm identifiers */
typedef enum ToastCompressionId
{
#define X(name,val,fn) TOAST_##name##_COMPRESSION_ID = (val),
TOAST_COMPRESSION_LIST
#undef X
} ToastCompressionId;

/* lookup table to check if compression method uses extended format */
static const bool toast_cmpid_extended[] = {
#define X(name,val,fn) \
/* PGLZ, LZ4 don't use extended format */ \
[TOAST_##name##_COMPRESSION_ID] = \
((val) != TOAST_PGLZ_COMPRESSION_ID && \
(val) != TOAST_LZ4_COMPRESSION_ID && \
(val) != TOAST_INVALID_COMPRESSION_ID),
TOAST_COMPRESSION_LIST
#undef X
};

#define TOAST_CMPID_EXTENDED(alg) (toast_cmpid_extended[alg])

/*
* Prototype for a per-datum metadata-size callback:
* given a pointer to the extended header, return
* how many metadata bytes follow it.
*/
typedef uint32 (*ToastMetaSizeFn) (const varatt_cmp_extended *hdr);

/* Callback table—indexed by ToastCompressionId */
static const ToastMetaSizeFn toast_meta_size_fns[] = {
#define X(name,val,fn) [TOAST_##name##_COMPRESSION_ID] = fn,
TOAST_COMPRESSION_LIST
#undef X
};

/* Calculates algorithm metadata size */
static inline uint32
toast_cmpid_meta_size(const varatt_cmp_extended *hdr)
{
Assert(hdr != NULL);
return toast_meta_size_fns[hdr->cmp_alg] (hdr);
}

Each compression algorithm provides a static callback that returns the
size of its metadata, given a pointer to the varatt_cmp_extended
header. Algorithms with fixed-size metadata return a constant, while
algorithms with variable-length metadata are responsible for defining
and parsing their own internal headers to compute the metadata size.

3. Resulting on-disk layouts for zstd

ZSTD (nodict) — datum on‑disk layout

+----------------------------------+
| va_header (uint32) |
+----------------------------------+
| va_tcinfo (uint32) | ← top two bits = 11 (extended)
+----------------------------------+
| cmp_alg (uint8) | ← (ZSTD_NODICT)
+----------------------------------+
| compressed bytes … | ← ZSTD frame
+----------------------------------+

ZSTD(dict) — datum on‑disk layout

+----------------------------------+
| va_header (uint32) |
+----------------------------------+
| va_tcinfo (uint32) | ← top two bits = 11 (extended)
+----------------------------------+
| cmp_alg (uint8) | ← (ZSTD_DICT)
+----------------------------------+
| dict_id (uint32) | ← dictionary OID
+----------------------------------+
| compressed bytes … | ← ZSTD frame
+----------------------------------+

I hope this updated design addresses your concerns. I would appreciate
any further feedback you may have. Thanks again for your guidance—it's
been very helpful.

v20-0001-varattrib_4b-design-proposal-to-make-it-extended.patch:
varattrib_4b extensibility – adds varatt_cmp_extended, metadata size
dispatch and useful macros; behaviour unchanged.
v20-0002-zstd-nodict-compression.patch: Plain ZSTD (non dict) support.

--
Nikhil Veldanda

Attachments:

v20-0002-zstd-nodict-compression.patchapplication/x-patch; name=v20-0002-zstd-nodict-compression.patchDownload
From dc6172c6945354634063b03215dc6798ae22cc2f Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Sat, 3 May 2025 02:14:02 +0000
Subject: [PATCH v20 2/2] zstd nodict compression

---
 contrib/amcheck/verify_heapam.c               |   1 +
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/reloptions.c        |  14 +-
 src/backend/access/common/toast_compression.c | 171 +++++++++++++++++-
 src/backend/access/common/toast_internals.c   |   4 +
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/psql/describe.c                       |   5 +-
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/access/toast_compression.h        |  36 +++-
 src/include/access/toast_internals.h          |   3 +-
 src/include/utils/attoptcache.h               |   1 +
 src/test/regress/expected/compression.out     |   5 +-
 src/test/regress/expected/compression_1.out   |   3 +
 .../expected/compression_zstd_nodict.out      | 152 ++++++++++++++++
 .../expected/compression_zstd_nodict_1.out    | 103 +++++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression.sql          |   1 +
 .../regress/sql/compression_zstd_nodict.sql   |  82 +++++++++
 20 files changed, 581 insertions(+), 26 deletions(-)
 create mode 100644 src/test/regress/expected/compression_zstd_nodict.out
 create mode 100644 src/test/regress/expected/compression_zstd_nodict_1.out
 create mode 100644 src/test/regress/sql/compression_zstd_nodict.sql

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index d7c2ac6951a..111bb308341 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1792,6 +1792,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_NODICT_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 01419d1c65f..451230023ec 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -246,10 +246,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
 				TOAST_PGLZ_COMPRESSION_ID)
@@ -485,6 +485,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_NODICT_COMPRESSION_ID:
+			return zstd_nodict_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +530,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_NODICT_COMPRESSION_ID:
+			return zstd_nodict_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 46c1dce222d..1267668a242 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -24,6 +24,7 @@
 #include "access/nbtree.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
+#include "access/toast_compression.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
@@ -381,7 +382,15 @@ static relopt_int intRelOpts[] =
 		},
 		-1, 0, 1024
 	},
-
+	{
+		{
+			"zstd_level",
+			"Set column's ZSTD compression level",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_LEVEL, MIN_ZSTD_LEVEL, MAX_ZSTD_LEVEL
+	},
 	/* list terminator */
 	{{NULL}}
 };
@@ -2097,7 +2106,8 @@ attribute_reloptions(Datum reloptions, bool validate)
 {
 	static const relopt_parse_elt tab[] = {
 		{"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)},
-		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}
+		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)},
+		{"zstd_level", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_level)},
 	};
 
 	return (bytea *) build_reloptions(reloptions, validate,
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 5e5d42d80ef..02823d1c435 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,6 +17,10 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
@@ -26,11 +30,19 @@
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
 
-#define NO_LZ4_SUPPORT() \
+#ifdef USE_ZSTD
+#define ZSTD_CHECK_ERROR(zstd_ret, msg) \
+	do { \
+		if (ZSTD_isError(zstd_ret)) \
+			ereport(ERROR, (errmsg("%s: %s", (msg), ZSTD_getErrorName(zstd_ret)))); \
+	} while (0)
+#endif
+
+#define COMPRESSION_METHOD_NOT_SUPPORTED(method) \
 	ereport(ERROR, \
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
-			 errmsg("compression method lz4 not supported"), \
-			 errdetail("This functionality requires the server to be built with lz4 support.")))
+			 errmsg("compression method %s not supported", method), \
+			 errdetail("This functionality requires the server to be built with %s support.", method)))
 
 /*
  * Compress a varlena using PGLZ.
@@ -140,7 +152,7 @@ struct varlena *
 lz4_compress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		valsize;
@@ -183,7 +195,7 @@ struct varlena *
 lz4_decompress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -216,7 +228,7 @@ struct varlena *
 lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -246,6 +258,129 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 #endif
 }
 
+/* Compress datum using ZSTD with optional dictionary (using cdict) */
+struct varlena *
+zstd_nodict_compress_datum(const struct varlena *value, CompressionInfo cmp)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	void	   *dest;
+	size_t		cmp_size;
+
+	/* Allocate space for the compressed varlena (header + data) */
+	compressed = (struct varlena *) palloc(max_size + VARATT_4BCE_HDRSZ(cmp.cmp_ext));
+	dest = (char *) compressed + VARATT_4BCE_HDRSZ(cmp.cmp_ext);
+
+	cmp_size = ZSTD_compress(dest,
+							 max_size,
+							 VARDATA_ANY(value),
+							 valsize,
+							 cmp.zstd_level);
+
+	if (ZSTD_isError(cmp_size))
+	{
+		pfree(compressed);
+		ZSTD_CHECK_ERROR(cmp_size, "ZSTD compression failed");
+	}
+
+	/*
+	 * If compression did not reduce size, return NULL so that the
+	 * uncompressed data is stored
+	 */
+	if (cmp_size > valsize)
+	{
+		pfree(compressed);
+		return NULL;
+	}
+
+	/* Set the compressed size in the varlena header */
+	SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARATT_4BCE_HDRSZ(cmp.cmp_ext));
+	return compressed;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+	return NULL;
+#endif
+}
+
+/* Decompression routine */
+struct varlena *
+zstd_nodict_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		cmp_size_exhdr = VARATT_4BCE_PAYLOAD_SIZE(value);
+	struct varlena *result;
+	size_t		uncmp_size;
+
+	/* Allocate space for the uncompressed data */
+	result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+	uncmp_size = ZSTD_decompress(VARDATA(result),
+								 actual_size_exhdr,
+								 VARATT_4BCE_PAYLOAD_PTR(value),
+								 cmp_size_exhdr);
+
+	if (ZSTD_isError(uncmp_size))
+	{
+		pfree(result);
+		ZSTD_CHECK_ERROR(uncmp_size, "ZSTD decompression failed");
+	}
+
+	/* Set final size in the varlena header */
+	SET_VARSIZE(result, uncmp_size + VARHDRSZ);
+	return result;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum using the streaming API and optional dictionary */
+struct varlena *
+zstd_nodict_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	size_t		ret;
+	ZSTD_DCtx  *ZstdDecompressionCtx = ZSTD_createDCtx();
+
+	inBuf.src = VARATT_4BCE_PAYLOAD_PTR(value);
+	inBuf.size = VARATT_4BCE_PAYLOAD_SIZE(value);
+	inBuf.pos = 0;
+
+	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+	outBuf.dst = (char *) result + VARHDRSZ;
+	outBuf.size = slicelength;
+	outBuf.pos = 0;
+
+	/* Common decompression loop */
+	while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+	{
+		ret = ZSTD_decompressStream(ZstdDecompressionCtx, &outBuf, &inBuf);
+		if (ZSTD_isError(ret))
+		{
+			pfree(result);
+			ZSTD_freeDCtx(ZstdDecompressionCtx);
+			ZSTD_CHECK_ERROR(ret, "zstd decompression failed");
+		}
+	}
+
+	Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+	SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+	ZSTD_freeDCtx(ZstdDecompressionCtx);
+	return result;
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+	return NULL;
+#endif
+}
+
 /*
  * Extract compression ID from a varlena.
  *
@@ -293,10 +428,17 @@ CompressionNameToMethod(const char *compression)
 	else if (strcmp(compression, "lz4") == 0)
 	{
 #ifndef USE_LZ4
-		NO_LZ4_SUPPORT();
+		COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd_nodict") == 0)
+	{
+#ifndef USE_ZSTD
+		COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+#endif
+		return TOAST_ZSTD_NODICT_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -313,6 +455,8 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_NODICT_COMPRESSION:
+			return "zstd_nodict";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
@@ -326,6 +470,7 @@ setup_compression_info(char cmethod, Form_pg_attribute att)
 
 	/* initialize from the attribute’s default settings */
 	info.cmethod = cmethod;
+	info.zstd_level = DEFAULT_ZSTD_LEVEL;
 	info.cmp_ext = NULL;
 
 	/* If the compression method is not valid, use the current default */
@@ -337,6 +482,18 @@ setup_compression_info(char cmethod, Form_pg_attribute att)
 		case TOAST_PGLZ_COMPRESSION:
 		case TOAST_LZ4_COMPRESSION:
 			break;
+		case TOAST_ZSTD_NODICT_COMPRESSION:
+			{
+				AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+				if (aopt != NULL)
+					info.zstd_level = aopt->zstd_level;
+
+				info.cmp_ext = palloc(sizeof(varatt_cmp_extended));
+
+				VARATT_4BCE_SET_HDR(info.cmp_ext, TOAST_ZSTD_NODICT_COMPRESSION_ID);
+			}
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", info.cmethod);
 	}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 83b537d51bf..5521d78bb48 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -68,6 +68,10 @@ toast_compress_datum(Datum value, CompressionInfo cmp)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_NODICT_COMPRESSION:
+			tmp = zstd_nodict_compress_datum((const struct varlena *) value, cmp);
+			cmid = TOAST_ZSTD_NODICT_COMPRESSION_ID;
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 3e4d5568bde..5b9151c7e16 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5301,6 +5301,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_NODICT_COMPRESSION_ID:
+			result = "zstd_nodict";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 2f8cbd86759..948454e2093 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd_nodict", TOAST_ZSTD_NODICT_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 34826d01380..f2d2ca39514 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -756,7 +756,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd_nodict'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1d08268393e..3831a7fab03 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2171,8 +2171,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'n' ? "zstd_nodict" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index c916b9299a8..2441acf41ce 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2875,11 +2875,11 @@ match_previous_words(int pattern_id,
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET ( */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "(") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "("))
-		COMPLETE_WITH("n_distinct", "n_distinct_inherited");
+		COMPLETE_WITH("n_distinct", "n_distinct_inherited", "zstd_level");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION"))
-		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4");
+		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4", "ZSTD_NODICT");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET EXPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "EXPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "EXPRESSION"))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 1aef65cde99..94d56c7a4ef 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -16,6 +16,10 @@
 #include "varatt.h"
 #include "catalog/pg_attribute.h"
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 /*
  * GUC support.
  *
@@ -36,6 +40,13 @@ unsupported_meta_size(const varatt_cmp_extended *hdr)
 	return 0;					/* unreachable */
 }
 
+/* no metadata for plain-ZSTD */
+static inline uint32
+zstd_nodict_meta_size(const varatt_cmp_extended *hdr)
+{
+	return 0;
+}
+
 /*
  * TOAST compression methods enumeration.
  *
@@ -43,10 +54,11 @@ unsupported_meta_size(const varatt_cmp_extended *hdr)
  * VALUE        : enum value
  * META-SIZE-FN	: Calculates algorithm metadata size.
  */
-#define TOAST_COMPRESSION_LIST                  \
-	X(PGLZ,         0, unsupported_meta_size)	\
-	X(LZ4,          1, unsupported_meta_size)  	\
-	X(INVALID,      2, unsupported_meta_size)	/* sentinel */
+#define TOAST_COMPRESSION_LIST					\
+	X(PGLZ,			0, unsupported_meta_size)	\
+	X(LZ4,			1, unsupported_meta_size)	\
+	X(ZSTD_NODICT,	2, zstd_nodict_meta_size)	\
+	X(INVALID,		3, unsupported_meta_size)	/* sentinel */
 
 /* Compression algorithm identifiers */
 typedef enum ToastCompressionId
@@ -96,6 +108,7 @@ toast_cmpid_meta_size(const varatt_cmp_extended *hdr)
 typedef struct CompressionInfo
 {
 	char		cmethod;
+	int			zstd_level;
 	varatt_cmp_extended *cmp_ext;	/* non-NULL only if uses extended
 									 * compression methods */
 } CompressionInfo;
@@ -107,10 +120,20 @@ typedef struct CompressionInfo
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_NODICT_COMPRESSION	'n'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
 
+#ifdef USE_ZSTD
+#define DEFAULT_ZSTD_LEVEL					ZSTD_CLEVEL_DEFAULT
+#define MIN_ZSTD_LEVEL						(int)-ZSTD_BLOCKSIZE_MAX
+#define MAX_ZSTD_LEVEL						22
+#else
+#define DEFAULT_ZSTD_LEVEL					0
+#define MIN_ZSTD_LEVEL						0
+#define MAX_ZSTD_LEVEL						0
+#endif
 
 /* pglz compression/decompression routines */
 extern struct varlena *pglz_compress_datum(const struct varlena *value);
@@ -124,6 +147,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd nodict compression/decompression routines */
+extern struct varlena *zstd_nodict_compress_datum(const struct varlena *value, CompressionInfo cmp);
+extern struct varlena *zstd_nodict_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_nodict_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index f4a4829ad17..b1859ef202b 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -35,7 +35,8 @@ typedef struct toast_compress_header
 	do {																				\
 		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);								\
 		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||								\
-				(cm_method) == TOAST_LZ4_COMPRESSION_ID);								\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID	||								\
+				(cm_method) == TOAST_ZSTD_NODICT_COMPRESSION_ID);						\
 		if (!TOAST_CMPID_EXTENDED((cm_method)))											\
 		{																				\
 			((toast_compress_header *)(ptr))->tcinfo =									\
diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h
index f684a772af5..51d65ebd646 100644
--- a/src/include/utils/attoptcache.h
+++ b/src/include/utils/attoptcache.h
@@ -21,6 +21,7 @@ typedef struct AttributeOpts
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	float8		n_distinct;
 	float8		n_distinct_inherited;
+	int			zstd_level;
 } AttributeOpts;
 
 extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum);
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 4dd9ee7200d..c7e108a0f52 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -238,10 +238,11 @@ NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd_nodict.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd_nodict.
+SET default_toast_compression = 'zstd_nodict';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 -- test alter compression method
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out
index 7bd7642b4b9..5b10d8c5259 100644
--- a/src/test/regress/expected/compression_1.out
+++ b/src/test/regress/expected/compression_1.out
@@ -233,6 +233,9 @@ HINT:  Available values: pglz.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
 HINT:  Available values: pglz.
+SET default_toast_compression = 'zstd_nodict';
+ERROR:  invalid value for parameter "default_toast_compression": "zstd_nodict"
+HINT:  Available values: pglz.
 SET default_toast_compression = 'lz4';
 ERROR:  invalid value for parameter "default_toast_compression": "lz4"
 HINT:  Available values: pglz.
diff --git a/src/test/regress/expected/compression_zstd_nodict.out b/src/test/regress/expected/compression_zstd_nodict.out
new file mode 100644
index 00000000000..d80814e0492
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_nodict.out
@@ -0,0 +1,152 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE;
+NOTICE:  table "cmdata_zstd_nodict" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict (
+    f1 TEXT COMPRESSION zstd_nodict
+);
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd_nodict;
+                                  Table "public.cmdata_zstd_nodict"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd_nodict |              | 
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd_nodict
+GROUP BY pg_column_compression(f1);
+ compression_method | row_count 
+--------------------+-----------
+ zstd_nodict        |        15
+(1 row)
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd_nodict;
+                     data_slice                     
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+(15 rows)
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict_2;
+NOTICE:  table "cmdata_zstd_nodict_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_nodict_2;
+                                 Table "public.cmdata_zstd_nodict_2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd_nodict |              | 
+
+DROP TABLE cmdata_zstd_nodict_2;
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict;
+NOTICE:  materialized view "compressmv_zstd_nodict" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS
+  SELECT f1 FROM cmdata_zstd_nodict;
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd_nodict;
+                          Materialized view "public.compressmv_zstd_nodict"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1
+   FROM cmdata_zstd_nodict;
+
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd_nodict;
+ mv_compression 
+----------------
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+(15 rows)
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd_nodict
+SET f1 = f1 || ' UPDATED';
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd_nodict;
+ preview 
+---------
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+(15 rows)
+
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd_nodict;
+DROP TABLE cmdata_zstd_nodict;
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_zstd_nodict_1.out b/src/test/regress/expected/compression_zstd_nodict_1.out
new file mode 100644
index 00000000000..161d11fcae2
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_nodict_1.out
@@ -0,0 +1,103 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE;
+NOTICE:  table "cmdata_zstd_nodict" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict (
+    f1 TEXT COMPRESSION zstd_nodict
+);
+ERROR:  compression method zstd_nodict not supported
+DETAIL:  This functionality requires the server to be built with zstd_nodict support.
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 1: INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('12345678...
+                    ^
+QUERY:  INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004))
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd_nodict;
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd_nodict
+GROUP BY pg_column_compression(f1);
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 3: FROM cmdata_zstd_nodict
+             ^
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd_nodict;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 2: FROM cmdata_zstd_nodict;
+             ^
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict_2;
+NOTICE:  table "cmdata_zstd_nodict_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION);
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 1: CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict I...
+                                                ^
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_nodict_2;
+DROP TABLE cmdata_zstd_nodict_2;
+ERROR:  table "cmdata_zstd_nodict_2" does not exist
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict;
+NOTICE:  materialized view "compressmv_zstd_nodict" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS
+  SELECT f1 FROM cmdata_zstd_nodict;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 2:   SELECT f1 FROM cmdata_zstd_nodict;
+                         ^
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd_nodict;
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd_nodict;
+ERROR:  relation "compressmv_zstd_nodict" does not exist
+LINE 2: FROM compressmv_zstd_nodict;
+             ^
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd_nodict
+SET f1 = f1 || ' UPDATED';
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 1: UPDATE cmdata_zstd_nodict
+               ^
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd_nodict;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 2: FROM cmdata_zstd_nodict;
+             ^
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd_nodict;
+ERROR:  materialized view "compressmv_zstd_nodict" does not exist
+DROP TABLE cmdata_zstd_nodict;
+ERROR:  table "cmdata_zstd_nodict" does not exist
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a424be2a6bf..75cea22c418 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_zstd_nodict memoize stats predicate numa
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index 490595fcfb2..27979eb7997 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -102,6 +102,7 @@ CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 SET default_toast_compression = 'I do not exist compression';
+SET default_toast_compression = 'zstd_nodict';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 
diff --git a/src/test/regress/sql/compression_zstd_nodict.sql b/src/test/regress/sql/compression_zstd_nodict.sql
new file mode 100644
index 00000000000..e1f18004cc9
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd_nodict.sql
@@ -0,0 +1,82 @@
+\set HIDE_TOAST_COMPRESSION false
+
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE;
+CREATE TABLE cmdata_zstd_nodict (
+    f1 TEXT COMPRESSION zstd_nodict
+);
+
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004));
+  END LOOP;
+END $$;
+
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd_nodict;
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd_nodict
+GROUP BY pg_column_compression(f1);
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd_nodict;
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict_2;
+CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_nodict_2;
+DROP TABLE cmdata_zstd_nodict_2;
+
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict;
+CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS
+  SELECT f1 FROM cmdata_zstd_nodict;
+
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd_nodict;
+
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd_nodict;
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd_nodict
+SET f1 = f1 || ' UPDATED';
+
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd_nodict;
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd_nodict;
+DROP TABLE cmdata_zstd_nodict;
+
+\set HIDE_TOAST_COMPRESSION true
-- 
2.47.1

v20-0001-varattrib_4b-design-proposal-to-make-it-extended.patchapplication/x-patch; name=v20-0001-varattrib_4b-design-proposal-to-make-it-extended.patchDownload
From 3043cfc51dc345683b5f2aac6b0c431680a476b6 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Sat, 3 May 2025 01:57:15 +0000
Subject: [PATCH v20 1/2] varattrib_4b design proposal to make it extended to
 support multiple compression algorithms.

---
 contrib/amcheck/verify_heapam.c               |  2 +-
 src/backend/access/brin/brin_tuple.c          |  4 +-
 src/backend/access/common/detoast.c           |  6 +-
 src/backend/access/common/indextuple.c        |  5 +-
 src/backend/access/common/toast_compression.c | 38 ++++++++-
 src/backend/access/common/toast_internals.c   | 18 ++--
 src/backend/access/table/toast_helper.c       |  4 +-
 src/include/access/toast_compression.h        | 85 ++++++++++++++++---
 src/include/access/toast_internals.h          | 38 ++++++---
 src/include/varatt.h                          | 75 +++++++++++++++-
 src/tools/pgindent/typedefs.list              |  2 +
 11 files changed, 232 insertions(+), 45 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index aa9cccd1da4..d7c2ac6951a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1786,7 +1786,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_get_compression_id(attr);
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..9c1e22e98c6 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -223,6 +223,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			{
 				Datum		cvalue;
 				char		compression;
+				CompressionInfo cmp;
 				Form_pg_attribute att = TupleDescAttr(brdesc->bd_tupdesc,
 													  keyno);
 
@@ -237,7 +238,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 				else
 					compression = InvalidCompressionMethod;
 
-				cvalue = toast_compress_datum(value, compression);
+				cmp = setup_compression_info(compression, att);
+				cvalue = toast_compress_datum(value, cmp);
 
 				if (DatumGetPointer(cvalue) != NULL)
 				{
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..01419d1c65f 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -478,7 +478,7 @@ toast_decompress_datum(struct varlena *attr)
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
@@ -514,14 +514,14 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 	 * have been seen to give wrong results if passed an output size that is
 	 * more than the data's true decompressed size.
 	 */
-	if ((uint32) slicelength >= TOAST_COMPRESS_EXTSIZE(attr))
+	if ((uint32) slicelength >= VARDATA_COMPRESSED_GET_EXTSIZE(attr))
 		return toast_decompress_datum(attr);
 
 	/*
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data slice using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 1986b943a28..0386f5a1491 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -123,9 +123,10 @@ index_form_tuple_context(TupleDesc tupleDescriptor,
 			 att->attstorage == TYPSTORAGE_MAIN))
 		{
 			Datum		cvalue;
+			CompressionInfo cmp;
 
-			cvalue = toast_compress_datum(untoasted_values[i],
-										  att->attcompression);
+			cmp = setup_compression_info(att->attcompression, att);
+			cvalue = toast_compress_datum(untoasted_values[i], cmp);
 
 			if (DatumGetPointer(cvalue) != NULL)
 			{
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97e..5e5d42d80ef 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -21,6 +21,7 @@
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
+#include "utils/attoptcache.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
@@ -266,7 +267,10 @@ toast_get_compression_id(struct varlena *attr)
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)
+			&& VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) == VARATT_4BCE_MASK)
+			cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(detoast_external_attr(attr));
+		else
 			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 	}
 	else if (VARATT_IS_COMPRESSED(attr))
@@ -314,3 +318,35 @@ GetCompressionMethodName(char method)
 			return NULL;		/* keep compiler quiet */
 	}
 }
+
+CompressionInfo
+setup_compression_info(char cmethod, Form_pg_attribute att)
+{
+	CompressionInfo info;
+
+	/* initialize from the attribute’s default settings */
+	info.cmethod = cmethod;
+	info.cmp_ext = NULL;
+
+	/* If the compression method is not valid, use the current default */
+	if (!CompressionMethodIsValid(cmethod))
+		info.cmethod = default_toast_compression;
+
+	switch (info.cmethod)
+	{
+		case TOAST_PGLZ_COMPRESSION:
+		case TOAST_LZ4_COMPRESSION:
+			break;
+		default:
+			elog(ERROR, "invalid compression method %c", info.cmethod);
+	}
+
+	return info;
+}
+
+void
+free_compression_info(CompressionInfo *info)
+{
+	if (info->cmp_ext != NULL)
+		pfree(info->cmp_ext);
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346ce..83b537d51bf 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -43,25 +43,22 @@ static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
  * ----------
  */
 Datum
-toast_compress_datum(Datum value, char cmethod)
+toast_compress_datum(Datum value, CompressionInfo cmp)
 {
 	struct varlena *tmp = NULL;
 	int32		valsize;
 	ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID;
+	varatt_cmp_extended *cmp_ext = cmp.cmp_ext;
 
 	Assert(!VARATT_IS_EXTERNAL(DatumGetPointer(value)));
 	Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value)));
 
 	valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(value));
 
-	/* If the compression method is not valid, use the current default */
-	if (!CompressionMethodIsValid(cmethod))
-		cmethod = default_toast_compression;
-
 	/*
 	 * Call appropriate compression routine for the compression method.
 	 */
-	switch (cmethod)
+	switch (cmp.cmethod)
 	{
 		case TOAST_PGLZ_COMPRESSION:
 			tmp = pglz_compress_datum((const struct varlena *) value);
@@ -72,11 +69,14 @@ toast_compress_datum(Datum value, char cmethod)
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
 		default:
-			elog(ERROR, "invalid compression method %c", cmethod);
+			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
 
 	if (tmp == NULL)
+	{
+		free_compression_info(&cmp);
 		return PointerGetDatum(NULL);
+	}
 
 	/*
 	 * We recheck the actual size even if compression reports success, because
@@ -92,13 +92,15 @@ toast_compress_datum(Datum value, char cmethod)
 	{
 		/* successful compression */
 		Assert(cmid != TOAST_INVALID_COMPRESSION_ID);
-		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid);
+		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD_INFO(tmp, valsize, cmid, cmp_ext);
+		free_compression_info(&cmp);
 		return PointerGetDatum(tmp);
 	}
 	else
 	{
 		/* incompressible data */
 		pfree(tmp);
+		free_compression_info(&cmp);
 		return PointerGetDatum(NULL);
 	}
 }
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d2..ba5af5db404 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -229,8 +229,10 @@ toast_tuple_try_compression(ToastTupleContext *ttc, int attribute)
 	Datum	   *value = &ttc->ttc_values[attribute];
 	Datum		new_value;
 	ToastAttrInfo *attr = &ttc->ttc_attr[attribute];
+	Form_pg_attribute att = TupleDescAttr(ttc->ttc_rel->rd_att, attribute);
+	CompressionInfo cmp = setup_compression_info(attr->tai_compression, att);
 
-	new_value = toast_compress_datum(*value, attr->tai_compression);
+	new_value = toast_compress_datum(*value, cmp);
 
 	if (DatumGetPointer(new_value) != NULL)
 	{
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..1aef65cde99 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,6 +13,9 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#include "varatt.h"
+#include "catalog/pg_attribute.h"
+
 /*
  * GUC support.
  *
@@ -23,24 +26,80 @@
 extern PGDLLIMPORT int default_toast_compression;
 
 /*
- * Built-in compression method ID.  The toast compression header will store
- * this in the first 2 bits of the raw length.  These built-in compression
- * method IDs are directly mapped to the built-in compression methods.
+ * Stub errors if someone tries to query metadata size
+ * for an algorithm that doesn’t support it.
+ */
+static inline uint32
+unsupported_meta_size(const varatt_cmp_extended *hdr)
+{
+	elog(ERROR, "toast_cmpid_meta_size called for unsupported compression algorithm");
+	return 0;					/* unreachable */
+}
+
+/*
+ * TOAST compression methods enumeration.
  *
- * Don't use these values for anything other than understanding the meaning
- * of the raw bits from a varlena; in particular, if the goal is to identify
- * a compression method, use the constants TOAST_PGLZ_COMPRESSION, etc.
- * below. We might someday support more than 4 compression methods, but
- * we can never have more than 4 values in this enum, because there are
- * only 2 bits available in the places where this is stored.
+ * NAME         : algorithm identifier
+ * VALUE        : enum value
+ * META-SIZE-FN	: Calculates algorithm metadata size.
  */
+#define TOAST_COMPRESSION_LIST                  \
+	X(PGLZ,         0, unsupported_meta_size)	\
+	X(LZ4,          1, unsupported_meta_size)  	\
+	X(INVALID,      2, unsupported_meta_size)	/* sentinel */
+
+/* Compression algorithm identifiers */
 typedef enum ToastCompressionId
 {
-	TOAST_PGLZ_COMPRESSION_ID = 0,
-	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+#define X(name,val,fn) TOAST_##name##_COMPRESSION_ID = (val),
+	TOAST_COMPRESSION_LIST
+#undef X
 } ToastCompressionId;
 
+/* lookup table to check if compression method uses extended format */
+static const bool toast_cmpid_extended[] = {
+#define X(name,val,fn)                                                  \
+	/* PGLZ, LZ4 don't use extended format */                  			\
+	[TOAST_##name##_COMPRESSION_ID] =                              		\
+			((val) != TOAST_PGLZ_COMPRESSION_ID &&                      \
+			(val) != TOAST_LZ4_COMPRESSION_ID  &&                       \
+			(val) != TOAST_INVALID_COMPRESSION_ID),
+	TOAST_COMPRESSION_LIST
+#undef X
+};
+
+#define TOAST_CMPID_EXTENDED(alg)  \
+	(toast_cmpid_extended[alg])
+
+/*
+ * Prototype for a per-datum metadata-size callback:
+ *   given a pointer to the extended header, return
+ *   how many metadata bytes follow it.
+ */
+typedef uint32 (*ToastMetaSizeFn) (const varatt_cmp_extended *hdr);
+
+/* Callback table—indexed by ToastCompressionId */
+static const ToastMetaSizeFn toast_meta_size_fns[] = {
+#define X(name,val,fn) [TOAST_##name##_COMPRESSION_ID] = fn,
+	TOAST_COMPRESSION_LIST
+#undef X
+};
+
+/* Calculates algorithm metadata size */
+static inline uint32
+toast_cmpid_meta_size(const varatt_cmp_extended *hdr)
+{
+	Assert(hdr != NULL);
+	return toast_meta_size_fns[hdr->cmp_alg] (hdr);
+}
+
+typedef struct CompressionInfo
+{
+	char		cmethod;
+	varatt_cmp_extended *cmp_ext;	/* non-NULL only if uses extended
+									 * compression methods */
+} CompressionInfo;
+
 /*
  * Built-in compression methods.  pg_attribute will store these in the
  * attcompression column.  In attcompression, InvalidCompressionMethod
@@ -69,5 +128,7 @@ extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
 extern const char *GetCompressionMethodName(char method);
+extern CompressionInfo setup_compression_info(char cmethod, Form_pg_attribute att);
+extern void free_compression_info(CompressionInfo *info);
 
 #endif							/* TOAST_COMPRESSION_H */
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..f4a4829ad17 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -31,21 +31,33 @@ typedef struct toast_compress_header
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
-#define TOAST_COMPRESS_EXTSIZE(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
-
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
-	do { \
-		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
-		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD_INFO(ptr, len, cm_method, cmp_ext)	\
+	do {																				\
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);								\
+		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||								\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID);								\
+		if (!TOAST_CMPID_EXTENDED((cm_method)))											\
+		{																				\
+			((toast_compress_header *)(ptr))->tcinfo =									\
+				((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS);		\
+		}																				\
+		else																			\
+		{																				\
+			/* extended path: mark EXT flag in tcinfo */								\
+			((toast_compress_header *)(ptr))->tcinfo =									\
+				((uint32)(len)) |														\
+				((uint32)(VARATT_4BCE_MASK) << VARLENA_EXTSIZE_BITS);					\
+			Assert((cmp_ext) != NULL);													\
+			/* copy header + algorithm-specific metadata */								\
+			memcpy(																		\
+				VARATT_4BCE_HDR_PTR(ptr),												\
+				(const void *)(cmp_ext), sizeof(varatt_cmp_extended) +					\
+				toast_cmpid_meta_size((const varatt_cmp_extended *)(cmp_ext))			\
+			);																			\
+		}																				\
 	} while (0)
 
-extern Datum toast_compress_datum(Datum value, char cmethod);
+extern Datum toast_compress_datum(Datum value, CompressionInfo cmp);
 extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d4998..91460f313c5 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -328,7 +328,8 @@ typedef struct
 #define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+	( (VARATT_IS_4BCE(PTR)) ? (VARATT_4BCE_CMP_METHOD(PTR)) \
+	: (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS))
 
 /* Same for external Datums; but note argument is a struct varatt_external */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
@@ -340,8 +341,17 @@ typedef struct
 	do { \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
 			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
+		if (!TOAST_CMPID_EXTENDED((cm))) \
+		{ \
+			/* Store the actual method in va_extinfo */ \
+			((toast_pointer).va_extinfo = \
+				(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
+		} \
+		else \
+		{ \
+			/* Store 11 in the top 2 bits, meaning "extended" method. */ 				\
+			(toast_pointer).va_extinfo = (uint32)(len) | (VARATT_4BCE_MASK << VARLENA_EXTSIZE_BITS ); \
+		} \
 	} while (0)
 
 /*
@@ -355,4 +365,63 @@ typedef struct
 	(VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
 	 (toast_pointer).va_rawsize - VARHDRSZ)
 
+/*
+ * varatt_cmp_extended: an optional per‐datum header for extended compression method.
+ * Only used when va_tcinfo’s top two bits are “11”.
+ */
+typedef struct varatt_cmp_extended
+{
+	uint8		cmp_alg;		/* algorithm id (0–255) */
+	char		cmp_meta[FLEXIBLE_ARRAY_MEMBER];	/* algorithm‐specific
+													 * metadata */
+} varatt_cmp_extended;
+
+/*
+ * 1) Detect the extended‐compression flag in va_tcinfo
+ *    (top 2 bits = 0b11 indicate “cmp_ext” path)
+ */
+#define VARATT_4BCE_MASK   0x3
+
+#define VARATT_IS_4BCE(PTR)                                    \
+	((((varattrib_4b *)(PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARATT_4BCE_MASK)
+
+/*
+ * 2) Pointer to varatt_cmp_extended header (just after the 8-byte varattrib_4b compressed headers)
+ */
+#define VARATT_4BCE_HDR_PTR(PTR)				\
+	((varatt_cmp_extended *) ((char *)(PTR) + VARHDRSZ_COMPRESSED))
+
+/* get the algorithm ID */
+#define VARATT_4BCE_CMP_METHOD(PTR)							\
+	((ToastCompressionId)VARATT_4BCE_HDR_PTR(PTR)->cmp_alg)
+
+/* set the algorithm ID */
+#define VARATT_4BCE_SET_HDR(EXT_PTR, alg) ((varatt_cmp_extended*)(EXT_PTR))->cmp_alg = (uint8)(alg);
+
+/*
+ * 3) Helpers to find metadata vs payload:
+ *    – cmp_meta[] pointer
+ *    – compressed‐bytes pointer
+ *    – compressed-bytes size
+ *    – total header size
+ */
+
+/* pointer to compression algorithm's metadata */
+#define VARATT_4BCE_META_PTR(PTR)				\
+	((void *)VARATT_4BCE_HDR_PTR(PTR)->cmp_meta)
+
+/* pointer to compressed bytes (after metadata) */
+#define VARATT_4BCE_PAYLOAD_PTR(PTR)		\
+	((void *) ((char *)VARATT_4BCE_META_PTR(PTR) + toast_cmpid_meta_size(VARATT_4BCE_HDR_PTR(PTR))))
+
+/* number of compressed‐payload bytes */
+#define VARATT_4BCE_PAYLOAD_SIZE(PTR)										\
+	( VARSIZE_4B(PTR) - VARHDRSZ_COMPRESSED	- sizeof(varatt_cmp_extended)	\
+		- toast_cmpid_meta_size(VARATT_4BCE_HDR_PTR(PTR)))
+
+/* total header+meta size before payload */
+#define VARATT_4BCE_HDRSZ(EXT_PTR)						\
+	( VARHDRSZ_COMPRESSED + sizeof(varatt_cmp_extended)	\
+		+ toast_cmpid_meta_size(EXT_PTR))
+
 #endif
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e5879e00dff..ea28675e0c9 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -482,6 +482,7 @@ CompositeIOData
 CompositeTypeStmt
 CompoundAffixFlag
 CompressFileHandle
+CompressionInfo
 CompressionLocation
 CompressorState
 ComputeXidHorizonsResult
@@ -4153,6 +4154,7 @@ uuid_t
 va_list
 vacuumingOptions
 validate_string_relopt
+varatt_cmp_extended
 varatt_expanded
 varattrib_1b
 varattrib_1b_e

base-commit: a675149e87706d01e4007150a0124b89bdef08be
-- 
2.47.1

#33Robert Haas
robertmhaas@gmail.com
In reply to: Nikhil Kumar Veldanda (#32)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Sun, May 4, 2025 at 8:54 AM Nikhil Kumar Veldanda
<veldanda.nikhilkumar17@gmail.com> wrote:

I agree. Each compression algorithm can decide its own metadata size
overhead. Callbacks can provide this information as well rather than
storing in fixed length bytes(3 bytes). The revised patch introduces a
"toast_cmpid_meta_size(const varatt_cmp_extended *hdr)", which
calculates the metadata size.

I don't understand why we need this. I don't see why we need any sort
of generalized concept of metadata at all here. The zstd-dict
compression method needs to store a four-byte OID, so let it do that.
But we don't need to brand that as metadata; and we don't need a
method for other parts of the system to ask how much metadata exists.
At least, I don't think we do.

--
Robert Haas
EDB: http://www.enterprisedb.com

#34Michael Paquier
michael@paquier.xyz
In reply to: Nikhil Kumar Veldanda (#32)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Sun, May 04, 2025 at 05:54:34AM -0700, Nikhil Kumar Veldanda wrote:

3. Resulting on-disk layouts for zstd

ZSTD (nodict) — datum on‑disk layout

+----------------------------------+
| va_header (uint32) |
+----------------------------------+
| va_tcinfo (uint32) | ← top two bits = 11 (extended)
+----------------------------------+
| cmp_alg (uint8) | ← (ZSTD_NODICT)
+----------------------------------+
| compressed bytes … | ← ZSTD frame
+----------------------------------+

This makes sense, yes. You are allocating an extra byte after
va_tcinfo that serves as a redirection if the three bits dedicated to
the compression method are set.

ZSTD(dict) — datum on‑disk layout
+----------------------------------+
| va_header (uint32) |
+----------------------------------+
| va_tcinfo (uint32) | ← top two bits = 11 (extended)
+----------------------------------+
| cmp_alg (uint8) | ← (ZSTD_DICT)
+----------------------------------+
| dict_id (uint32) | ← dictionary OID
+----------------------------------+
| compressed bytes … | ← ZSTD frame
+----------------------------------+

I hope this updated design addresses your concerns. I would appreciate
any further feedback you may have. Thanks again for your guidance—it's
been very helpful.

That makes sense as well structurally if we include a dictionary for
each value. Not sure that we need that much space, for this purpose,
though. We are going to need the extra byte anyway AFAIK, so better
to start with that.

I have been reading 0001 and I'm finding that the integration does not
seem to fit much with the existing varatt_external, making the whole
result slightly confusing. A simple thing: the last bit that we can
use is in varatt_external's va_extinfo, where the patch is using
VARATT_4BCE_MASK to track that we need to go beyond varatt_external to
know what kind of compression information we should use. This is an
important point, and it is not documented around varatt_external which
still assumes that the last bit could be used for a compression
method. With what you are doing in 0001 (or even 0002), this becomes
wrong.

Shouldn't we have a new struct portion in varattrib_4b's union for
this purpose at least (I don't recall that we rely on varattrib_4b's
size which would get larger with this extra byte for the new extended
data with the three bits set for the compression are set in
va_extinfo, correct me if I'm wrong here).
--
Michael

#35Nikita Malakhov
hukutoc@gmail.com
In reply to: Michael Paquier (#34)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi!

Michael, what do you think of this approach (extending varatt_external)
vs extending varatt itself by new tag and structure? The second approach
allows more flexibility, independence of existing structure without
modifying
varatt_4b and is extensible further. I mentioned it above (extending
the TOAST pointer), and it could be implemented more easily and in a less
confusing way.

I'm +1 on storing dictionary somewhere around actual data (not necessary
in the data storage area itself) but strongly against new catalog table
with dictionaries - it involves a lot of side effects, including locks
while working
with this table resulting in performance degradation, and so on.

--

Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/

#36Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Robert Haas (#33)
2 attachment(s)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Robert,

On Mon, May 5, 2025 at 8:07 AM Robert Haas <robertmhaas@gmail.com> wrote:

I don't understand why we need this. I don't see why we need any sort
of generalized concept of metadata at all here. The zstd-dict
compression method needs to store a four-byte OID, so let it do that.
But we don't need to brand that as metadata; and we don't need a
method for other parts of the system to ask how much metadata exists.
At least, I don't think we do.

Thank you for the feedback. My intention in introducing the
toast_cmpid_meta_size helper to centralize header-size computation
across all compression algorithms and to provide generic macros that
can be applied to any extended algorithm methods.

I agree that algorithm-specific metadata details or its sizes need not
be exposed beyond their own routines. Each compression method
inherently knows its layout requirements and should handle them
internally in their routines. I’ve removed the toast_cmpid_meta_size
helper and eliminated the metadata branding.

In the varatt_cmp_extended, the cmp_data field carries the algorithm
payload: for zstd-nodict, it’s a ZSTD frame; for zstd-dict, it’s a
four-byte dictionary OID followed by the ZSTD frame. This approach
ensures the algorithm's framing is fully self-contained in its
routines.

/*
* varatt_cmp_extended: an optional per-datum header for extended
compression method.
* Only used when va_tcinfo’s top two bits are “11”.
*/
typedef struct varatt_cmp_extended
{
uint8 cmp_alg;
char cmp_data[FLEXIBLE_ARRAY_MEMBER];
} varatt_cmp_extended;

I’ve updated patch v21, please review it and let me know if you have
any questions or feedback? Thank you!

v21-0001-varattrib_4b-design-proposal-to-make-it-extended.patch:
varattrib_4b extensibility – adds varatt_cmp_extended, useful macros;
behaviour unchanged.
v21-0002-zstd-nodict-compression.patch: Plain ZSTD (non dict) support
and few basic tests.

--
Nikhil Veldanda

Attachments:

v21-0002-zstd-nodict-compression.patchapplication/octet-stream; name=v21-0002-zstd-nodict-compression.patchDownload
From 5614f0055381c82af803b010af4ffa23901cc9af Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Wed, 7 May 2025 06:43:56 +0000
Subject: [PATCH v21 2/2] zstd nodict compression

---
 contrib/amcheck/verify_heapam.c               |   1 +
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/reloptions.c        |  14 +-
 src/backend/access/common/toast_compression.c | 165 ++++++++++++++-
 src/backend/access/common/toast_internals.c   |   4 +
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/psql/describe.c                       |   5 +-
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/access/toast_compression.h        |  23 +-
 src/include/access/toast_internals.h          |   3 +-
 src/include/utils/attoptcache.h               |   1 +
 src/include/varatt.h                          |   3 +-
 src/test/regress/expected/compression.out     |   5 +-
 src/test/regress/expected/compression_1.out   |   3 +
 .../expected/compression_zstd_nodict.out      | 198 ++++++++++++++++++
 .../expected/compression_zstd_nodict_1.out    | 104 +++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression.sql          |   1 +
 .../regress/sql/compression_zstd_nodict.sql   |  83 ++++++++
 21 files changed, 615 insertions(+), 24 deletions(-)
 create mode 100644 src/test/regress/expected/compression_zstd_nodict.out
 create mode 100644 src/test/regress/expected/compression_zstd_nodict_1.out
 create mode 100644 src/test/regress/sql/compression_zstd_nodict.sql

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index d7c2ac6951a..111bb308341 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1792,6 +1792,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_NODICT_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 8e74ad3569f..3f52aa16c61 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -246,10 +246,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (!VARATT_EXTERNAL_COMPRESS_METHOD_EXTENDED(toast_pointer)
 				&& VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) == TOAST_PGLZ_COMPRESSION_ID)
@@ -485,6 +485,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_NODICT_COMPRESSION_ID:
+			return zstd_nodict_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +530,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_NODICT_COMPRESSION_ID:
+			return zstd_nodict_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 46c1dce222d..1267668a242 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -24,6 +24,7 @@
 #include "access/nbtree.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
+#include "access/toast_compression.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
@@ -381,7 +382,15 @@ static relopt_int intRelOpts[] =
 		},
 		-1, 0, 1024
 	},
-
+	{
+		{
+			"zstd_level",
+			"Set column's ZSTD compression level",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_LEVEL, MIN_ZSTD_LEVEL, MAX_ZSTD_LEVEL
+	},
 	/* list terminator */
 	{{NULL}}
 };
@@ -2097,7 +2106,8 @@ attribute_reloptions(Datum reloptions, bool validate)
 {
 	static const relopt_parse_elt tab[] = {
 		{"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)},
-		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}
+		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)},
+		{"zstd_level", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_level)},
 	};
 
 	return (bytea *) build_reloptions(reloptions, validate,
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index bf6bc505aaf..89644f9d2b4 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,6 +17,10 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
@@ -26,11 +30,19 @@
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
 
-#define NO_LZ4_SUPPORT() \
+#ifdef USE_ZSTD
+#define ZSTD_CHECK_ERROR(zstd_ret, msg) \
+	do { \
+		if (ZSTD_isError(zstd_ret)) \
+			ereport(ERROR, (errmsg("%s: %s", (msg), ZSTD_getErrorName(zstd_ret)))); \
+	} while (0)
+#endif
+
+#define COMPRESSION_METHOD_NOT_SUPPORTED(method) \
 	ereport(ERROR, \
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
-			 errmsg("compression method lz4 not supported"), \
-			 errdetail("This functionality requires the server to be built with lz4 support.")))
+			 errmsg("compression method %s not supported", method), \
+			 errdetail("This functionality requires the server to be built with %s support.", method)))
 
 /*
  * Compress a varlena using PGLZ.
@@ -140,7 +152,7 @@ struct varlena *
 lz4_compress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		valsize;
@@ -183,7 +195,7 @@ struct varlena *
 lz4_decompress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -216,7 +228,7 @@ struct varlena *
 lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -246,6 +258,127 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 #endif
 }
 
+/* Compress datum using ZSTD with optional dictionary (using cdict) */
+struct varlena *
+zstd_nodict_compress_datum(const struct varlena *value, CompressionInfo cmp)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	size_t		cmp_size;
+
+	/* Allocate space for the compressed varlena (header + data) */
+	compressed = (struct varlena *) palloc(max_size + VARHDRSZ_4BCE);
+
+	cmp_size = ZSTD_compress(VARDATA_4BCE(compressed),
+							 max_size,
+							 VARDATA_ANY(value),
+							 valsize,
+							 cmp.zstd_level);
+
+	if (ZSTD_isError(cmp_size))
+	{
+		pfree(compressed);
+		ZSTD_CHECK_ERROR(cmp_size, "ZSTD compression failed");
+	}
+
+	/*
+	 * If compression did not reduce size, return NULL so that the
+	 * uncompressed data is stored
+	 */
+	if (cmp_size > valsize)
+	{
+		pfree(compressed);
+		return NULL;
+	}
+
+	/* Set the compressed size in the varlena header */
+	SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARHDRSZ_4BCE);
+	return compressed;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+	return NULL;
+#endif
+}
+
+/* Decompression routine */
+struct varlena *
+zstd_nodict_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		zstd_compressed_len = VARSIZE_ANY(value) - VARHDRSZ_4BCE;
+	struct varlena *result;
+	size_t		uncmp_size;
+
+	/* Allocate space for the uncompressed data */
+	result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+	uncmp_size = ZSTD_decompress(VARDATA(result),
+								 actual_size_exhdr,
+								 VARDATA_4BCE(value),
+								 zstd_compressed_len);
+
+	if (ZSTD_isError(uncmp_size))
+	{
+		pfree(result);
+		ZSTD_CHECK_ERROR(uncmp_size, "ZSTD decompression failed");
+	}
+
+	/* Set final size in the varlena header */
+	SET_VARSIZE(result, uncmp_size + VARHDRSZ);
+	return result;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum using the streaming API and optional dictionary */
+struct varlena *
+zstd_nodict_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	size_t		ret;
+	ZSTD_DCtx  *ZstdDecompressionCtx = ZSTD_createDCtx();
+
+	inBuf.src = VARDATA_4BCE(value);
+	inBuf.size = VARSIZE_ANY(value) - VARHDRSZ_4BCE;
+	inBuf.pos = 0;
+
+	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+	outBuf.dst = (char *) result + VARHDRSZ;
+	outBuf.size = slicelength;
+	outBuf.pos = 0;
+
+	/* Common decompression loop */
+	while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+	{
+		ret = ZSTD_decompressStream(ZstdDecompressionCtx, &outBuf, &inBuf);
+		if (ZSTD_isError(ret))
+		{
+			pfree(result);
+			ZSTD_freeDCtx(ZstdDecompressionCtx);
+			ZSTD_CHECK_ERROR(ret, "zstd decompression failed");
+		}
+	}
+
+	Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+	SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+	ZSTD_freeDCtx(ZstdDecompressionCtx);
+	return result;
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+	return NULL;
+#endif
+}
+
 /*
  * Extract compression ID from a varlena.
  *
@@ -293,10 +426,17 @@ CompressionNameToMethod(const char *compression)
 	else if (strcmp(compression, "lz4") == 0)
 	{
 #ifndef USE_LZ4
-		NO_LZ4_SUPPORT();
+		COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd_nodict") == 0)
+	{
+#ifndef USE_ZSTD
+		COMPRESSION_METHOD_NOT_SUPPORTED("zstd_nodict");
+#endif
+		return TOAST_ZSTD_NODICT_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -313,6 +453,8 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_NODICT_COMPRESSION:
+			return "zstd_nodict";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
@@ -326,6 +468,7 @@ setup_cmp_info(char cmethod, Form_pg_attribute att)
 
 	/* initialize from the attribute’s default settings */
 	info.cmethod = cmethod;
+	info.zstd_level = DEFAULT_ZSTD_LEVEL;
 
 	/* If the compression method is not valid, use the current default */
 	if (!CompressionMethodIsValid(cmethod))
@@ -336,6 +479,14 @@ setup_cmp_info(char cmethod, Form_pg_attribute att)
 		case TOAST_PGLZ_COMPRESSION:
 		case TOAST_LZ4_COMPRESSION:
 			break;
+		case TOAST_ZSTD_NODICT_COMPRESSION:
+			{
+				AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+				if (aopt != NULL)
+					info.zstd_level = aopt->zstd_level;
+			}
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", info.cmethod);
 	}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 29a0fb81211..ba27d4363fa 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -67,6 +67,10 @@ toast_compress_datum(Datum value, CompressionInfo cmp)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_NODICT_COMPRESSION:
+			tmp = zstd_nodict_compress_datum((const struct varlena *) value, cmp);
+			cmid = TOAST_ZSTD_NODICT_COMPRESSION_ID;
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 3e4d5568bde..5b9151c7e16 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5301,6 +5301,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_NODICT_COMPRESSION_ID:
+			result = "zstd_nodict";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 2f8cbd86759..948454e2093 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd_nodict", TOAST_ZSTD_NODICT_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 34826d01380..f2d2ca39514 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -756,7 +756,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd_nodict'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1d08268393e..3831a7fab03 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2171,8 +2171,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'n' ? "zstd_nodict" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index c916b9299a8..2441acf41ce 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2875,11 +2875,11 @@ match_previous_words(int pattern_id,
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET ( */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "(") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "("))
-		COMPLETE_WITH("n_distinct", "n_distinct_inherited");
+		COMPLETE_WITH("n_distinct", "n_distinct_inherited", "zstd_level");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION"))
-		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4");
+		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4", "ZSTD_NODICT");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET EXPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "EXPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "EXPRESSION"))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 379197452c4..18657c02e53 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -16,6 +16,10 @@
 #include "varatt.h"
 #include "catalog/pg_attribute.h"
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 /*
  * GUC support.
  *
@@ -29,7 +33,8 @@ typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_ZSTD_NODICT_COMPRESSION_ID = 2,
+	TOAST_INVALID_COMPRESSION_ID = 3,
 } ToastCompressionId;
 
 /*
@@ -54,6 +59,7 @@ toast_cmpid_extended(ToastCompressionId cmpid)
 typedef struct CompressionInfo
 {
 	char		cmethod;
+	int			zstd_level;
 } CompressionInfo;
 
 /*
@@ -63,10 +69,20 @@ typedef struct CompressionInfo
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_NODICT_COMPRESSION	'n'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
 
+#ifdef USE_ZSTD
+#define DEFAULT_ZSTD_LEVEL					ZSTD_CLEVEL_DEFAULT
+#define MIN_ZSTD_LEVEL						(int)-ZSTD_BLOCKSIZE_MAX
+#define MAX_ZSTD_LEVEL						22
+#else
+#define DEFAULT_ZSTD_LEVEL					0
+#define MIN_ZSTD_LEVEL						0
+#define MAX_ZSTD_LEVEL						0
+#endif
 
 /* pglz compression/decompression routines */
 extern struct varlena *pglz_compress_datum(const struct varlena *value);
@@ -80,6 +96,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd nodict compression/decompression routines */
+extern struct varlena *zstd_nodict_compress_datum(const struct varlena *value, CompressionInfo cmp);
+extern struct varlena *zstd_nodict_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_nodict_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 19ee84e352b..3c5654e84ed 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -35,7 +35,8 @@ typedef struct toast_compress_header
 	do {																				\
 		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);								\
 		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||								\
-				(cm_method) == TOAST_LZ4_COMPRESSION_ID);								\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID	||								\
+				(cm_method) == TOAST_ZSTD_NODICT_COMPRESSION_ID);						\
 		if (!TOAST_CMPID_EXTENDED((cm_method)))											\
 		{																				\
 			((toast_compress_header *)(ptr))->tcinfo =									\
diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h
index f684a772af5..51d65ebd646 100644
--- a/src/include/utils/attoptcache.h
+++ b/src/include/utils/attoptcache.h
@@ -21,6 +21,7 @@ typedef struct AttributeOpts
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	float8		n_distinct;
 	float8		n_distinct_inherited;
+	int			zstd_level;
 } AttributeOpts;
 
 extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index c7da820d55f..f259405f19a 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -342,7 +342,8 @@ typedef struct
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
 	do { \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-				(cm) == TOAST_LZ4_COMPRESSION_ID); \
+				(cm) == TOAST_LZ4_COMPRESSION_ID	||	\
+				(cm) == TOAST_ZSTD_NODICT_COMPRESSION_ID); \
 		if (!TOAST_CMPID_EXTENDED((cm))) \
 		{ \
 			/* Store the actual method in va_extinfo */ \
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 4dd9ee7200d..c7e108a0f52 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -238,10 +238,11 @@ NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd_nodict.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd_nodict.
+SET default_toast_compression = 'zstd_nodict';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 -- test alter compression method
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out
index 7bd7642b4b9..5b10d8c5259 100644
--- a/src/test/regress/expected/compression_1.out
+++ b/src/test/regress/expected/compression_1.out
@@ -233,6 +233,9 @@ HINT:  Available values: pglz.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
 HINT:  Available values: pglz.
+SET default_toast_compression = 'zstd_nodict';
+ERROR:  invalid value for parameter "default_toast_compression": "zstd_nodict"
+HINT:  Available values: pglz.
 SET default_toast_compression = 'lz4';
 ERROR:  invalid value for parameter "default_toast_compression": "lz4"
 HINT:  Available values: pglz.
diff --git a/src/test/regress/expected/compression_zstd_nodict.out b/src/test/regress/expected/compression_zstd_nodict.out
new file mode 100644
index 00000000000..0714a533be5
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_nodict.out
@@ -0,0 +1,198 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE;
+NOTICE:  table "cmdata_zstd_nodict" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict (
+    f1 TEXT COMPRESSION zstd_nodict
+);
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)); -- inline
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1000000)); -- externally stored
+  END LOOP;
+END $$;
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd_nodict;
+                                  Table "public.cmdata_zstd_nodict"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd_nodict |              | 
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd_nodict
+GROUP BY pg_column_compression(f1);
+ compression_method | row_count 
+--------------------+-----------
+ zstd_nodict        |        30
+(1 row)
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd_nodict;
+                     data_slice                     
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+(30 rows)
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict_2;
+NOTICE:  table "cmdata_zstd_nodict_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_nodict_2;
+                                 Table "public.cmdata_zstd_nodict_2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd_nodict |              | 
+
+DROP TABLE cmdata_zstd_nodict_2;
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict;
+NOTICE:  materialized view "compressmv_zstd_nodict" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS
+  SELECT f1 FROM cmdata_zstd_nodict;
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd_nodict;
+                          Materialized view "public.compressmv_zstd_nodict"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1
+   FROM cmdata_zstd_nodict;
+
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd_nodict;
+ mv_compression 
+----------------
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+ zstd_nodict
+(30 rows)
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd_nodict
+SET f1 = f1 || ' UPDATED';
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd_nodict;
+ preview 
+---------
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+(30 rows)
+
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd_nodict;
+DROP TABLE cmdata_zstd_nodict;
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_zstd_nodict_1.out b/src/test/regress/expected/compression_zstd_nodict_1.out
new file mode 100644
index 00000000000..62ea8876fa1
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_nodict_1.out
@@ -0,0 +1,104 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE;
+NOTICE:  table "cmdata_zstd_nodict" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict (
+    f1 TEXT COMPRESSION zstd_nodict
+);
+ERROR:  compression method zstd_nodict not supported
+DETAIL:  This functionality requires the server to be built with zstd_nodict support.
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)); -- inline
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1000000)); -- externally stored
+  END LOOP;
+END $$;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 1: INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('12345678...
+                    ^
+QUERY:  INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004))
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd_nodict;
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd_nodict
+GROUP BY pg_column_compression(f1);
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 3: FROM cmdata_zstd_nodict
+             ^
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd_nodict;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 2: FROM cmdata_zstd_nodict;
+             ^
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict_2;
+NOTICE:  table "cmdata_zstd_nodict_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION);
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 1: CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict I...
+                                                ^
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_nodict_2;
+DROP TABLE cmdata_zstd_nodict_2;
+ERROR:  table "cmdata_zstd_nodict_2" does not exist
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict;
+NOTICE:  materialized view "compressmv_zstd_nodict" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS
+  SELECT f1 FROM cmdata_zstd_nodict;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 2:   SELECT f1 FROM cmdata_zstd_nodict;
+                         ^
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd_nodict;
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd_nodict;
+ERROR:  relation "compressmv_zstd_nodict" does not exist
+LINE 2: FROM compressmv_zstd_nodict;
+             ^
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd_nodict
+SET f1 = f1 || ' UPDATED';
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 1: UPDATE cmdata_zstd_nodict
+               ^
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd_nodict;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 2: FROM cmdata_zstd_nodict;
+             ^
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd_nodict;
+ERROR:  materialized view "compressmv_zstd_nodict" does not exist
+DROP TABLE cmdata_zstd_nodict;
+ERROR:  table "cmdata_zstd_nodict" does not exist
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a424be2a6bf..75cea22c418 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_zstd_nodict memoize stats predicate numa
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index 490595fcfb2..27979eb7997 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -102,6 +102,7 @@ CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 SET default_toast_compression = 'I do not exist compression';
+SET default_toast_compression = 'zstd_nodict';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 
diff --git a/src/test/regress/sql/compression_zstd_nodict.sql b/src/test/regress/sql/compression_zstd_nodict.sql
new file mode 100644
index 00000000000..fd3918d68ef
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd_nodict.sql
@@ -0,0 +1,83 @@
+\set HIDE_TOAST_COMPRESSION false
+
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE;
+CREATE TABLE cmdata_zstd_nodict (
+    f1 TEXT COMPRESSION zstd_nodict
+);
+
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)); -- inline
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1000000)); -- externally stored
+  END LOOP;
+END $$;
+
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd_nodict;
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd_nodict
+GROUP BY pg_column_compression(f1);
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd_nodict;
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict_2;
+CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_nodict_2;
+DROP TABLE cmdata_zstd_nodict_2;
+
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict;
+CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS
+  SELECT f1 FROM cmdata_zstd_nodict;
+
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd_nodict;
+
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd_nodict;
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd_nodict
+SET f1 = f1 || ' UPDATED';
+
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd_nodict;
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd_nodict;
+DROP TABLE cmdata_zstd_nodict;
+
+\set HIDE_TOAST_COMPRESSION true
-- 
2.47.1

v21-0001-varattrib_4b-design-proposal-to-make-it-extended.patchapplication/octet-stream; name=v21-0001-varattrib_4b-design-proposal-to-make-it-extended.patchDownload
From 57f738651bf021544bd0709ee60ca0454ef1845f Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <nikhilkv@amazon.com>
Date: Wed, 7 May 2025 06:43:25 +0000
Subject: [PATCH v21 1/2] varattrib_4b design proposal to make it extended to
 support multiple compression algorithms.

---
 contrib/amcheck/verify_heapam.c               |  2 +-
 src/backend/access/brin/brin_tuple.c          |  4 +-
 src/backend/access/common/detoast.c           | 10 ++--
 src/backend/access/common/indextuple.c        |  5 +-
 src/backend/access/common/toast_compression.c | 30 +++++++++++-
 src/backend/access/common/toast_internals.c   | 10 ++--
 src/backend/access/table/toast_helper.c       |  4 +-
 src/include/access/toast_compression.h        | 40 +++++++++++-----
 src/include/access/toast_internals.h          | 31 +++++++-----
 src/include/varatt.h                          | 48 +++++++++++++++++--
 src/tools/pgindent/typedefs.list              |  2 +
 11 files changed, 139 insertions(+), 47 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index aa9cccd1da4..d7c2ac6951a 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1786,7 +1786,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = toast_get_compression_id(attr);
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..eb19739da03 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -223,6 +223,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			{
 				Datum		cvalue;
 				char		compression;
+				CompressionInfo cmp;
 				Form_pg_attribute att = TupleDescAttr(brdesc->bd_tupdesc,
 													  keyno);
 
@@ -237,7 +238,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 				else
 					compression = InvalidCompressionMethod;
 
-				cvalue = toast_compress_datum(value, compression);
+				cmp = setup_cmp_info(compression, att);
+				cvalue = toast_compress_datum(value, cmp);
 
 				if (DatumGetPointer(cvalue) != NULL)
 				{
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..8e74ad3569f 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -251,8 +251,8 @@ detoast_attr_slice(struct varlena *attr,
 			 * determine how much compressed data we need to be sure of being
 			 * able to decompress the required slice.
 			 */
-			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
-				TOAST_PGLZ_COMPRESSION_ID)
+			if (!VARATT_EXTERNAL_COMPRESS_METHOD_EXTENDED(toast_pointer)
+				&& VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) == TOAST_PGLZ_COMPRESSION_ID)
 				max_size = pglz_maximum_compressed_size(slicelimit, max_size);
 
 			/*
@@ -478,7 +478,7 @@ toast_decompress_datum(struct varlena *attr)
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
@@ -514,14 +514,14 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 	 * have been seen to give wrong results if passed an output size that is
 	 * more than the data's true decompressed size.
 	 */
-	if ((uint32) slicelength >= TOAST_COMPRESS_EXTSIZE(attr))
+	if ((uint32) slicelength >= VARDATA_COMPRESSED_GET_EXTSIZE(attr))
 		return toast_decompress_datum(attr);
 
 	/*
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data slice using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 1986b943a28..1fe0e4288cc 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -123,9 +123,10 @@ index_form_tuple_context(TupleDesc tupleDescriptor,
 			 att->attstorage == TYPSTORAGE_MAIN))
 		{
 			Datum		cvalue;
+			CompressionInfo cmp;
 
-			cvalue = toast_compress_datum(untoasted_values[i],
-										  att->attcompression);
+			cmp = setup_cmp_info(att->attcompression, att);
+			cvalue = toast_compress_datum(untoasted_values[i], cmp);
 
 			if (DatumGetPointer(cvalue) != NULL)
 			{
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97e..bf6bc505aaf 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -21,6 +21,7 @@
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
+#include "utils/attoptcache.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
@@ -266,7 +267,10 @@ toast_get_compression_id(struct varlena *attr)
 
 		VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
 
-		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
+		if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)
+			&& VARATT_EXTERNAL_COMPRESS_METHOD_EXTENDED(toast_pointer))
+			cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(detoast_external_attr(attr));
+		else
 			cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 	}
 	else if (VARATT_IS_COMPRESSED(attr))
@@ -314,3 +318,27 @@ GetCompressionMethodName(char method)
 			return NULL;		/* keep compiler quiet */
 	}
 }
+
+CompressionInfo
+setup_cmp_info(char cmethod, Form_pg_attribute att)
+{
+	CompressionInfo info;
+
+	/* initialize from the attribute’s default settings */
+	info.cmethod = cmethod;
+
+	/* If the compression method is not valid, use the current default */
+	if (!CompressionMethodIsValid(cmethod))
+		info.cmethod = default_toast_compression;
+
+	switch (info.cmethod)
+	{
+		case TOAST_PGLZ_COMPRESSION:
+		case TOAST_LZ4_COMPRESSION:
+			break;
+		default:
+			elog(ERROR, "invalid compression method %c", info.cmethod);
+	}
+
+	return info;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346ce..29a0fb81211 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -43,7 +43,7 @@ static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
  * ----------
  */
 Datum
-toast_compress_datum(Datum value, char cmethod)
+toast_compress_datum(Datum value, CompressionInfo cmp)
 {
 	struct varlena *tmp = NULL;
 	int32		valsize;
@@ -54,14 +54,10 @@ toast_compress_datum(Datum value, char cmethod)
 
 	valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(value));
 
-	/* If the compression method is not valid, use the current default */
-	if (!CompressionMethodIsValid(cmethod))
-		cmethod = default_toast_compression;
-
 	/*
 	 * Call appropriate compression routine for the compression method.
 	 */
-	switch (cmethod)
+	switch (cmp.cmethod)
 	{
 		case TOAST_PGLZ_COMPRESSION:
 			tmp = pglz_compress_datum((const struct varlena *) value);
@@ -72,7 +68,7 @@ toast_compress_datum(Datum value, char cmethod)
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
 		default:
-			elog(ERROR, "invalid compression method %c", cmethod);
+			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
 
 	if (tmp == NULL)
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d2..6a554167636 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -229,8 +229,10 @@ toast_tuple_try_compression(ToastTupleContext *ttc, int attribute)
 	Datum	   *value = &ttc->ttc_values[attribute];
 	Datum		new_value;
 	ToastAttrInfo *attr = &ttc->ttc_attr[attribute];
+	Form_pg_attribute att = TupleDescAttr(ttc->ttc_rel->rd_att, attribute);
+	CompressionInfo cmp = setup_cmp_info(attr->tai_compression, att);
 
-	new_value = toast_compress_datum(*value, attr->tai_compression);
+	new_value = toast_compress_datum(*value, cmp);
 
 	if (DatumGetPointer(new_value) != NULL)
 	{
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..379197452c4 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,6 +13,9 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#include "varatt.h"
+#include "catalog/pg_attribute.h"
+
 /*
  * GUC support.
  *
@@ -22,18 +25,6 @@
  */
 extern PGDLLIMPORT int default_toast_compression;
 
-/*
- * Built-in compression method ID.  The toast compression header will store
- * this in the first 2 bits of the raw length.  These built-in compression
- * method IDs are directly mapped to the built-in compression methods.
- *
- * Don't use these values for anything other than understanding the meaning
- * of the raw bits from a varlena; in particular, if the goal is to identify
- * a compression method, use the constants TOAST_PGLZ_COMPRESSION, etc.
- * below. We might someday support more than 4 compression methods, but
- * we can never have more than 4 values in this enum, because there are
- * only 2 bits available in the places where this is stored.
- */
 typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
@@ -41,6 +32,30 @@ typedef enum ToastCompressionId
 	TOAST_INVALID_COMPRESSION_ID = 2,
 } ToastCompressionId;
 
+/*
+ * toast_cmpid_extended
+ *
+ * Returns true if the given compression ID uses the extended on-disk format.
+ */
+static inline bool
+toast_cmpid_extended(ToastCompressionId cmpid)
+{
+	/*
+	 * only PGLZ, LZ4 are not extended; everything else uses extended on-disk
+	 * format.
+	 */
+	return !(cmpid == TOAST_PGLZ_COMPRESSION_ID ||
+			 cmpid == TOAST_LZ4_COMPRESSION_ID ||
+			 cmpid == TOAST_INVALID_COMPRESSION_ID);
+}
+
+#define TOAST_CMPID_EXTENDED(alg)	(toast_cmpid_extended(alg))
+
+typedef struct CompressionInfo
+{
+	char		cmethod;
+} CompressionInfo;
+
 /*
  * Built-in compression methods.  pg_attribute will store these in the
  * attcompression column.  In attcompression, InvalidCompressionMethod
@@ -69,5 +84,6 @@ extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
 extern const char *GetCompressionMethodName(char method);
+extern CompressionInfo setup_cmp_info(char cmethod, Form_pg_attribute att);
 
 #endif							/* TOAST_COMPRESSION_H */
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..19ee84e352b 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -31,21 +31,26 @@ typedef struct toast_compress_header
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
-#define TOAST_COMPRESS_EXTSIZE(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
-
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
-	do { \
-		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
-		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method)				\
+	do {																				\
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);								\
+		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||								\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID);								\
+		if (!TOAST_CMPID_EXTENDED((cm_method)))											\
+		{																				\
+			((toast_compress_header *)(ptr))->tcinfo =									\
+				((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS);		\
+		}																				\
+		else																			\
+		{																				\
+			/* extended path: mark EXT flag in tcinfo */								\
+			((toast_compress_header *)(ptr))->tcinfo =									\
+				((uint32)(len)) | ((uint32)(VARATT_4BCE_MASK) << VARLENA_EXTSIZE_BITS);	\
+			VARATT_4BCE_CMP_METHOD(ptr) = (cm_method);									\
+		}																				\
 	} while (0)
 
-extern Datum toast_compress_datum(Datum value, char cmethod);
+extern Datum toast_compress_datum(Datum value, CompressionInfo cmp);
 extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d4998..c7da820d55f 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -328,20 +328,32 @@ typedef struct
 #define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+	( (VARATT_IS_4BCE(PTR)) ? (VARATT_4BCE_CMP_METHOD(PTR)) \
+	: (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS))
 
 /* Same for external Datums; but note argument is a struct varatt_external */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
 #define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
 	((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS)
+#define VARATT_EXTERNAL_COMPRESS_METHOD_EXTENDED(toast_pointer)	\
+	(((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS) == VARATT_4BCE_MASK)
 
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
 	do { \
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
+				(cm) == TOAST_LZ4_COMPRESSION_ID); \
+		if (!TOAST_CMPID_EXTENDED((cm))) \
+		{ \
+			/* Store the actual method in va_extinfo */ \
+			((toast_pointer).va_extinfo = \
+				(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
+		} \
+		else \
+		{ \
+			/* Store 11 in the top 2 bits, meaning "extended" method. */ 				\
+			(toast_pointer).va_extinfo = (uint32)(len) | (VARATT_4BCE_MASK << VARLENA_EXTSIZE_BITS); \
+		} \
 	} while (0)
 
 /*
@@ -355,4 +367,32 @@ typedef struct
 	(VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
 	 (toast_pointer).va_rawsize - VARHDRSZ)
 
+/*
+ * varatt_cmp_extended: an optional per‐datum header for extended compression method.
+ * Only used when va_tcinfo’s top two bits are “11”.
+ */
+typedef struct varatt_cmp_extended
+{
+	uint8		cmp_alg;		/* algorithm id (0–255) */
+	char		cmp_data[FLEXIBLE_ARRAY_MEMBER];
+} varatt_cmp_extended;
+
+/*
+ * Detect the extended‐compression flag in va_tcinfo
+ *  (top 2 bits = 0b11 indicate “cmp_ext” path)
+ */
+#define VARATT_4BCE_MASK   0x3
+
+#define VARATT_IS_4BCE(PTR)	\
+	((((varattrib_4b *)(PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) == VARATT_4BCE_MASK)
+
+#define VARDATA_4BCE(PTR)	\
+	((varatt_cmp_extended *) VARDATA_4B_C(PTR))->cmp_data
+
+/* get the algorithm ID */
+#define VARATT_4BCE_CMP_METHOD(PTR)                          \
+	(((varatt_cmp_extended *) VARDATA_4B_C(PTR))->cmp_alg)
+
+#define VARHDRSZ_4BCE	(VARHDRSZ_COMPRESSED + offsetof(varatt_cmp_extended, cmp_data))
+
 #endif
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9ea573fae21..0c16efd96cc 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -483,6 +483,7 @@ CompositeIOData
 CompositeTypeStmt
 CompoundAffixFlag
 CompressFileHandle
+CompressionInfo
 CompressionLocation
 CompressorState
 ComputeXidHorizonsResult
@@ -4154,6 +4155,7 @@ uuid_t
 va_list
 vacuumingOptions
 validate_string_relopt
+varatt_cmp_extended
 varatt_expanded
 varattrib_1b
 varattrib_1b_e

base-commit: ab42d643c14509cf1345588f55d798284b11a91e
-- 
2.47.1

#37Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#34)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Michael, Thanks for the feedback.

On Wed, May 7, 2025 at 12:49 AM Michael Paquier <michael@paquier.xyz> wrote:

I have been reading 0001 and I'm finding that the integration does not
seem to fit much with the existing varatt_external, making the whole
result slightly confusing. A simple thing: the last bit that we can
use is in varatt_external's va_extinfo, where the patch is using
VARATT_4BCE_MASK to track that we need to go beyond varatt_external to
know what kind of compression information we should use. This is an
important point, and it is not documented around varatt_external which
still assumes that the last bit could be used for a compression
method. With what you are doing in 0001 (or even 0002), this becomes
wrong.

This is the current logic used in patch for varatt_external.

When a datum is compressed with an extended algorithm and must live in
external storage, we set the top two bits of
va_extinfo(varatt_external) to 0b11.

To figure out the compression method for an external TOAST datum:

1. Inspect the top two bits of va_extinfo.
2. If they equal 0b11(VARATT_4BCE_MASK), call
toast_get_compression_id, which invokes detoast_external_attr to fetch
the datum in its 4-byte varattrib form (no decompression) and then
reads its compression header to find the compression method.
3. Otherwise, fall back to the existing
VARATT_EXTERNAL_GET_COMPRESS_METHOD path to get the compression
method.

We use this macro VARATT_EXTERNAL_COMPRESS_METHOD_EXTENDED to
determine if the compression method is extended or not.

Across the entire codebase, external TOAST‐pointer compression methods
are only inspected in the following functions:
1. pg_column_compression
2. check_tuple_attribute (verify_heapam pg function)
3. detoast_attr_slice (just to check pglz or not)

Could you please help me understand what’s incorrect about this approach?

Shouldn't we have a new struct portion in varattrib_4b's union for
this purpose at least (I don't recall that we rely on varattrib_4b's
size which would get larger with this extra byte for the new extended
data with the three bits set for the compression are set in
va_extinfo, correct me if I'm wrong here).
--

In patch v21, va_compressed.va_data points to varatt_cmp_extended, so
adding it isn’t strictly necessary. If we do want to fold it into the
varattrib_4b union, we could define it like this:

```
typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;
struct /* Compressed-in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method; see va_extinfo */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
} va_compressed;
struct
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method; see va_extinfo */
uint8 cmp_alg;
char cmp_data[FLEXIBLE_ARRAY_MEMBER];
} varatt_cmp_extended;
} varattrib_4b;
```
we don't depend on varattrib_4b size anywhere.

--
Nikhil Veldanda

#38Michael Paquier
michael@paquier.xyz
In reply to: Nikhil Kumar Veldanda (#37)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Wed, May 07, 2025 at 04:39:17PM -0700, Nikhil Kumar Veldanda wrote:

In patch v21, va_compressed.va_data points to varatt_cmp_extended, so
adding it isn’t strictly necessary. If we do want to fold it into the
varattrib_4b union, we could define it like this:

```
typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;
struct /* Compressed-in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method; see va_extinfo */
char va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
} va_compressed;
struct
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size (excludes header) and
* compression method; see va_extinfo */
uint8 cmp_alg;
char cmp_data[FLEXIBLE_ARRAY_MEMBER];
} varatt_cmp_extended;
} varattrib_4b;
```
we don't depend on varattrib_4b size anywhere.

Yes, I was wondering if this is not the most natural approach in terms
of structure once if we plug an extra byte into the varlena header if
all the bits of va_extinfo for the compression information are used.
Having all the bits may not mean that this necessarily means that the
information would be cmp_data all the time, just that this a natural
option when plugging in a new compression method in the new byte
available.

FWIW, I've tested this exact change yesterday, wondering if we depend
on sizeof(varattrib_4b) after looking at the code and getting the
impression that we don't even for some the in-memory comparisons, and
noted two things:
- check-world was OK.
- a pg_upgrade'd instance with a regression database seems kind of
OK, but I've not done that much in-depth checking on this side so I
have less confidence about that.
--
Michael

#39Michael Paquier
michael@paquier.xyz
In reply to: Nikita Malakhov (#35)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Wed, May 07, 2025 at 11:40:14AM +0300, Nikita Malakhov wrote:

Michael, what do you think of this approach (extending varatt_external)
vs extending varatt itself by new tag and structure?

I'm reserved on that. What I'm afraid here is more complications in
the backend code because we have quite a few places where we do
varatt lookups to decide what should be happen, like in PLs, so this
brings complications of its own for something that could be isolated
behind a varattrib_4b, where detoasting is under control. The patch
posted at [1]/messages/by-id/CAN-LCVNxbnpHh4PVUUc9g6dPibE8wZALiLtxcs3TjfivxDkCkA@mail.gmail.com -- Michael means that the custom area could be anything, how do you
make sure that the backend is able to understand what could be
anything? I guess that this also depends on the pluggable toast part,
of course, but I've not studied enough what's been proposed to have a
hard opinion. If you have very specific pointers, please feel free.

The second approach
allows more flexibility, independence of existing structure without
modifying
varatt_4b and is extensible further. I mentioned it above (extending
the TOAST pointer), and it could be implemented more easily and in a less
confusing way.

If you mean [0]/messages/by-id/CAN-LCVMq2X=fhx7KLxfeDyb3P+BXuCkHC0g=9GF+JD4izfVa0Q@mail.gmail.com, putting an "extended" flag into ToastCompressionId
which is something used now by the internals of TOAST for a
compression method, with ToastCompressionId being limited to have up
to 4 elements in its enum, does not feel right. In concept, once
extended, this may point to something more than a compression method,
as there's also metadata around the compression method added. At
least that's what I'm understanding as a possible scenario from all
the proposals in this area. There's some overlap with
common/compression.h, for example, even if we are never going to care
about gzip in this case, just saying that this has been buzzing me in
the core code for some time.

One first thing I'd try to do here is to untangle this situation, by
allowing ToastCompressionId to have more extensibility so as we could
use it to track more compression methods, or just perhaps remove it
entirely in a smart way by keeping the information related to the
extra byte and the two bits of va_tcinfo for the compression method
isolated in varatt.h, shaping the code so as adding more compression
methods in the extra byte put after va_tcinfo would be easier once the
surroundings of varattrib_4b are extended. Without an agreement about
how to use the last bit we have, there's perhaps little point in
aiming for any of that now.

FWIW, extending the area around varattrib_4b feels a natural thing to
do here, and it does not have to overlap with the possibilities around
the varatts.

I'm +1 on storing dictionary somewhere around actual data (not necessary
in the data storage area itself) but strongly against new catalog table
with dictionaries - it involves a lot of side effects, including locks
while working
with this table resulting in performance degradation, and so on.

Just wondering. Have you looked at the potential overhead of doing
computation and decomputation of a dictionnary? zstd mentions in its
docs that these can easily cause a lot of overhead, hence handling
this stuff without some kind of caching is going to be costly if
performing a lot of chunk decompressions. It's something that could
be decided later on, of course. If this area of the code is made
pluggable, then it's up to an extension to just do it.

[0]: /messages/by-id/CAN-LCVMq2X=fhx7KLxfeDyb3P+BXuCkHC0g=9GF+JD4izfVa0Q@mail.gmail.com
[1]: /messages/by-id/CAN-LCVNxbnpHh4PVUUc9g6dPibE8wZALiLtxcs3TjfivxDkCkA@mail.gmail.com -- Michael
--
Michael

#40Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#38)
2 attachment(s)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Wed, May 7, 2025 at 5:38 PM Michael Paquier <michael@paquier.xyz> wrote:

Yes, I was wondering if this is not the most natural approach in terms
of structure once if we plug an extra byte into the varlena header if
all the bits of va_extinfo for the compression information are used.
Having all the bits may not mean that this necessarily means that the
information would be cmp_data all the time, just that this a natural
option when plugging in a new compression method in the new byte
available.

Thanks for reviewing and providing feedback on the patch. Regarding
questions about varatt_external—specifically, storing compression
methods in one byte for extended compression methods for external
ondisk datum here’s the proposal for varatt_external. We check for
compression methods for external ondisk datum in 3 trivial places in
core, my previous proposal just mark 0b11 in the top bits of
va_extinfo and fetch externally stored chunks and form varattrib_4b to
find the compression method id for extended compression methods.
However, I understand why embedding the method byte directly is
clearer.

```
typedef struct varatt_external
{
int32 va_rawsize; /* Original data size (includes header) */
uint32 va_extinfo; /* External size (without header) and
* compression method */
Oid va_valueid; /* Unique ID within TOAST table */
Oid va_toastrelid; /* OID of TOAST table containing it */
/* -------- optional trailer -------- */
union
{
struct /* compression-method trailer */
{
uint8 va_ecinfo; /* Extended-compression-method info */
} cmp;
} extended; /* “extended” = optional byte */
} varatt_external;
```

I'm proposing not to store algorithm metadata exclusively at
varatt_external level because storing metadata within varatt_external
is not always appropriate because in scenarios where datum initially
qualifies for out-of-line storage but becomes sufficiently small in
size after compression—specifically under the 2KB threshold(extended
storage type)—it no longer meets the criteria for external storage.
Consequently, it cannot utilize a TOAST pointer and must instead be
stored in-line.
Given this behavior, it is more robust to store metadata at the
varattrib_4b level. This ensures that metadata remains accessible
regardless of whether the datum ends up stored in-line or externally.
Moreover, during detoasting it first fetches the external data,
reconstructs it into varattrib_4b, then decompresses—so keeping
metadata in varattrib_4b matches that flow.

This is the layout for extra 1 byte in both varatt_external and varattrib_4b.
```
bit 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| cmid − 2 | F|
+---+---+---+---+---+---+---+---+

• Bits 7–1 (cmid − 2)
– 7-bit field holding compression IDs: raw ∈ [0…127] ⇒ cmid = raw +
2 ([2…129])
• Bit 0 (F)
– flag indicating whether the algorithm expects metadata
```

Introduced metadata flag in the 1-byte layout, To prevent zstd from
exposing dict or nodict types for ToastCompressionId. This metadata
flag indicates whether the algorithm expects any metadata or not. For
the ZSTD scenario, if the flag is set, it expects a dictid; otherwise,
no dictid is present.

```
typedef enum ToastCompressionId
{
TOAST_PGLZ_COMPRESSION_ID = 0,
TOAST_LZ4_COMPRESSION_ID = 1,
TOAST_ZSTD_COMPRESSION_ID = 2,
TOAST_INVALID_COMPRESSION_ID = 3,
} ToastCompressionId;

// varattrib_4b remains unchanged from the previous proposal
typedef union
{
struct /* Normal varlena (4-byte length) */
{
uint32 va_header;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_4byte;

struct /* Compressed in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size and method; see va_extinfo */
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed;

struct /* Extended compressed in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size and method; see va_extinfo */
uint8 va_ecinfo;
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;
```

During compression, compression methods (zstd_compress_datum) will
determine whether to use metadata(dictionary) or not based on
CompressionInfo.meta.

Per-column ZSTD compression levels:
Since ZSTD supports compression levels (default = 3, up to
ZSTD_maxCLevel()—currently 22—and negative “fast” levels), I’m
proposing an option for users to choose their preferred level on a
per-column basis via pg_attribute.attoptions. If unset, we’ll use
ZSTD’s default:

```
typedef struct AttributeOpts
{
int32 vl_len_; /* varlena header (do not touch!) */
float8 n_distinct;
float8 n_distinct_inherited;
int zstd_level; /* user-specified ZSTD level */
} AttributeOpts;

ALTER TABLE tblname
ALTER COLUMN colname
SET (zstd_level = 5);
```

Since PostgreSQL doesn’t currently expose LZ4 compression levels, I
propose adding per-column ZSTD compression level settings so users can
tune the speed/ratio trade-off. I’d like to hear thoughts on this
approach.

v24-0001-Design-to-extend-the-varattrib_4b-varatt_externa.patch -
Design proposal for varattrib_4b & varatt_external
v24-0002-zstd-nodict-compression.patch - ZSTD no dictionary implementation.

--
Nikhil Veldanda

Attachments:

v24-0002-zstd-nodict-compression.patchapplication/octet-stream; name=v24-0002-zstd-nodict-compression.patchDownload
From 76dbfc87f89b768fb06e09503384ad7e6cfa46a5 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Tue, 20 May 2025 07:11:24 +0000
Subject: [PATCH v24 2/2] zstd nodict compression

---
 contrib/amcheck/verify_heapam.c               |   1 +
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/reloptions.c        |  14 +-
 src/backend/access/common/toast_compression.c | 191 ++++++++++++++++-
 src/backend/access/common/toast_internals.c   |   4 +
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/pg_dump/pg_dump.c                     |   3 +
 src/bin/psql/describe.c                       |   5 +-
 src/bin/psql/tab-complete.in.c                |   4 +-
 src/include/access/toast_compression.h        |  23 +-
 src/include/access/toast_internals.h          |   3 +-
 src/include/utils/attoptcache.h               |   1 +
 src/include/varatt.h                          |   3 +-
 src/test/regress/expected/compression.out     |   5 +-
 src/test/regress/expected/compression_1.out   |   3 +
 .../regress/expected/compression_zstd.out     | 198 ++++++++++++++++++
 .../regress/expected/compression_zstd_1.out   | 104 +++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression.sql          |   1 +
 src/test/regress/sql/compression_zstd.sql     |  83 ++++++++
 22 files changed, 644 insertions(+), 24 deletions(-)
 create mode 100644 src/test/regress/expected/compression_zstd.out
 create mode 100644 src/test/regress/expected/compression_zstd_1.out
 create mode 100644 src/test/regress/sql/compression_zstd.sql

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 2161d129502..b50f3b43951 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1792,6 +1792,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 01419d1c65f..6a2e6c9683d 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -246,10 +246,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
 				TOAST_PGLZ_COMPRESSION_ID)
@@ -485,6 +485,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +530,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 46c1dce222d..1267668a242 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -24,6 +24,7 @@
 #include "access/nbtree.h"
 #include "access/reloptions.h"
 #include "access/spgist_private.h"
+#include "access/toast_compression.h"
 #include "catalog/pg_type.h"
 #include "commands/defrem.h"
 #include "commands/tablespace.h"
@@ -381,7 +382,15 @@ static relopt_int intRelOpts[] =
 		},
 		-1, 0, 1024
 	},
-
+	{
+		{
+			"zstd_level",
+			"Set column's ZSTD compression level",
+			RELOPT_KIND_ATTRIBUTE,
+			ShareUpdateExclusiveLock
+		},
+		DEFAULT_ZSTD_LEVEL, MIN_ZSTD_LEVEL, MAX_ZSTD_LEVEL
+	},
 	/* list terminator */
 	{{NULL}}
 };
@@ -2097,7 +2106,8 @@ attribute_reloptions(Datum reloptions, bool validate)
 {
 	static const relopt_parse_elt tab[] = {
 		{"n_distinct", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct)},
-		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)}
+		{"n_distinct_inherited", RELOPT_TYPE_REAL, offsetof(AttributeOpts, n_distinct_inherited)},
+		{"zstd_level", RELOPT_TYPE_INT, offsetof(AttributeOpts, zstd_level)},
 	};
 
 	return (bytea *) build_reloptions(reloptions, validate,
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 422a387b06f..5e7322ce7b9 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,6 +17,10 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
@@ -26,11 +30,19 @@
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
 
-#define NO_LZ4_SUPPORT() \
+#ifdef USE_ZSTD
+#define ZSTD_CHECK_ERROR(zstd_ret, msg) \
+	do { \
+		if (ZSTD_isError(zstd_ret)) \
+			ereport(ERROR, (errmsg("%s: %s", (msg), ZSTD_getErrorName(zstd_ret)))); \
+	} while (0)
+#endif
+
+#define COMPRESSION_METHOD_NOT_SUPPORTED(method) \
 	ereport(ERROR, \
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
-			 errmsg("compression method lz4 not supported"), \
-			 errdetail("This functionality requires the server to be built with lz4 support.")))
+			 errmsg("compression method %s not supported", method), \
+			 errdetail("This functionality requires the server to be built with %s support.", method)))
 
 /*
  * Compress a varlena using PGLZ.
@@ -140,7 +152,7 @@ struct varlena *
 lz4_compress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		valsize;
@@ -183,7 +195,7 @@ struct varlena *
 lz4_decompress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -216,7 +228,7 @@ struct varlena *
 lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -246,6 +258,153 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 #endif
 }
 
+/* Compress datum using ZSTD */
+struct varlena *
+zstd_compress_datum(const struct varlena *value, CompressionInfo cmp)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	size_t		cmp_size;
+
+	if (!cmp.meta)				/* ZSTD no dictionary */
+	{
+		/* Allocate space for the compressed varlena (header + data) */
+		compressed = (struct varlena *) palloc(max_size + VARHDRSZ_4BCE);
+
+		cmp_size = ZSTD_compress(VARDATA_4BCE(compressed),
+								 max_size,
+								 VARDATA_ANY(value),
+								 valsize,
+								 cmp.zstd_level);
+
+		if (ZSTD_isError(cmp_size))
+		{
+			pfree(compressed);
+			ZSTD_CHECK_ERROR(cmp_size, "ZSTD compression failed");
+		}
+
+		/*
+		 * If compression did not reduce size, return NULL so that the
+		 * uncompressed data is stored
+		 */
+		if (cmp_size > valsize)
+		{
+			pfree(compressed);
+			return NULL;
+		}
+
+		/* Set the compressed size in the varlena header */
+		SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARHDRSZ_4BCE);
+	}
+	else
+		elog(ERROR, "ZSTD metadata(dictionary) based compression not supported yet");
+
+	return compressed;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompression routine */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	/* ZSTD no dictionary compression */
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		zstd_compressed_len;
+	struct varlena *result;
+	size_t		uncmp_size;
+	bool		meta = VARATT_4BCE_PTR_HAS_META(value);
+
+	if (!meta)					/* ZSTD no dictionary */
+	{
+		zstd_compressed_len = VARSIZE_ANY(value) - VARHDRSZ_4BCE;
+
+		/* Allocate space for the uncompressed data */
+		result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+		uncmp_size = ZSTD_decompress(VARDATA(result),
+									 actual_size_exhdr,
+									 VARDATA_4BCE(value),
+									 zstd_compressed_len);
+
+		if (ZSTD_isError(uncmp_size))
+		{
+			pfree(result);
+			ZSTD_CHECK_ERROR(uncmp_size, "ZSTD decompression failed");
+		}
+
+		/* Set final size in the varlena header */
+		SET_VARSIZE(result, uncmp_size + VARHDRSZ);
+	}
+	else
+		elog(ERROR, "ZSTD metadata(dictionary) based decompression not supported yet");
+
+	return result;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	/* ZSTD no dictionary compression */
+
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	size_t		ret;
+	ZSTD_DCtx  *ZstdDecompressionCtx;
+	bool		meta = VARATT_4BCE_PTR_HAS_META(value);
+
+	if (!meta)					/* ZSTD no dictionary */
+	{
+		ZstdDecompressionCtx = ZSTD_createDCtx();
+		inBuf.src = VARDATA_4BCE(value);
+		inBuf.size = VARSIZE_ANY(value) - VARHDRSZ_4BCE;
+		inBuf.pos = 0;
+
+		result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+		outBuf.dst = (char *) result + VARHDRSZ;
+		outBuf.size = slicelength;
+		outBuf.pos = 0;
+
+		/* Common decompression loop */
+		while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+		{
+			ret = ZSTD_decompressStream(ZstdDecompressionCtx, &outBuf, &inBuf);
+			if (ZSTD_isError(ret))
+			{
+				pfree(result);
+				ZSTD_freeDCtx(ZstdDecompressionCtx);
+				ZSTD_CHECK_ERROR(ret, "zstd decompression failed");
+			}
+		}
+
+		Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+		SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+		ZSTD_freeDCtx(ZstdDecompressionCtx);
+	}
+	else
+		elog(ERROR, "ZSTD metadata(dictionary) based decompression not supported yet");
+
+	return result;
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+	return NULL;
+#endif
+}
+
 /*
  * Extract compression ID from a varlena.
  *
@@ -290,10 +449,17 @@ CompressionNameToMethod(const char *compression)
 	else if (strcmp(compression, "lz4") == 0)
 	{
 #ifndef USE_LZ4
-		NO_LZ4_SUPPORT();
+		COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd") == 0)
+	{
+#ifndef USE_ZSTD
+		COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+#endif
+		return TOAST_ZSTD_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -310,6 +476,8 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_COMPRESSION:
+			return "zstd";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
@@ -324,6 +492,7 @@ setup_cmp_info(char cmethod, Form_pg_attribute att)
 	/* initialize from the attribute’s default settings */
 	info.cmethod = cmethod;
 	info.meta = false;
+	info.zstd_level = DEFAULT_ZSTD_LEVEL;
 
 	/* If the compression method is not valid, use the current default */
 	if (!CompressionMethodIsValid(cmethod))
@@ -334,6 +503,14 @@ setup_cmp_info(char cmethod, Form_pg_attribute att)
 		case TOAST_PGLZ_COMPRESSION:
 		case TOAST_LZ4_COMPRESSION:
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			{
+				AttributeOpts *aopt = get_attribute_options(att->attrelid, att->attnum);
+
+				if (aopt != NULL)
+					info.zstd_level = aopt->zstd_level;
+			}
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", info.cmethod);
 	}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7ba28744e90..b779f61da0a 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -67,6 +67,10 @@ toast_compress_datum(Datum value, CompressionInfo cmp)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			tmp = zstd_compress_datum((const struct varlena *) value, cmp);
+			cmid = TOAST_ZSTD_COMPRESSION_ID;
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 3e4d5568bde..063780e56dc 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5301,6 +5301,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_COMPRESSION_ID:
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 2f8cbd86759..9c167653f43 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd", TOAST_ZSTD_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 34826d01380..4dd6da32324 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -756,7 +756,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e2e7975b34e..99c364e2690 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -17552,6 +17552,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 					case 'l':
 						cmname = "lz4";
 						break;
+					case 'z':
+						cmname = "zstd";
+						break;
 					default:
 						cmname = NULL;
 						break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1d08268393e..26951f8f890 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2171,8 +2171,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'z' ? "zstd" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index ec65ab79fec..64f26156aa2 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2881,11 +2881,11 @@ match_previous_words(int pattern_id,
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET ( */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "(") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "("))
-		COMPLETE_WITH("n_distinct", "n_distinct_inherited");
+		COMPLETE_WITH("n_distinct", "n_distinct_inherited", "zstd_level");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION"))
-		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4");
+		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4", "ZSTD");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET EXPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "EXPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "EXPRESSION"))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 3812fe89fe4..426f364bfab 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -15,6 +15,10 @@
 
 #include "catalog/pg_attribute.h"
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 /*
  * GUC support.
  *
@@ -28,13 +32,15 @@ typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_ZSTD_COMPRESSION_ID = 2,
+	TOAST_INVALID_COMPRESSION_ID = 3,
 } ToastCompressionId;
 
 typedef struct CompressionInfo
 {
 	char		cmethod;
 	bool		meta;
+	int			zstd_level;
 } CompressionInfo;
 
 /*
@@ -44,11 +50,21 @@ typedef struct CompressionInfo
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_COMPRESSION			'z'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
 #define TOAST_CMPID_EXTENDED(cmpid)	(!(cmpid == TOAST_PGLZ_COMPRESSION_ID || cmpid == TOAST_LZ4_COMPRESSION_ID ||cmpid == TOAST_INVALID_COMPRESSION_ID))
 
+#ifdef USE_ZSTD
+#define DEFAULT_ZSTD_LEVEL					ZSTD_CLEVEL_DEFAULT
+#define MIN_ZSTD_LEVEL						(int)-ZSTD_BLOCKSIZE_MAX
+#define MAX_ZSTD_LEVEL						22
+#else
+#define DEFAULT_ZSTD_LEVEL					0
+#define MIN_ZSTD_LEVEL						0
+#define MAX_ZSTD_LEVEL						0
+#endif
 
 /* pglz compression/decompression routines */
 extern struct varlena *pglz_compress_datum(const struct varlena *value);
@@ -62,6 +78,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd nodict compression/decompression routines */
+extern struct varlena *zstd_compress_datum(const struct varlena *value, CompressionInfo cmp);
+extern struct varlena *zstd_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 966317f2399..431ed4b038a 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -25,7 +25,8 @@
 	do {																				\
 		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);								\
 		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||								\
-				(cm_method) == TOAST_LZ4_COMPRESSION_ID);								\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID	||								\
+				(cm_method) == TOAST_ZSTD_COMPRESSION_ID);								\
 		if (!TOAST_CMPID_EXTENDED((cm_method)))											\
 		{																				\
 			((varattrib_4b *)(ptr))->va_compressed.va_tcinfo =							\
diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h
index f684a772af5..51d65ebd646 100644
--- a/src/include/utils/attoptcache.h
+++ b/src/include/utils/attoptcache.h
@@ -21,6 +21,7 @@ typedef struct AttributeOpts
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	float8		n_distinct;
 	float8		n_distinct_inherited;
+	int			zstd_level;
 } AttributeOpts;
 
 extern AttributeOpts *get_attribute_options(Oid attrelid, int attnum);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index bb4496d81d6..eb63d28af82 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -386,7 +386,8 @@ typedef struct
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm, meta)				\
 	do {																						\
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID ||												\
-				(cm) == TOAST_LZ4_COMPRESSION_ID);												\
+				(cm) == TOAST_LZ4_COMPRESSION_ID ||												\
+				(cm) == TOAST_ZSTD_COMPRESSION_ID);												\
 		if (!TOAST_CMPID_EXTENDED(cm))															\
 		{																						\
 			/* method fits in the low bits of va_extinfo */										\
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 4dd9ee7200d..94495388ade 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -238,10 +238,11 @@ NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz, lz4.
+HINT:  Available values: pglz, lz4, zstd.
+SET default_toast_compression = 'zstd';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 -- test alter compression method
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out
index 7bd7642b4b9..0ce49152176 100644
--- a/src/test/regress/expected/compression_1.out
+++ b/src/test/regress/expected/compression_1.out
@@ -233,6 +233,9 @@ HINT:  Available values: pglz.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
 HINT:  Available values: pglz.
+SET default_toast_compression = 'zstd';
+ERROR:  invalid value for parameter "default_toast_compression": "zstd"
+HINT:  Available values: pglz.
 SET default_toast_compression = 'lz4';
 ERROR:  invalid value for parameter "default_toast_compression": "lz4"
 HINT:  Available values: pglz.
diff --git a/src/test/regress/expected/compression_zstd.out b/src/test/regress/expected/compression_zstd.out
new file mode 100644
index 00000000000..5a05a3e6d54
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd.out
@@ -0,0 +1,198 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE;
+NOTICE:  table "cmdata_zstd_nodict" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict (
+    f1 TEXT COMPRESSION zstd
+);
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)); -- inline
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 2500000)); -- externally stored
+  END LOOP;
+END $$;
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd_nodict;
+                                  Table "public.cmdata_zstd_nodict"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd_nodict
+GROUP BY pg_column_compression(f1);
+ compression_method | row_count 
+--------------------+-----------
+ zstd               |        30
+(1 row)
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd_nodict;
+                     data_slice                     
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+(30 rows)
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict_2;
+NOTICE:  table "cmdata_zstd_nodict_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_nodict_2;
+                                 Table "public.cmdata_zstd_nodict_2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+DROP TABLE cmdata_zstd_nodict_2;
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict;
+NOTICE:  materialized view "compressmv_zstd_nodict" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS
+  SELECT f1 FROM cmdata_zstd_nodict;
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd_nodict;
+                          Materialized view "public.compressmv_zstd_nodict"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1
+   FROM cmdata_zstd_nodict;
+
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd_nodict;
+ mv_compression 
+----------------
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+ zstd
+(30 rows)
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd_nodict
+SET f1 = f1 || ' UPDATED';
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd_nodict;
+ preview 
+---------
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+ UPDATED
+(30 rows)
+
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd_nodict;
+DROP TABLE cmdata_zstd_nodict;
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_zstd_1.out b/src/test/regress/expected/compression_zstd_1.out
new file mode 100644
index 00000000000..ae87a0b652f
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_1.out
@@ -0,0 +1,104 @@
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE;
+NOTICE:  table "cmdata_zstd_nodict" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict (
+    f1 TEXT COMPRESSION zstd
+);
+ERROR:  compression method zstd not supported
+DETAIL:  This functionality requires the server to be built with zstd support.
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)); -- inline
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 2500000)); -- externally stored
+  END LOOP;
+END $$;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 1: INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('12345678...
+                    ^
+QUERY:  INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004))
+CONTEXT:  PL/pgSQL function inline_code_block line 4 at SQL statement
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd_nodict;
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd_nodict
+GROUP BY pg_column_compression(f1);
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 3: FROM cmdata_zstd_nodict
+             ^
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd_nodict;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 2: FROM cmdata_zstd_nodict;
+             ^
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict_2;
+NOTICE:  table "cmdata_zstd_nodict_2" does not exist, skipping
+CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION);
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 1: CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict I...
+                                                ^
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_nodict_2;
+DROP TABLE cmdata_zstd_nodict_2;
+ERROR:  table "cmdata_zstd_nodict_2" does not exist
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict;
+NOTICE:  materialized view "compressmv_zstd_nodict" does not exist, skipping
+CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS
+  SELECT f1 FROM cmdata_zstd_nodict;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 2:   SELECT f1 FROM cmdata_zstd_nodict;
+                         ^
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd_nodict;
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd_nodict;
+ERROR:  relation "compressmv_zstd_nodict" does not exist
+LINE 2: FROM compressmv_zstd_nodict;
+             ^
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd_nodict
+SET f1 = f1 || ' UPDATED';
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 1: UPDATE cmdata_zstd_nodict
+               ^
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd_nodict;
+ERROR:  relation "cmdata_zstd_nodict" does not exist
+LINE 2: FROM cmdata_zstd_nodict;
+             ^
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd_nodict;
+ERROR:  materialized view "compressmv_zstd_nodict" does not exist
+DROP TABLE cmdata_zstd_nodict;
+ERROR:  table "cmdata_zstd_nodict" does not exist
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a424be2a6bf..7e1d227b976 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_zstd memoize stats predicate numa
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index 490595fcfb2..e29909558f9 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -102,6 +102,7 @@ CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 -- test default_toast_compression GUC
 SET default_toast_compression = '';
 SET default_toast_compression = 'I do not exist compression';
+SET default_toast_compression = 'zstd';
 SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 
diff --git a/src/test/regress/sql/compression_zstd.sql b/src/test/regress/sql/compression_zstd.sql
new file mode 100644
index 00000000000..4200d9a78fc
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd.sql
@@ -0,0 +1,83 @@
+\set HIDE_TOAST_COMPRESSION false
+
+-- Ensure stable results regardless of the installation's default.
+SET default_toast_compression = 'pglz';
+
+----------------------------------------------------------------
+-- 1. Create Test Table with Zstd Compression
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict CASCADE;
+CREATE TABLE cmdata_zstd_nodict (
+    f1 TEXT COMPRESSION zstd
+);
+
+----------------------------------------------------------------
+-- 2. Insert Data Rows
+----------------------------------------------------------------
+DO $$
+BEGIN
+  FOR i IN 1..15 LOOP
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 1004)); -- inline
+    INSERT INTO cmdata_zstd_nodict (f1) VALUES (repeat('1234567890', 2500000)); -- externally stored
+  END LOOP;
+END $$;
+
+----------------------------------------------------------------
+-- 3. Verify Table Structure and Compression Settings
+----------------------------------------------------------------
+-- Table Structure for cmdata_zstd
+\d+ cmdata_zstd_nodict;
+
+-- Compression Settings for f1 Column
+SELECT pg_column_compression(f1) AS compression_method,
+       count(*) AS row_count
+FROM cmdata_zstd_nodict
+GROUP BY pg_column_compression(f1);
+
+----------------------------------------------------------------
+-- 4. Decompression Tests
+----------------------------------------------------------------
+--  Decompression Slice Test (Extracting Substrings)
+SELECT SUBSTR(f1, 200, 50) AS data_slice
+FROM cmdata_zstd_nodict;
+
+----------------------------------------------------------------
+-- 5. Test Table Creation with LIKE INCLUDING COMPRESSION
+----------------------------------------------------------------
+DROP TABLE IF EXISTS cmdata_zstd_nodict_2;
+CREATE TABLE cmdata_zstd_nodict_2 (LIKE cmdata_zstd_nodict INCLUDING COMPRESSION);
+--  Table Structure for cmdata_zstd_2
+\d+ cmdata_zstd_nodict_2;
+DROP TABLE cmdata_zstd_nodict_2;
+
+----------------------------------------------------------------
+-- 6. Materialized View Compression Test
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW IF EXISTS compressmv_zstd_nodict;
+CREATE MATERIALIZED VIEW compressmv_zstd_nodict AS
+  SELECT f1 FROM cmdata_zstd_nodict;
+
+--  Materialized View Structure for compressmv_zstd
+\d+ compressmv_zstd_nodict;
+
+--  Materialized View Compression Check
+SELECT pg_column_compression(f1) AS mv_compression
+FROM compressmv_zstd_nodict;
+
+----------------------------------------------------------------
+-- 7. Additional Updates and Round-Trip Tests
+----------------------------------------------------------------
+-- Update some rows to check if the dictionary remains effective after modifications.
+UPDATE cmdata_zstd_nodict
+SET f1 = f1 || ' UPDATED';
+
+--  Verification of Updated Rows
+SELECT SUBSTR(f1, LENGTH(f1) - 7 + 1, 7) AS preview
+FROM cmdata_zstd_nodict;
+----------------------------------------------------------------
+-- 8. Clean Up
+----------------------------------------------------------------
+DROP MATERIALIZED VIEW compressmv_zstd_nodict;
+DROP TABLE cmdata_zstd_nodict;
+
+\set HIDE_TOAST_COMPRESSION true
-- 
2.47.1

v24-0001-Design-to-extend-the-varattrib_4b-varatt_externa.patchapplication/octet-stream; name=v24-0001-Design-to-extend-the-varattrib_4b-varatt_externa.patchDownload
From efb7ec44fe72817ad005341fb8be816bfc0c20ea Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Tue, 20 May 2025 05:43:29 +0000
Subject: [PATCH v24 1/2] Design to extend the varattrib_4b/varatt_external
 format for support of multiple TOAST compression algorithms.

---
 contrib/amcheck/verify_heapam.c               |   2 +-
 src/backend/access/brin/brin_tuple.c          |   4 +-
 src/backend/access/common/detoast.c           |   6 +-
 src/backend/access/common/indextuple.c        |   5 +-
 src/backend/access/common/toast_compression.c |  26 ++++
 src/backend/access/common/toast_internals.c   |  29 +++--
 src/backend/access/table/toast_helper.c       |   8 +-
 src/include/access/detoast.h                  |   7 +-
 src/include/access/toast_compression.h        |  22 ++--
 src/include/access/toast_internals.h          |  42 +++----
 src/include/varatt.h                          | 114 +++++++++++++++---
 src/tools/pgindent/typedefs.list              |   2 +-
 12 files changed, 189 insertions(+), 78 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index aa9cccd1da4..2161d129502 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1786,7 +1786,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 861f397e6db..eb19739da03 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -223,6 +223,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 			{
 				Datum		cvalue;
 				char		compression;
+				CompressionInfo cmp;
 				Form_pg_attribute att = TupleDescAttr(brdesc->bd_tupdesc,
 													  keyno);
 
@@ -237,7 +238,8 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 				else
 					compression = InvalidCompressionMethod;
 
-				cvalue = toast_compress_datum(value, compression);
+				cmp = setup_cmp_info(compression, att);
+				cvalue = toast_compress_datum(value, cmp);
 
 				if (DatumGetPointer(cvalue) != NULL)
 				{
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..01419d1c65f 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -478,7 +478,7 @@ toast_decompress_datum(struct varlena *attr)
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
@@ -514,14 +514,14 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 	 * have been seen to give wrong results if passed an output size that is
 	 * more than the data's true decompressed size.
 	 */
-	if ((uint32) slicelength >= TOAST_COMPRESS_EXTSIZE(attr))
+	if ((uint32) slicelength >= VARDATA_COMPRESSED_GET_EXTSIZE(attr))
 		return toast_decompress_datum(attr);
 
 	/*
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data slice using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 1986b943a28..1fe0e4288cc 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -123,9 +123,10 @@ index_form_tuple_context(TupleDesc tupleDescriptor,
 			 att->attstorage == TYPSTORAGE_MAIN))
 		{
 			Datum		cvalue;
+			CompressionInfo cmp;
 
-			cvalue = toast_compress_datum(untoasted_values[i],
-										  att->attcompression);
+			cmp = setup_cmp_info(att->attcompression, att);
+			cvalue = toast_compress_datum(untoasted_values[i], cmp);
 
 			if (DatumGetPointer(cvalue) != NULL)
 			{
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97e..422a387b06f 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -21,6 +21,7 @@
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
+#include "utils/attoptcache.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
@@ -314,3 +315,28 @@ GetCompressionMethodName(char method)
 			return NULL;		/* keep compiler quiet */
 	}
 }
+
+CompressionInfo
+setup_cmp_info(char cmethod, Form_pg_attribute att)
+{
+	CompressionInfo info;
+
+	/* initialize from the attribute’s default settings */
+	info.cmethod = cmethod;
+	info.meta = false;
+
+	/* If the compression method is not valid, use the current default */
+	if (!CompressionMethodIsValid(cmethod))
+		info.cmethod = default_toast_compression;
+
+	switch (info.cmethod)
+	{
+		case TOAST_PGLZ_COMPRESSION:
+		case TOAST_LZ4_COMPRESSION:
+			break;
+		default:
+			elog(ERROR, "invalid compression method %c", info.cmethod);
+	}
+
+	return info;
+}
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346ce..7ba28744e90 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -43,7 +43,7 @@ static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
  * ----------
  */
 Datum
-toast_compress_datum(Datum value, char cmethod)
+toast_compress_datum(Datum value, CompressionInfo cmp)
 {
 	struct varlena *tmp = NULL;
 	int32		valsize;
@@ -54,14 +54,10 @@ toast_compress_datum(Datum value, char cmethod)
 
 	valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(value));
 
-	/* If the compression method is not valid, use the current default */
-	if (!CompressionMethodIsValid(cmethod))
-		cmethod = default_toast_compression;
-
 	/*
 	 * Call appropriate compression routine for the compression method.
 	 */
-	switch (cmethod)
+	switch (cmp.cmethod)
 	{
 		case TOAST_PGLZ_COMPRESSION:
 			tmp = pglz_compress_datum((const struct varlena *) value);
@@ -72,7 +68,7 @@ toast_compress_datum(Datum value, char cmethod)
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
 		default:
-			elog(ERROR, "invalid compression method %c", cmethod);
+			elog(ERROR, "invalid compression method %c", cmp.cmethod);
 	}
 
 	if (tmp == NULL)
@@ -90,9 +86,11 @@ toast_compress_datum(Datum value, char cmethod)
 	 */
 	if (VARSIZE(tmp) < valsize - 2)
 	{
+		bool		meta = cmp.meta;
+
 		/* successful compression */
 		Assert(cmid != TOAST_INVALID_COMPRESSION_ID);
-		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid);
+		TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid, meta);
 		return PointerGetDatum(tmp);
 	}
 	else
@@ -143,6 +141,7 @@ toast_save_datum(Relation rel, Datum value,
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	ToastCompressionId cm = TOAST_INVALID_COMPRESSION_ID;
 
 	Assert(!VARATT_IS_EXTERNAL(value));
 
@@ -179,14 +178,18 @@ toast_save_datum(Relation rel, Datum value,
 	}
 	else if (VARATT_IS_COMPRESSED(dval))
 	{
+		bool		meta;
+
 		data_p = VARDATA(dval);
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
 		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
-
+		cm = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
+		meta = TOAST_CMPID_EXTENDED(cm) ?
+			VARATT_4BCE_HAS_META(((varattrib_4b *) (dval))->va_compressed_ext.va_ecinfo) :
+			false;
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo, cm, meta);
 		/* Assert that the numbers look like it's compressed */
 		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
@@ -368,9 +371,9 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
+	result = (struct varlena *) palloc(TOAST_CMPID_EXTENDED(cm) ? TOAST_POINTER_EXT_SIZE : TOAST_POINTER_NOEXT_SIZE);
 	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, TOAST_CMPID_EXTENDED(cm) ? TOAST_POINTER_EXT_SIZE - VARHDRSZ_EXTERNAL : TOAST_POINTER_NOEXT_SIZE - VARHDRSZ_EXTERNAL);
 
 	return PointerGetDatum(result);
 }
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d2..1edd07634db 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,7 +171,7 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
+ * The column must have a minimum size of MAXALIGN(TOAST_POINTER_NOEXT_SIZE);
  * if not, no benefit is to be expected by compressing it.
  *
  * The return value is the index of the biggest suitable column, or
@@ -184,7 +184,7 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = MAXALIGN(TOAST_POINTER_NOEXT_SIZE);
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
@@ -229,8 +229,10 @@ toast_tuple_try_compression(ToastTupleContext *ttc, int attribute)
 	Datum	   *value = &ttc->ttc_values[attribute];
 	Datum		new_value;
 	ToastAttrInfo *attr = &ttc->ttc_attr[attribute];
+	Form_pg_attribute att = TupleDescAttr(ttc->ttc_rel->rd_att, attribute);
+	CompressionInfo cmp = setup_cmp_info(attr->tai_compression, att);
 
-	new_value = toast_compress_datum(*value, attr->tai_compression);
+	new_value = toast_compress_datum(*value, cmp);
 
 	if (DatumGetPointer(new_value) != NULL)
 	{
diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c3..8dbbe4d8192 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -23,12 +23,13 @@
 do { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
 	Assert(VARATT_IS_EXTERNAL(attre)); \
-	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
-	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
+	memset(&(toast_pointer), 0, sizeof(toast_pointer)); \
+	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), VARSIZE_EXTERNAL(attre) - VARHDRSZ_EXTERNAL); \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_POINTER_NOEXT_SIZE (VARHDRSZ_EXTERNAL + offsetof(varatt_external, extended))
+#define TOAST_POINTER_EXT_SIZE (TOAST_POINTER_NOEXT_SIZE + MEMBER_SIZE(varatt_external, extended.cmp))
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..3812fe89fe4 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,6 +13,8 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#include "catalog/pg_attribute.h"
+
 /*
  * GUC support.
  *
@@ -22,18 +24,6 @@
  */
 extern PGDLLIMPORT int default_toast_compression;
 
-/*
- * Built-in compression method ID.  The toast compression header will store
- * this in the first 2 bits of the raw length.  These built-in compression
- * method IDs are directly mapped to the built-in compression methods.
- *
- * Don't use these values for anything other than understanding the meaning
- * of the raw bits from a varlena; in particular, if the goal is to identify
- * a compression method, use the constants TOAST_PGLZ_COMPRESSION, etc.
- * below. We might someday support more than 4 compression methods, but
- * we can never have more than 4 values in this enum, because there are
- * only 2 bits available in the places where this is stored.
- */
 typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
@@ -41,6 +31,12 @@ typedef enum ToastCompressionId
 	TOAST_INVALID_COMPRESSION_ID = 2,
 } ToastCompressionId;
 
+typedef struct CompressionInfo
+{
+	char		cmethod;
+	bool		meta;
+} CompressionInfo;
+
 /*
  * Built-in compression methods.  pg_attribute will store these in the
  * attcompression column.  In attcompression, InvalidCompressionMethod
@@ -51,6 +47,7 @@ typedef enum ToastCompressionId
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
+#define TOAST_CMPID_EXTENDED(cmpid)	(!(cmpid == TOAST_PGLZ_COMPRESSION_ID || cmpid == TOAST_LZ4_COMPRESSION_ID ||cmpid == TOAST_INVALID_COMPRESSION_ID))
 
 
 /* pglz compression/decompression routines */
@@ -69,5 +66,6 @@ extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
 extern const char *GetCompressionMethodName(char method);
+extern CompressionInfo setup_cmp_info(char cmethod, Form_pg_attribute att);
 
 #endif							/* TOAST_COMPRESSION_H */
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..966317f2399 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -17,35 +17,31 @@
 #include "utils/relcache.h"
 #include "utils/snapshot.h"
 
-/*
- *	The information at the start of the compressed toast data.
- */
-typedef struct toast_compress_header
-{
-	int32		vl_len_;		/* varlena header (do not touch directly!) */
-	uint32		tcinfo;			/* 2 bits for compression method and 30 bits
-								 * external size; see va_extinfo */
-} toast_compress_header;
-
 /*
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
-#define TOAST_COMPRESS_EXTSIZE(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
-
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
-	do { \
-		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
-		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method, meta)			\
+	do {																				\
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);								\
+		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||								\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID);								\
+		if (!TOAST_CMPID_EXTENDED((cm_method)))											\
+		{																				\
+			((varattrib_4b *)(ptr))->va_compressed.va_tcinfo =							\
+				((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS);		\
+		}																				\
+		else																			\
+		{																				\
+			/* extended path: mark EXT flag in tcinfo */								\
+			((varattrib_4b *)(ptr))->va_compressed_ext.va_tcinfo =						\
+				((uint32)(len)) | ((uint32)(VARATT_4BCE_EXTFLAG) << VARLENA_EXTSIZE_BITS);	\
+			((varattrib_4b *)(ptr))->va_compressed_ext.va_ecinfo = 						\
+				VARATT_4BCE_ENCODE((meta), (cm_method));							\
+		}																				\
 	} while (0)
 
-extern Datum toast_compress_datum(Datum value, char cmethod);
+extern Datum toast_compress_datum(Datum value, CompressionInfo cmp);
 extern Oid	toast_get_valid_index(Oid toastoid, LOCKMODE lock);
 
 extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d4998..bb4496d81d6 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -28,6 +28,9 @@
  * you need to memcpy from the tuple into a local struct variable before
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
+ *
+ * When the top two bits of va_extinfo (as checked by VARATT_4BCE_EXTFLAG) are set,
+ * It means it holds additional information.
  */
 typedef struct varatt_external
 {
@@ -36,6 +39,14 @@ typedef struct varatt_external
 								 * compression method */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+	/* -------- optional trailer -------- */
+	union
+	{
+		struct					/* compression-method trailer */
+		{
+			uint8		va_ecinfo;	/* Extended compression methods info */
+		}			cmp;
+	}			extended;		/* "extended" = optional bytes */
 }			varatt_external;
 
 /*
@@ -93,11 +104,24 @@ typedef enum vartag_external
 #define VARTAG_IS_EXPANDED(tag) \
 	(((tag) & ~1) == VARTAG_EXPANDED_RO)
 
-#define VARTAG_SIZE(tag) \
-	((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \
-	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
-	 (tag) == VARTAG_ONDISK ? sizeof(varatt_external) : \
-	 (AssertMacro(false), 0))
+#define MEMBER_SIZE(type, member)  sizeof( ((type *)0)->member )
+
+#define VARTAG_SIZE(PTR)														\
+(																				\
+	VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT ?									\
+		sizeof(varatt_indirect) :												\
+	VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR)) ?									\
+		sizeof(varatt_expanded) :												\
+	VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK ?										\
+		(offsetof(varatt_external, extended) +									\
+			((UNALIGNED_U32((const uint8 *)(PTR) + VARHDRSZ_EXTERNAL +			\
+							offsetof(varatt_external, va_extinfo))				\
+			>> VARLENA_EXTSIZE_BITS) == VARATT_4BCE_EXTFLAG						\
+				? MEMBER_SIZE(varatt_external, extended.cmp)					\
+				: 0))															\
+		:																		\
+	(AssertMacro(false), 0)														\
+)
 
 /*
  * These structs describe the header of a varlena object that may have been
@@ -122,6 +146,14 @@ typedef union
 								 * compression method; see va_extinfo */
 		char		va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
 	}			va_compressed;
+	struct
+	{
+		uint32		va_header;
+		uint32		va_tcinfo;	/* Original data size (excludes header) and
+								 * compression method; see va_extinfo */
+		uint8		va_ecinfo;	/* algorithm id (0–255) */
+		char		va_data[FLEXIBLE_ARRAY_MEMBER];
+	}			va_compressed_ext;
 } varattrib_4b;
 
 typedef struct
@@ -206,6 +238,12 @@ typedef struct
 	(((varattrib_1b_e *) (PTR))->va_header = 0x80, \
 	 ((varattrib_1b_e *) (PTR))->va_tag = (tag))
 
+#define UNALIGNED_U32(ptr)							\
+	( (uint32) (((const uint8 *)(ptr))[3])			\
+	| ((uint32)(((const uint8 *)(ptr))[2]) <<  8)	\
+	| ((uint32)(((const uint8 *)(ptr))[1]) << 16)	\
+	| ((uint32)(((const uint8 *)(ptr))[0]) << 24) )
+
 #else							/* !WORDS_BIGENDIAN */
 
 #define VARATT_IS_4B(PTR) \
@@ -239,6 +277,12 @@ typedef struct
 	(((varattrib_1b_e *) (PTR))->va_header = 0x01, \
 	 ((varattrib_1b_e *) (PTR))->va_tag = (tag))
 
+#define UNALIGNED_U32(ptr)							\
+	( (uint32) (((const uint8 *)(ptr))[0])			\
+	| ((uint32)(((const uint8 *)(ptr))[1]) <<  8)	\
+	| ((uint32)(((const uint8 *)(ptr))[2]) << 16)	\
+	| ((uint32)(((const uint8 *)(ptr))[3]) << 24) )
+
 #endif							/* WORDS_BIGENDIAN */
 
 #define VARDATA_4B(PTR)		(((varattrib_4b *) (PTR))->va_4byte.va_data)
@@ -282,7 +326,7 @@ typedef struct
 #define VARDATA_SHORT(PTR)					VARDATA_1B(PTR)
 
 #define VARTAG_EXTERNAL(PTR)				VARTAG_1B_E(PTR)
-#define VARSIZE_EXTERNAL(PTR)				(VARHDRSZ_EXTERNAL + VARTAG_SIZE(VARTAG_EXTERNAL(PTR)))
+#define VARSIZE_EXTERNAL(PTR)				(VARHDRSZ_EXTERNAL + VARTAG_SIZE(PTR))
 #define VARDATA_EXTERNAL(PTR)				VARDATA_1B_E(PTR)
 
 #define VARATT_IS_COMPRESSED(PTR)			VARATT_IS_4B_C(PTR)
@@ -328,20 +372,33 @@ typedef struct
 #define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
 	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+	( (VARATT_IS_4BCE(PTR)) ? VARATT_4BCE_GET_CMID(((varattrib_4b *) (PTR))->va_compressed_ext.va_ecinfo) \
+	: (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS))
 
 /* Same for external Datums; but note argument is a struct varatt_external */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
-#define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
-	((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS)
-
-#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
-	do { \
-		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
+#define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer)							\
+	( ((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS) == VARATT_4BCE_EXTFLAG	\
+		? VARATT_4BCE_GET_CMID((toast_pointer).extended.cmp.va_ecinfo)				\
+			: (toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS )
+
+#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm, meta)				\
+	do {																						\
+		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID ||												\
+				(cm) == TOAST_LZ4_COMPRESSION_ID);												\
+		if (!TOAST_CMPID_EXTENDED(cm))															\
+		{																						\
+			/* method fits in the low bits of va_extinfo */										\
+			(toast_pointer).va_extinfo = (uint32)(len) | ((uint32)(cm) << VARLENA_EXTSIZE_BITS);\
+		}																						\
+		else																					\
+		{																						\
+			/* set “extended” flag and store the extra byte */									\
+			(toast_pointer).va_extinfo = (uint32)(len) |										\
+				(VARATT_4BCE_EXTFLAG << VARLENA_EXTSIZE_BITS);									\
+			(toast_pointer).extended.cmp.va_ecinfo = VARATT_4BCE_ENCODE(meta, cm);		\
+		}																						\
 	} while (0)
 
 /*
@@ -355,4 +412,29 @@ typedef struct
 	(VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
 	 (toast_pointer).va_rawsize - VARHDRSZ)
 
+/* Upper-two-bit pattern 0b11 marks “extended compression info present”. */
+#define VARATT_4BCE_EXTFLAG             0x3
+
+/* Helper: pack <flag, cmid> into a single byte:  flag (b0), cmid-2 (b1..7) */
+#define VARATT_4BCE_ENCODE(flag, cmid) \
+    ((uint8)((((cmid) - 2) << 1) | ((flag) & 0x01)))
+
+#define VARATT_4BCE_HAS_META(raw)       ((raw) & 0x01u)
+#define VARATT_4BCE_GET_CMID(raw)       ((((raw) >> 1) & 0x7Fu) + 2)
+
+/* Pointer-level helpers */
+#define VARATT_4BCE_PTR_HAS_META(ptr) \
+    VARATT_4BCE_HAS_META(((varattrib_4b *)(ptr))->va_compressed_ext.va_ecinfo)
+
+/* Does this varattrib use the “compressed-extended” format? */
+#define VARATT_IS_4BCE(ptr) \
+    ((((varattrib_4b *)(ptr))->va_compressed_ext.va_tcinfo >> VARLENA_EXTSIZE_BITS) \
+        == VARATT_4BCE_EXTFLAG)
+
+/* Access the start of the compressed payload */
+#define VARDATA_4BCE(ptr) \
+    (((varattrib_4b *)(ptr))->va_compressed_ext.va_data)
+
+#define VARHDRSZ_4BCE	(offsetof(varattrib_4b, va_compressed_ext.va_data))
+
 #endif
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9ea573fae21..27adbebed3f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -483,6 +483,7 @@ CompositeIOData
 CompositeTypeStmt
 CompoundAffixFlag
 CompressFileHandle
+CompressionInfo
 CompressionLocation
 CompressorState
 ComputeXidHorizonsResult
@@ -4101,7 +4102,6 @@ timeout_handler_proc
 timeout_params
 timerCA
 tlist_vinfo
-toast_compress_header
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg

base-commit: 54675d89863378b092c838a4e110551203d89b54
-- 
2.47.1

#41Michael Paquier
michael@paquier.xyz
In reply to: Nikhil Kumar Veldanda (#40)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Tue, May 27, 2025 at 02:59:17AM -0700, Nikhil Kumar Veldanda wrote:

typedef struct varatt_external
{
int32 va_rawsize; /* Original data size (includes header) */
uint32 va_extinfo; /* External size (without header) and
* compression method */
Oid va_valueid; /* Unique ID within TOAST table */
Oid va_toastrelid; /* OID of TOAST table containing it */
/* -------- optional trailer -------- */
union
{
struct /* compression-method trailer */
{
uint8 va_ecinfo; /* Extended-compression-method info */
} cmp;
} extended; /* “extended” = optional byte */
} varatt_external;
```

Yeah, something like that does make sense to me. If the three bits of
va_extinfo are set, we'd look at the next one. I'll try to think a
bit harder about the structure of varatt.h; that's the important bit
and some of its stuff is outdated. That's not necessarily related to
the patch discussed here.

I'm proposing not to store any metadata exclusively at varatt_external
level because storing metadata within varatt_external is not always
appropriate because in scenarios where datum initially qualifies for
out-of-line storage but becomes sufficiently small in size after
compression—specifically under the 2KB threshold(extended storage
type)—it no longer meets the criteria for external storage.

By metadata, are you referring to the dictionary data, like an ID
pointing to a dictionary stored elsewhere or even the dictionary data
itself? It could be something else, of course. I think that it makes
sense, because we don't need the dictionary to know which code path to
take; the compression method is the only important information to be
able to redirect to the slice, compression or decompression routines.

Given this behavior, it is more robust to store metadata at the
varattrib_4b level. This ensures that metadata remains accessible
regardless of whether the datum ends up stored in-line or externally.
Moreover, during detoasting it first fetches the external data,
reconstructs it into varattrib_4b, then decompresses—so keeping
metadata in varattrib_4b matches that flow.

Okay.

This is the layout for extra 1 byte in both varatt_external and varattrib_4b.
```
bit 7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| cmid − 2 | F|
+---+---+---+---+---+---+---+---+

• Bits 7–1 (cmid − 2)
– 7-bit field holding compression IDs: raw ∈ [0…127] ⇒ cmid = raw +
2 ([2…129])
• Bit 0 (F)
– flag indicating whether the algorithm expects metadata
```

Yeah, dedicating one bit to this fact should be more than enough, and
the metadata associated to each compression method may differ. I
don't have a lot of imagination on the matter with any other
compression methods floating around in the industry, unfortunately, so
my imagination is limited.

Introduced metadata flag in the 1-byte layout, To prevent zstd from
exposing dict or nodict types for ToastCompressionId. This metadata
flag indicates whether the algorithm expects any metadata or not. For
the ZSTD scenario, if the flag is set, it expects a dictid; otherwise,
no dictid is present.
```
typedef enum ToastCompressionId
{
TOAST_PGLZ_COMPRESSION_ID = 0,
TOAST_LZ4_COMPRESSION_ID = 1,
TOAST_ZSTD_COMPRESSION_ID = 2,
TOAST_INVALID_COMPRESSION_ID = 3,
} ToastCompressionId;

This makes sense to me; we should try to untangle ToastCompressionId
the dependency between ToastCompressionId and what's stored on disk
for the purpose of extensibility.

struct /* Extended compressed in-line format */
{
uint32 va_header;
uint32 va_tcinfo; /* Original data size and method; see va_extinfo */
uint8 va_ecinfo; /* Algorithm ID (0–255) */
char va_data[FLEXIBLE_ARRAY_MEMBER];
} va_compressed_ext;
} varattrib_4b;
```

Yep.

During compression, compression methods (zstd_compress_datum) will
determine whether to use metadata(dictionary) or not based on
CompressionInfo.meta.

Not sure about this one.

ALTER TABLE tblname
ALTER COLUMN colname
SET (zstd_level = 5);
```

Since PostgreSQL currently doesn’t expose LZ4 compression levels, I
propose adding per-column ZSTD compression level settings so users can
tune the speed/ratio trade-off. I’d like to hear thoughts on this
approach.

Specifying that as an attribute option makes sense here, but I don't
think that this has to be linked to the initial patch set that should
extend the toast data for the new compression method. It's a bit hard
to say how relevant that is, and IMV it's kind of hard for users to
know which level makes more sense. Setting up the wrong level can be
equally very costly in CPU. For now, my suggestion would be to focus
on the basics, and discard this part until we figure out the rest.

Anyway, I've read through the patches, and got a couple of comments.
This includes a few pieces that we are going to need to make the
implementation a bit easier that I've noticed while reading your
patch. Some of them can be implemented even before we add this extra
byte for the new compression methods in the varlena headers.

+CompressionInfo
+setup_cmp_info(char cmethod, Form_pg_attribute att)

This routine declares a Form_pg_attribute as argument, does not use
it. Due to that, it looks that attoptcache.h is pulled into
toast_compression.c.

Patch 0001 has the concept of metadata with various facilities, like
VARATT_4BCE_HAS_META(), CompressionInfo, etc. However at the current
stage we don't need that at all. Wouldn't it be better to delay this
kind of abstraction layer to happen after we discuss how (and if) the
dictionary part should be introduced rather than pay the cost of the
facility in the first step of the implementation? This is not
required as a first step. The toast zstd routines introduced in patch
0002 use !meta, discard meta=true as an error case.

+/* Helper: pack <flag, cmid> into a single byte: flag (b0), cmid-2
(b1..7) */

Having a one-liner here is far from enough? This is the kind of thing
where we should spend time describing how things are done and why they
are done this way. This is not sufficient, there's just too much to
guess. The fact that we have VARATT_4BCE_EXTFLAG is only, but there's
no real information about va_ecinfo and that it relates to the three
bits sets, for example.

+#define VARTAG_SIZE(PTR) \
[...]
UNALIGNED_U32()

This stuff feels magic. It's hard for someone to understand what's
going on here, and there is no explanation about why it's done this
way.

-toast_compress_datum(Datum value, char cmethod)
+toast_compress_datum(Datum value, CompressionInfo cmp)
[...]
-   /* If the compression method is not valid, use the current default */
-   if (!CompressionMethodIsValid(cmethod))
-       cmethod = default_toast_compression;

Removing the fallback to the default toast compression GUC if nothing
is valid does not look right. There could be extensions that depend
on that, and it's unclear what the benefits of setup_cmp_info() are,
because it is not documented, so it's hard for one to understand how
to use these changes.

-   result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
+   result = (struct varlena *) palloc(TOAST_CMPID_EXTENDED(cm) ? TOAST_POINTER_EXT_SIZE : TOAST_POINTER_NOEXT_SIZE);
[...]
-   memcpy(VARDATA_EXTERNAL(result), &toast_pointer,
sizeof(toast_pointer));
+   memcpy(VARDATA_EXTERNAL(result), &toast_pointer,
TOAST_CMPID_EXTENDED(cm) ? TOAST_POINTER_EXT_SIZE - VARHDRSZ_EXTERNAL
: TOAST_POINTER_NOEXT_SIZE - VARHDRSZ_EXTERNAL) ; 

That looks, err... Hard to maintain to me. Okay, that's a
calculation for the extended compression part, but perhaps this is a
sign that we need to think harder about the surroundings of the
toast_pointer to ease such calculations.

+    {
+        {
+            "zstd_level",
+            "Set column's ZSTD compression level",
+            RELOPT_KIND_ATTRIBUTE,
+            ShareUpdateExclusiveLock
+        },
+        DEFAULT_ZSTD_LEVEL, MIN_ZSTD_LEVEL, MAX_ZSTD_LEVEL
+    },

This could be worth a patch on its own, once we get the basics sorted
out. I'm not even sure that we absolutely need that, TBH. The last
time I've had a discussion on the matter for WAL compression we
discarded the argument about the level because it's hard to understand
how to tune, and the default is enough to work magics. For WAL, we've
been using ZSTD_CLEVEL_DEFAULT in xloginsert.c, and I've not actually
heard much about people wanting to tune the compression level. That
was a few years ago, perhaps there are some more different opinions on
the matter.

+#define COMPRESSION_METHOD_NOT_SUPPORTED(method) \
     ereport(ERROR, \
             (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
-             errmsg("compression method lz4 not supported"), \
-             errdetail("This functionality requires the server to be built with lz4 support.")))
+             errmsg("compression method %s not supported", method), \
+             errdetail("This functionality requires the server to be built with %s support.", method)))

Let's make that a first independent patch that applies on top of the
rest. My original zstd toast patch did the same, without a split.

Your patch introduces a new compression_zstd, touching very lightly
compression.sql. I think that we should and can do much better than
that in the long term. The coverage of compression.sql is quite good,
and what the zstd code is adding does not cover all of it. Let's
rework the tests of HEAD and split compression.sql for the LZ4 and
pglz parts. If one takes a diff between compression.out and
compression_1.out, he/she would notice that the only differences are
caused by the existence of the lz4 table. This is not the smartest
move we can do if we add more compression methods, so I'd suggest the
following:
- Add a new SQL function called pg_toast_compression_available(text)
or similar, able to return if a toast compression method is supported
or not. This would need two arguments once the initial support for
zstd is done: lz4 and zstd. For head, we only require one: lz4.
- Now, the actual reason why a function returning a boolean result is
useful is for the SQL tests. It is possible with \if to make the
tests conditional if LZ4 is supported or now, limiting the noise if
LZ4 is not supported. See for example the tricks we use for the UTF-8
encoding or NUMA.
- Move the tests related to lz4 into a separate file, outside
compression.sql, in a new file called compression_lz4.sql. With the
addition of zstd toast support, we would add a new file:
compression_zstd.sql. The new zstd suite would then just need to
copy-paste the original one, with few tweaks. It may be better to
parameterize that but we don't do that anymore these days with
input/output regression files.
--
Michael

#42Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#41)
3 attachment(s)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Thanks Michael, for providing feedback.

On Fri, May 30, 2025 at 12:21 AM Michael Paquier <michael@paquier.xyz> wrote:

During compression, compression methods (zstd_compress_datum) will
determine whether to use metadata(dictionary) or not based on
CompressionInfo.meta.

Not sure about this one.

I removed the meta field and the CompressionInfo struct. I originally
used CompressionInfo to carry the compression method, zstd_level, and
zstd dict ID downstream, but since we’re now using a default
compression level for zstd, it’s no longer needed.

ALTER TABLE tblname
ALTER COLUMN colname
SET (zstd_level = 5);
```

Specifying that as an attribute option makes sense here, but I don't
think that this has to be linked to the initial patch set that should
extend the toast data for the new compression method. It's a bit hard
to say how relevant that is, and IMV it's kind of hard for users to
know which level makes more sense. Setting up the wrong level can be
equally very costly in CPU. For now, my suggestion would be to focus
on the basics, and discard this part until we figure out the rest.

Ack. I’ve removed that option and will stick with ZSTD_CLEVEL_DEFAULT
as the compression level.

+CompressionInfo
+setup_cmp_info(char cmethod, Form_pg_attribute att)

Removed setup_cmp_info and its references.

This routine declares a Form_pg_attribute as argument, does not use
it. Due to that, it looks that attoptcache.h is pulled into
toast_compression.c.

Removed it.

Patch 0001 has the concept of metadata with various facilities, like
VARATT_4BCE_HAS_META(), CompressionInfo, etc. However at the current
stage we don't need that at all. Wouldn't it be better to delay this
kind of abstraction layer to happen after we discuss how (and if) the
dictionary part should be introduced rather than pay the cost of the
facility in the first step of the implementation? This is not
required as a first step. The toast zstd routines introduced in patch
0002 use !meta, discard meta=true as an error case.

Removed all metadata-related abstractions from patch 0001.

+/* Helper: pack <flag, cmid> into a single byte: flag (b0), cmid-2
(b1..7) */

Having a one-liner here is far from enough? This is the kind of thing
where we should spend time describing how things are done and why they
are done this way. This is not sufficient, there's just too much to
guess. The fact that we have VARATT_4BCE_EXTFLAG is only, but there's
no real information about va_ecinfo and that it relates to the three
bits sets, for example.

I’ve added a detailed comment explaining the one-byte layout.

+#define VARTAG_SIZE(PTR) \
[...]
UNALIGNED_U32()

This stuff feels magic. It's hard for someone to understand what's
going on here, and there is no explanation about why it's done this
way.

To clarify, we need to read a 32-bit value from an unaligned address
(specifically va_extinfo inside varatt_external) to determine the
toast_pointer size (by checking the top two bits to see if they equal
0b11, indicating an optional trailer). I wrote two versions of
READ_U32_UNALIGNED(ptr) that load four bytes individually and
reassemble them according to little- or big-endian order:

/**
* Safely read a 32-bit unsigned integer from *any* address, even when
* that address is **not** naturally aligned to 4 bytes. We do the load
* one byte at a time and re-assemble the word in *host* byte order.
* For LITTLE ENDIAN systems
*/
#define READ_U32_UNALIGNED(ptr) \
( (uint32) (((const uint8 *)(ptr))[0]) \
| ((uint32)(((const uint8 *)(ptr))[1]) << 8) \
| ((uint32)(((const uint8 *)(ptr))[2]) << 16) \
| ((uint32)(((const uint8 *)(ptr))[3]) << 24) )

/**
* For BIG ENDIAN systems.
*/
#define READ_U32_UNALIGNED(ptr) \
( (uint32) (((const uint8 *)(ptr))[3]) \
| ((uint32)(((const uint8 *)(ptr))[2]) << 8) \
| ((uint32)(((const uint8 *)(ptr))[1]) << 16) \
| ((uint32)(((const uint8 *)(ptr))[0]) << 24) )

Alternatively, one could use:

#define READ_U32_UNALIGNED(src) \
({ \
uint32 _tmp; \
memcpy(&_tmp, (src), sizeof(uint32)); \
_tmp; \
})

I chose the byte-by-byte version to avoid extra instructions in a hot path.

-toast_compress_datum(Datum value, char cmethod)
+toast_compress_datum(Datum value, CompressionInfo cmp)
[...]
-   /* If the compression method is not valid, use the current default */
-   if (!CompressionMethodIsValid(cmethod))
-       cmethod = default_toast_compression;

Removing the fallback to the default toast compression GUC if nothing
is valid does not look right. There could be extensions that depend
on that, and it's unclear what the benefits of setup_cmp_info() are,
because it is not documented, so it's hard for one to understand how
to use these changes.

I removed setup_cmp_info, all related code has been deleted.

-   result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
+   result = (struct varlena *) palloc(TOAST_CMPID_EXTENDED(cm) ? TOAST_POINTER_EXT_SIZE : TOAST_POINTER_NOEXT_SIZE);
[...]
-   memcpy(VARDATA_EXTERNAL(result), &toast_pointer,
sizeof(toast_pointer));
+   memcpy(VARDATA_EXTERNAL(result), &toast_pointer,
TOAST_CMPID_EXTENDED(cm) ? TOAST_POINTER_EXT_SIZE - VARHDRSZ_EXTERNAL
: TOAST_POINTER_NOEXT_SIZE - VARHDRSZ_EXTERNAL) ;

That looks, err... Hard to maintain to me. Okay, that's a
calculation for the extended compression part, but perhaps this is a
sign that we need to think harder about the surroundings of the
toast_pointer to ease such calculations.

I simplified it by introducing a helper macro:
Now both the palloc call and the memcpy length calculation simply use
TOAST_POINTER_SIZE(cm) and TOAST_POINTER_SIZE(cm) − VARHDRSZ_EXTERNAL,
respectively.

#define TOAST_POINTER_SIZE(cm) \
(TOAST_CMPID_EXTENDED(cm) ? TOAST_POINTER_EXT_SIZE : TOAST_POINTER_NOEXT_SIZE)

+    {
+        {
+            "zstd_level",
+            "Set column's ZSTD compression level",
+            RELOPT_KIND_ATTRIBUTE,
+            ShareUpdateExclusiveLock
+        },
+        DEFAULT_ZSTD_LEVEL, MIN_ZSTD_LEVEL, MAX_ZSTD_LEVEL
+    },

This could be worth a patch on its own, once we get the basics sorted
out. I'm not even sure that we absolutely need that, TBH. The last
time I've had a discussion on the matter for WAL compression we
discarded the argument about the level because it's hard to understand
how to tune, and the default is enough to work magics. For WAL, we've
been using ZSTD_CLEVEL_DEFAULT in xloginsert.c, and I've not actually
heard much about people wanting to tune the compression level. That
was a few years ago, perhaps there are some more different opinions on
the matter.

Removed it.

Your patch introduces a new compression_zstd, touching very lightly
compression.sql. I think that we should and can do much better than
that in the long term. The coverage of compression.sql is quite good,
and what the zstd code is adding does not cover all of it. Let's
rework the tests of HEAD and split compression.sql for the LZ4 and
pglz parts. If one takes a diff between compression.out and
compression_1.out, he/she would notice that the only differences are
caused by the existence of the lz4 table. This is not the smartest
move we can do if we add more compression methods, so I'd suggest the
following:
- Add a new SQL function called pg_toast_compression_available(text)
or similar, able to return if a toast compression method is supported
or not. This would need two arguments once the initial support for
zstd is done: lz4 and zstd. For head, we only require one: lz4.
- Now, the actual reason why a function returning a boolean result is
useful is for the SQL tests. It is possible with \if to make the
tests conditional if LZ4 is supported or now, limiting the noise if
LZ4 is not supported. See for example the tricks we use for the UTF-8
encoding or NUMA.
- Move the tests related to lz4 into a separate file, outside
compression.sql, in a new file called compression_lz4.sql. With the
addition of zstd toast support, we would add a new file:
compression_zstd.sql. The new zstd suite would then just need to
copy-paste the original one, with few tweaks. It may be better to
parameterize that but we don't do that anymore these days with
input/output regression files.

Agreed. I introduced pg_compression_available(text) and refactored the
SQL tests accordingly. I split out LZ4 tests into compression_lz4.sql
and created compression_zstd.sql with the appropriate differences.

v25-0001-Add-pg_compression_available-and-split-sql-compr.patch -
Introduced pg_compression_available function and split sql tests
related to compression
v25-0002-Design-to-extend-the-varattrib_4b-varatt_externa.patch -
Design proposal for varattrib_4b & varatt_external
v25-0003-Implement-Zstd-compression-no-dictionary-support.patch - zstd
no dictionary compression implementation

--
Nikhil Veldanda

Attachments:

v25-0003-Implement-Zstd-compression-no-dictionary-support.patchapplication/octet-stream; name=v25-0003-Implement-Zstd-compression-no-dictionary-support.patchDownload
From c5e6d08fa3fe3febd5b6b31ad3bda109fa693167 Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Thu, 5 Jun 2025 05:30:14 +0000
Subject: [PATCH v25 3/3] Implement Zstd compression (no dictionary support)

---
 contrib/amcheck/verify_heapam.c               |   1 +
 doc/src/sgml/catalogs.sgml                    |  28 +-
 doc/src/sgml/config.sgml                      |  12 +-
 doc/src/sgml/ref/alter_table.sgml             |   8 +-
 doc/src/sgml/ref/create_table.sgml            |  13 +-
 src/backend/access/common/detoast.c           |  12 +-
 src/backend/access/common/toast_compression.c | 162 +++++++-
 src/backend/access/common/toast_internals.c   |   4 +
 src/backend/utils/adt/varlena.c               |   3 +
 src/backend/utils/misc/guc_tables.c           |   3 +
 src/backend/utils/misc/postgresql.conf.sample |   2 +-
 src/bin/pg_dump/pg_dump.c                     |   3 +
 src/bin/psql/describe.c                       |   5 +-
 src/bin/psql/tab-complete.in.c                |   2 +-
 src/include/access/toast_compression.h        |  13 +-
 src/include/access/toast_internals.h          |   3 +-
 src/include/varatt.h                          |   3 +-
 .../regress/expected/compression_zstd.out     | 376 ++++++++++++++++++
 .../regress/expected/compression_zstd_1.out   |   5 +
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression_zstd.sql     | 162 ++++++++
 21 files changed, 776 insertions(+), 46 deletions(-)
 create mode 100644 src/test/regress/expected/compression_zstd.out
 create mode 100644 src/test/regress/expected/compression_zstd_1.out
 create mode 100644 src/test/regress/sql/compression_zstd.sql

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index 2161d129502..b50f3b43951 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1792,6 +1792,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 				/* List of all valid compression method IDs */
 			case TOAST_PGLZ_COMPRESSION_ID:
 			case TOAST_LZ4_COMPRESSION_ID:
+			case TOAST_ZSTD_COMPRESSION_ID:
 				valid = true;
 				break;
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fa86c569dc4..ef37c9c4630 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1240,19 +1240,21 @@
      </row>
 
      <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>attcompression</structfield> <type>char</type>
-      </para>
-      <para>
-       The current compression method of the column.  Typically this is
-       <literal>'\0'</literal> to specify use of the current default setting
-       (see <xref linkend="guc-default-toast-compression"/>).  Otherwise,
-       <literal>'p'</literal> selects pglz compression, while
-       <literal>'l'</literal> selects <productname>LZ4</productname>
-       compression.  However, this field is ignored
-       whenever <structfield>attstorage</structfield> does not allow
-       compression.
-      </para></entry>
+      <entry role="catalog_table_entry">
+        <para role="column_definition">
+          <structfield>attcompression</structfield> <type>char</type>
+        </para>
+        <para>
+          The current compression method of the column.  Typically this is
+          <literal>'\0'</literal> to specify use of the current default setting
+          (see <xref linkend="guc-default-toast-compression"/>).  Otherwise,
+          <literal>'p'</literal> selects pglz compression, while
+          <literal>'l'</literal> selects <productname>LZ4</productname> compression,
+          and <literal>'z'</literal> selects <productname>ZSTD</productname> compression.
+          However, this field is ignored whenever
+          <structfield>attstorage</structfield> does not allow compression.
+        </para>
+      </entry>
      </row>
 
      <row>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 021153b2a5f..11a76910539 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3402,8 +3402,8 @@ include_dir 'conf.d'
         A compressed page image will be decompressed during WAL replay.
         The supported methods are <literal>pglz</literal>,
         <literal>lz4</literal> (if <productname>PostgreSQL</productname>
-        was compiled with <option>--with-lz4</option>) and
-        <literal>zstd</literal> (if <productname>PostgreSQL</productname>
+        was compiled with <option>--with-lz4</option>),
+        and <literal>zstd</literal> (if <productname>PostgreSQL</productname>
         was compiled with <option>--with-zstd</option>).
         The default value is <literal>off</literal>.
         Only superusers and users with the appropriate <literal>SET</literal>
@@ -9817,9 +9817,11 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         the <literal>COMPRESSION</literal> column option in
         <command>CREATE TABLE</command> or
         <command>ALTER TABLE</command>.)
-        The supported compression methods are <literal>pglz</literal> and
-        (if <productname>PostgreSQL</productname> was compiled with
-        <option>--with-lz4</option>) <literal>lz4</literal>.
+        The supported compression methods are <literal>pglz</literal>,
+        <literal>lz4</literal> (if <productname>PostgreSQL</productname>
+        was compiled with <option>--with-lz4</option>),
+        and <literal>zstd</literal> (if <productname>PostgreSQL</productname>
+        was compiled with <option>--with-zstd</option>).
         The default is <literal>pglz</literal>.
        </para>
       </listitem>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index d63f3a621ac..cc32f4aa699 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -443,10 +443,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       its existing compression method, rather than being recompressed with the
       compression method of the target column.
       The supported compression
-      methods are <literal>pglz</literal> and <literal>lz4</literal>.
-      (<literal>lz4</literal> is available only if <option>--with-lz4</option>
-      was used when building <productname>PostgreSQL</productname>.)  In
-      addition, <replaceable class="parameter">compression_method</replaceable>
+      methods are <literal>pglz</literal>, 
+      <literal>lz4</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-lz4</option>),
+      and <literal>zstd</literal> (if <productname>PostgreSQL</productname> was compiled with <option>--with-zstd</option>).
+      In addition, <replaceable class="parameter">compression_method</replaceable>
       can be <literal>default</literal>, which selects the default behavior of
       consulting the <xref linkend="guc-default-toast-compression"/> setting
       at the time of data insertion to determine the method to use.
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 4a41b2f5530..a5149282b7a 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -337,16 +337,19 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      <para>
       The <literal>COMPRESSION</literal> clause sets the compression method
       for the column.  Compression is supported only for variable-width data
-      types, and is used only when the column's storage mode
+      types, and is used only when the column’s storage mode
       is <literal>main</literal> or <literal>extended</literal>.
       (See <xref linkend="sql-altertable"/> for information on
       column storage modes.) Setting this property for a partitioned table
       has no direct effect, because such tables have no storage of their own,
       but the configured value will be inherited by newly-created partitions.
-      The supported compression methods are <literal>pglz</literal> and
-      <literal>lz4</literal>.  (<literal>lz4</literal> is available only if
-      <option>--with-lz4</option> was used when building
-      <productname>PostgreSQL</productname>.)  In addition,
+      The supported compression methods are
+      <literal>pglz</literal>,
+      <literal>lz4</literal> (if <productname>PostgreSQL</productname>
+      was compiled with <option>--with-lz4</option>),
+      and <literal>zstd</literal> (if <productname>PostgreSQL</productname>
+      was compiled with <option>--with-zstd</option>).
+      In addition,
       <replaceable class="parameter">compression_method</replaceable>
       can be <literal>default</literal> to explicitly specify the default
       behavior, which is to consult the
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 01419d1c65f..6a2e6c9683d 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -246,10 +246,10 @@ detoast_attr_slice(struct varlena *attr,
 			 * Determine maximum amount of compressed data needed for a prefix
 			 * of a given length (after decompression).
 			 *
-			 * At least for now, if it's LZ4 data, we'll have to fetch the
-			 * whole thing, because there doesn't seem to be an API call to
-			 * determine how much compressed data we need to be sure of being
-			 * able to decompress the required slice.
+			 * At least for now, if it's LZ4 or Zstandard data, we'll have to
+			 * fetch the whole thing, because there doesn't seem to be an API
+			 * call to determine how much compressed data we need to be sure
+			 * of being able to decompress the required slice.
 			 */
 			if (VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) ==
 				TOAST_PGLZ_COMPRESSION_ID)
@@ -485,6 +485,8 @@ toast_decompress_datum(struct varlena *attr)
 			return pglz_decompress_datum(attr);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum(attr);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum(attr);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
@@ -528,6 +530,8 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 			return pglz_decompress_datum_slice(attr, slicelength);
 		case TOAST_LZ4_COMPRESSION_ID:
 			return lz4_decompress_datum_slice(attr, slicelength);
+		case TOAST_ZSTD_COMPRESSION_ID:
+			return zstd_decompress_datum_slice(attr, slicelength);
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index fb93555bdb0..37c85c7fb18 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -17,6 +17,10 @@
 #include <lz4.h>
 #endif
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 #include "access/detoast.h"
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
@@ -28,11 +32,19 @@
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
 
-#define NO_LZ4_SUPPORT() \
+#ifdef USE_ZSTD
+#define ZSTD_CHECK_ERROR(zstd_ret, msg) \
+	do { \
+		if (ZSTD_isError(zstd_ret)) \
+			ereport(ERROR, (errmsg("%s: %s", (msg), ZSTD_getErrorName(zstd_ret)))); \
+	} while (0)
+#endif
+
+#define COMPRESSION_METHOD_NOT_SUPPORTED(method) \
 	ereport(ERROR, \
 			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
-			 errmsg("compression method lz4 not supported"), \
-			 errdetail("This functionality requires the server to be built with lz4 support.")))
+			 errmsg("compression method %s not supported", method), \
+			 errdetail("This functionality requires the server to be built with %s support.", method)))
 
 /*
  * Compress a varlena using PGLZ.
@@ -142,7 +154,7 @@ struct varlena *
 lz4_compress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		valsize;
@@ -185,7 +197,7 @@ struct varlena *
 lz4_decompress_datum(const struct varlena *value)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -218,7 +230,7 @@ struct varlena *
 lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 {
 #ifndef USE_LZ4
-	NO_LZ4_SUPPORT();
+	COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 	return NULL;				/* keep compiler quiet */
 #else
 	int32		rawsize;
@@ -248,6 +260,133 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength)
 #endif
 }
 
+/* Compress datum using ZSTD */
+struct varlena *
+zstd_compress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	uint32		valsize = VARSIZE_ANY_EXHDR(value);
+	size_t		max_size = ZSTD_compressBound(valsize);
+	struct varlena *compressed;
+	size_t		cmp_size;
+
+	/* Allocate space for the compressed varlena (header + data) */
+	compressed = (struct varlena *) palloc(max_size + VARHDRSZ_4BCE);
+
+	cmp_size = ZSTD_compress(VARDATA_4BCE(compressed),
+							 max_size,
+							 VARDATA_ANY(value),
+							 valsize,
+							 ZSTD_CLEVEL_DEFAULT);
+
+	if (ZSTD_isError(cmp_size))
+	{
+		pfree(compressed);
+		ZSTD_CHECK_ERROR(cmp_size, "ZSTD compression failed");
+	}
+
+	/**
+	 *  If compression did not reduce size, return NULL so that the uncompressed data is stored
+	 */
+	if (cmp_size > valsize)
+	{
+		pfree(compressed);
+		return NULL;
+	}
+
+	/* Set the compressed size in the varlena header */
+	SET_VARSIZE_COMPRESSED(compressed, cmp_size + VARHDRSZ_4BCE);
+
+	return compressed;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompression routine */
+struct varlena *
+zstd_decompress_datum(const struct varlena *value)
+{
+#ifdef USE_ZSTD
+	/* ZSTD no dictionary compression */
+	uint32		actual_size_exhdr = VARDATA_COMPRESSED_GET_EXTSIZE(value);
+	uint32		cmplen;
+	struct varlena *result;
+	size_t		ucmplen;
+
+	cmplen = VARSIZE_ANY(value) - VARHDRSZ_4BCE;
+
+	/* Allocate space for the uncompressed data */
+	result = (struct varlena *) palloc(actual_size_exhdr + VARHDRSZ);
+
+	ucmplen = ZSTD_decompress(VARDATA(result),
+							  actual_size_exhdr,
+							  VARDATA_4BCE(value),
+							  cmplen);
+
+	if (ZSTD_isError(ucmplen))
+	{
+		pfree(result);
+		ZSTD_CHECK_ERROR(ucmplen, "ZSTD decompression failed");
+	}
+
+	/* Set final size in the varlena header */
+	SET_VARSIZE(result, ucmplen + VARHDRSZ);
+	return result;
+
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+	return NULL;
+#endif
+}
+
+/* Decompress a slice of the datum */
+struct varlena *
+zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength)
+{
+#ifdef USE_ZSTD
+	/* ZSTD no dictionary compression */
+
+	struct varlena *result;
+	ZSTD_inBuffer inBuf;
+	ZSTD_outBuffer outBuf;
+	size_t		ret;
+	ZSTD_DCtx  *zstdDctx = ZSTD_createDCtx();
+
+	inBuf.src = VARDATA_4BCE(value);
+	inBuf.size = VARSIZE_ANY(value) - VARHDRSZ_4BCE;
+	inBuf.pos = 0;
+
+	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
+	outBuf.dst = (char *) result + VARHDRSZ;
+	outBuf.size = slicelength;
+	outBuf.pos = 0;
+
+	/* Common decompression loop */
+	while (inBuf.pos < inBuf.size && outBuf.pos < outBuf.size)
+	{
+		ret = ZSTD_decompressStream(zstdDctx, &outBuf, &inBuf);
+		if (ZSTD_isError(ret))
+		{
+			pfree(result);
+			ZSTD_freeDCtx(zstdDctx);
+			ZSTD_CHECK_ERROR(ret, "zstd decompression failed");
+		}
+	}
+
+	Assert(outBuf.size == slicelength && outBuf.pos == slicelength);
+	SET_VARSIZE(result, outBuf.pos + VARHDRSZ);
+	ZSTD_freeDCtx(zstdDctx);
+
+	return result;
+#else
+	COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+	return NULL;
+#endif
+}
+
 /*
  * Extract compression ID from a varlena.
  *
@@ -292,10 +431,17 @@ CompressionNameToMethod(const char *compression)
 	else if (strcmp(compression, "lz4") == 0)
 	{
 #ifndef USE_LZ4
-		NO_LZ4_SUPPORT();
+		COMPRESSION_METHOD_NOT_SUPPORTED("lz4");
 #endif
 		return TOAST_LZ4_COMPRESSION;
 	}
+	else if (strcmp(compression, "zstd") == 0)
+	{
+#ifndef USE_ZSTD
+		COMPRESSION_METHOD_NOT_SUPPORTED("zstd");
+#endif
+		return TOAST_ZSTD_COMPRESSION;
+	}
 
 	return InvalidCompressionMethod;
 }
@@ -312,6 +458,8 @@ GetCompressionMethodName(char method)
 			return "pglz";
 		case TOAST_LZ4_COMPRESSION:
 			return "lz4";
+		case TOAST_ZSTD_COMPRESSION:
+			return "zstd";
 		default:
 			elog(ERROR, "invalid compression method %c", method);
 			return NULL;		/* keep compiler quiet */
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 32653af2e9e..500443a3535 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -71,6 +71,10 @@ toast_compress_datum(Datum value, char cmethod)
 			tmp = lz4_compress_datum((const struct varlena *) value);
 			cmid = TOAST_LZ4_COMPRESSION_ID;
 			break;
+		case TOAST_ZSTD_COMPRESSION:
+			tmp = zstd_compress_datum((const struct varlena *) value);
+			cmid = TOAST_ZSTD_COMPRESSION_ID;
+			break;
 		default:
 			elog(ERROR, "invalid compression method %c", cmethod);
 	}
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 3e4d5568bde..063780e56dc 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -5301,6 +5301,9 @@ pg_column_compression(PG_FUNCTION_ARGS)
 		case TOAST_LZ4_COMPRESSION_ID:
 			result = "lz4";
 			break;
+		case TOAST_ZSTD_COMPRESSION_ID:
+			result = "zstd";
+			break;
 		default:
 			elog(ERROR, "invalid compression method id %d", cmid);
 	}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f04bfedb2fd..4bc1a6029ec 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -460,6 +460,9 @@ static const struct config_enum_entry default_toast_compression_options[] = {
 	{"pglz", TOAST_PGLZ_COMPRESSION, false},
 #ifdef  USE_LZ4
 	{"lz4", TOAST_LZ4_COMPRESSION, false},
+#endif
+#ifdef  USE_ZSTD
+	{"zstd", TOAST_ZSTD_COMPRESSION, false},
 #endif
 	{NULL, 0, false}
 };
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 341f88adc87..b45b6e5f0ce 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -752,7 +752,7 @@ autovacuum_worker_slots = 16	# autovacuum worker slots to allocate
 #row_security = on
 #default_table_access_method = 'heap'
 #default_tablespace = ''		# a tablespace name, '' uses the default
-#default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
+#default_toast_compression = 'pglz'	# 'pglz' or 'lz4' or 'zstd'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
 #check_function_bodies = on
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 37432e66efd..6083ae1a6ad 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -17570,6 +17570,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 					case 'l':
 						cmname = "lz4";
 						break;
+					case 'z':
+						cmname = "zstd";
+						break;
 					default:
 						cmname = NULL;
 						break;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1d08268393e..26951f8f890 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2171,8 +2171,9 @@ describeOneTableDetails(const char *schemaname,
 			/* these strings are literal in our syntax, so not translated. */
 			printTableAddCell(&cont, (compression[0] == 'p' ? "pglz" :
 									  (compression[0] == 'l' ? "lz4" :
-									   (compression[0] == '\0' ? "" :
-										"???"))),
+									   (compression[0] == 'z' ? "zstd" :
+										(compression[0] == '\0' ? "" :
+										 "???")))),
 							  false, false);
 		}
 
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index ec65ab79fec..a3ff8c1d9ae 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -2885,7 +2885,7 @@ match_previous_words(int pattern_id,
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET COMPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "COMPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "COMPRESSION"))
-		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4");
+		COMPLETE_WITH("DEFAULT", "PGLZ", "LZ4", "ZSTD");
 	/* ALTER TABLE ALTER [COLUMN] <foo> SET EXPRESSION */
 	else if (Matches("ALTER", "TABLE", MatchAny, "ALTER", "COLUMN", MatchAny, "SET", "EXPRESSION") ||
 			 Matches("ALTER", "TABLE", MatchAny, "ALTER", MatchAny, "SET", "EXPRESSION"))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 62b77edf372..0d7b521c481 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -13,6 +13,10 @@
 #ifndef TOAST_COMPRESSION_H
 #define TOAST_COMPRESSION_H
 
+#ifdef USE_ZSTD
+#include <zstd.h>
+#endif
+
 /*
  * GUC support.
  *
@@ -43,7 +47,8 @@ typedef enum ToastCompressionId
 {
 	TOAST_PGLZ_COMPRESSION_ID = 0,
 	TOAST_LZ4_COMPRESSION_ID = 1,
-	TOAST_INVALID_COMPRESSION_ID = 2,
+	TOAST_ZSTD_COMPRESSION_ID = 2,
+	TOAST_INVALID_COMPRESSION_ID = 3,
 } ToastCompressionId;
 
 /*
@@ -53,6 +58,7 @@ typedef enum ToastCompressionId
  */
 #define TOAST_PGLZ_COMPRESSION			'p'
 #define TOAST_LZ4_COMPRESSION			'l'
+#define TOAST_ZSTD_COMPRESSION			'z'
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
@@ -73,6 +79,11 @@ extern struct varlena *lz4_decompress_datum(const struct varlena *value);
 extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value,
 												  int32 slicelength);
 
+/* zstd nodict compression/decompression routines */
+extern struct varlena *zstd_compress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum(const struct varlena *value);
+extern struct varlena *zstd_decompress_datum_slice(const struct varlena *value, int32 slicelength);
+
 /* other stuff */
 extern ToastCompressionId toast_get_compression_id(struct varlena *attr);
 extern char CompressionNameToMethod(const char *compression);
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 857b53431c8..35277086f52 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -25,7 +25,8 @@
 	do {																														\
 		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);																		\
 		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||																		\
-				(cm_method) == TOAST_LZ4_COMPRESSION_ID);																		\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID ||																		\
+				(cm_method) == TOAST_ZSTD_COMPRESSION_ID);																		\
 		if (!TOAST_CMPID_EXTENDED((cm_method)))																					\
 			((varattrib_4b *)(ptr))->va_compressed.va_tcinfo = ((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS);	\
 		else																													\
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 39dcfc4b4b8..4ca8dac814f 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -401,7 +401,8 @@ typedef struct
 #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm)						\
 	do {																							\
 		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID ||													\
-				(cm) == TOAST_LZ4_COMPRESSION_ID);													\
+				(cm) == TOAST_LZ4_COMPRESSION_ID ||													\
+				(cm) == TOAST_ZSTD_COMPRESSION_ID);													\
 		if (!TOAST_CMPID_EXTENDED((cm)))															\
 			/* method fits in the low bits of va_extinfo */											\
 			(toast_pointer).va_extinfo = (uint32)(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS);	\
diff --git a/src/test/regress/expected/compression_zstd.out b/src/test/regress/expected/compression_zstd.out
new file mode 100644
index 00000000000..97222b20a28
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd.out
@@ -0,0 +1,376 @@
+SELECT NOT(pg_compression_available('zstd')) AS skip_test \gset
+\if :skip_test
+   \echo '*** skipping zstd tests (zstd not available) ***'
+   \quit
+\endif
+CREATE SCHEMA zstd;
+SET search_path TO zstd, public;
+\set HIDE_TOAST_COMPRESSION false
+-- ensure we get stable results regardless of installation's default
+SET default_toast_compression = 'zstd';
+-- test creating table with compression method
+CREATE TABLE cmdata(f1 text COMPRESSION pglz);
+CREATE INDEX idx ON cmdata(f1);
+INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
+\d+ cmdata
+                                         Table "zstd.cmdata"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | pglz        |              | 
+Indexes:
+    "idx" btree (f1)
+
+CREATE TABLE cmdata1(f1 TEXT COMPRESSION zstd);
+INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));  -- inline
+INSERT INTO cmdata1 VALUES (repeat('1234567890', 2500000)); -- externally stored
+\d+ cmdata1
+                                         Table "zstd.cmdata1"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+-- verify stored compression method in the data
+SELECT pg_column_compression(f1) FROM cmdata;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+SELECT pg_column_compression(f1) FROM cmdata1;
+ pg_column_compression 
+-----------------------
+ zstd
+ zstd
+(2 rows)
+
+-- decompress data slice
+SELECT SUBSTR(f1, 200, 5) FROM cmdata;
+ substr 
+--------
+ 01234
+(1 row)
+
+SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
+                       substr                       
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+ 01234567890123456789012345678901234567890123456789
+(2 rows)
+
+-- copy with table creation
+SELECT * INTO cmmove1 FROM cmdata;
+\d+ cmmove1
+                                         Table "zstd.cmmove1"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+
+SELECT pg_column_compression(f1) FROM cmmove1;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+-- copy to existing table
+CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
+INSERT INTO cmmove3 SELECT * FROM cmdata;
+INSERT INTO cmmove3 SELECT * FROM cmdata1;
+SELECT pg_column_compression(f1) FROM cmmove3;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+ zstd
+(3 rows)
+
+-- test LIKE INCLUDING COMPRESSION
+CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
+\d+ cmdata2
+                                         Table "zstd.cmdata2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+
+DROP TABLE cmdata2;
+-- try setting compression for incompressible data type
+CREATE TABLE cmdata2 (f1 int COMPRESSION pglz);
+ERROR:  column data type integer does not support compression
+-- update using datum from different table
+CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
+INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmmove2;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
+SELECT pg_column_compression(f1) FROM cmmove2;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+-- test externally stored compressed data
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
+CREATE TABLE cmdata2 (f1 text COMPRESSION pglz);
+INSERT INTO cmdata2 SELECT large_val() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata2;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata1;
+ pg_column_compression 
+-----------------------
+ zstd
+ zstd
+ zstd
+(3 rows)
+
+SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
+ substr 
+--------
+ 01234
+ 01234
+ 79026
+(3 rows)
+
+SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
+ substr 
+--------
+ 79026
+(1 row)
+
+DROP TABLE cmdata2;
+--test column type update varlena/non-varlena
+CREATE TABLE cmdata2 (f1 int);
+\d+ cmdata2
+                                          Table "zstd.cmdata2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
+ f1     | integer |           |          |         | plain   |             |              | 
+
+ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
+\d+ cmdata2
+                                               Table "zstd.cmdata2"
+ Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | character varying |           |          |         | extended |             |              | 
+
+ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
+\d+ cmdata2
+                                          Table "zstd.cmdata2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
+ f1     | integer |           |          |         | plain   |             |              | 
+
+--changing column storage should not impact the compression method
+--but the data should not be compressed
+ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
+ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz;
+\d+ cmdata2
+                                               Table "zstd.cmdata2"
+ Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | character varying |           |          |         | extended | pglz        |              | 
+
+ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain;
+\d+ cmdata2
+                                               Table "zstd.cmdata2"
+ Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
+--------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
+ f1     | character varying |           |          |         | plain   | pglz        |              | 
+
+INSERT INTO cmdata2 VALUES (repeat('123456789', 800));
+SELECT pg_column_compression(f1) FROM cmdata2;
+ pg_column_compression 
+-----------------------
+ 
+(1 row)
+
+-- test compression with materialized view
+CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
+\d+ compressmv
+                                 Materialized view "zstd.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata1;
+
+SELECT pg_column_compression(f1) FROM cmdata1;
+ pg_column_compression 
+-----------------------
+ zstd
+ zstd
+ zstd
+(3 rows)
+
+SELECT pg_column_compression(x) FROM compressmv;
+ pg_column_compression 
+-----------------------
+ zstd
+ zstd
+ zstd
+(3 rows)
+
+-- test compression with partition
+CREATE TABLE cmpart(f1 text COMPRESSION zstd) PARTITION BY HASH(f1);
+CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
+ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+ pg_column_compression 
+-----------------------
+ zstd
+(1 row)
+
+SELECT pg_column_compression(f1) FROM cmpart2;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+-- test compression with inheritance
+CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
+NOTICE:  merging multiple inherited definitions of column "f1"
+ERROR:  column "f1" has a compression method conflict
+DETAIL:  pglz versus zstd
+CREATE TABLE cminh(f1 TEXT COMPRESSION zstd) INHERITS(cmdata); -- error
+NOTICE:  merging column "f1" with inherited definition
+ERROR:  column "f1" has a compression method conflict
+DETAIL:  pglz versus zstd
+CREATE TABLE cmdata3(f1 text);
+CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
+NOTICE:  merging multiple inherited definitions of column "f1"
+-- test default_toast_compression GUC
+SET default_toast_compression = 'zstd';
+-- test alter compression method
+ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION zstd;
+INSERT INTO cmdata VALUES (repeat('123456789', 4004));
+\d+ cmdata
+                                         Table "zstd.cmdata"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | zstd        |              | 
+Indexes:
+    "idx" btree (f1)
+Child tables: cminh
+
+SELECT pg_column_compression(f1) FROM cmdata;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
+\d+ cmdata2
+                                               Table "zstd.cmdata2"
+ Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
+--------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
+ f1     | character varying |           |          |         | plain   |             |              | 
+
+-- test alter compression method for materialized views
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION zstd;
+\d+ compressmv
+                                 Materialized view "zstd.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended | zstd        |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata1;
+
+-- test alter compression method for partitioned tables
+ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION zstd;
+-- new data should be compressed with the current compression method
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+ pg_column_compression 
+-----------------------
+ zstd
+ pglz
+(2 rows)
+
+SELECT pg_column_compression(f1) FROM cmpart2;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+-- VACUUM FULL does not recompress
+SELECT pg_column_compression(f1) FROM cmdata;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+VACUUM FULL cmdata;
+SELECT pg_column_compression(f1) FROM cmdata;
+ pg_column_compression 
+-----------------------
+ pglz
+ zstd
+(2 rows)
+
+-- test expression index
+DROP TABLE cmdata2;
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION zstd);
+CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
+INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
+generate_series(1, 50) g), VERSION());
+-- check data is ok
+SELECT length(f1) FROM cmdata;
+ length 
+--------
+  10000
+  36036
+(2 rows)
+
+SELECT length(f1) FROM cmdata1;
+  length  
+----------
+    10040
+ 25000000
+    12449
+(3 rows)
+
+SELECT length(f1) FROM cmmove1;
+ length 
+--------
+  10000
+(1 row)
+
+SELECT length(f1) FROM cmmove2;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove3;
+  length  
+----------
+    10000
+    10040
+ 25000000
+(3 rows)
+
+CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails
+ERROR:  invalid compression method "i_do_not_exist_compression"
+CREATE TABLE badcompresstbl (a text);
+ALTER TABLE badcompresstbl ALTER a SET COMPRESSION I_Do_Not_Exist_Compression; -- fails
+ERROR:  invalid compression method "i_do_not_exist_compression"
+DROP TABLE badcompresstbl;
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_zstd_1.out b/src/test/regress/expected/compression_zstd_1.out
new file mode 100644
index 00000000000..6ad1a812533
--- /dev/null
+++ b/src/test/regress/expected/compression_zstd_1.out
@@ -0,0 +1,5 @@
+SELECT NOT(pg_compression_available('zstd')) AS skip_test \gset
+\if :skip_test
+   \echo '*** skipping zstd tests (zstd not available) ***'
+*** skipping zstd tests (zstd not available) ***
+   \quit
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fbffc67ae60..1ef4797cd10 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 compression_zstd memoize stats predicate numa
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression_zstd.sql b/src/test/regress/sql/compression_zstd.sql
new file mode 100644
index 00000000000..ec709387a17
--- /dev/null
+++ b/src/test/regress/sql/compression_zstd.sql
@@ -0,0 +1,162 @@
+SELECT NOT(pg_compression_available('zstd')) AS skip_test \gset
+\if :skip_test
+   \echo '*** skipping zstd tests (zstd not available) ***'
+   \quit
+\endif
+
+CREATE SCHEMA zstd;
+SET search_path TO zstd, public;
+
+\set HIDE_TOAST_COMPRESSION false
+
+-- ensure we get stable results regardless of installation's default
+SET default_toast_compression = 'zstd';
+
+-- test creating table with compression method
+CREATE TABLE cmdata(f1 text COMPRESSION pglz);
+CREATE INDEX idx ON cmdata(f1);
+INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
+\d+ cmdata
+CREATE TABLE cmdata1(f1 TEXT COMPRESSION zstd);
+INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));  -- inline
+INSERT INTO cmdata1 VALUES (repeat('1234567890', 2500000)); -- externally stored
+\d+ cmdata1
+
+-- verify stored compression method in the data
+SELECT pg_column_compression(f1) FROM cmdata;
+SELECT pg_column_compression(f1) FROM cmdata1;
+
+-- decompress data slice
+SELECT SUBSTR(f1, 200, 5) FROM cmdata;
+SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
+
+-- copy with table creation
+SELECT * INTO cmmove1 FROM cmdata;
+\d+ cmmove1
+SELECT pg_column_compression(f1) FROM cmmove1;
+
+-- copy to existing table
+CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
+INSERT INTO cmmove3 SELECT * FROM cmdata;
+INSERT INTO cmmove3 SELECT * FROM cmdata1;
+SELECT pg_column_compression(f1) FROM cmmove3;
+
+-- test LIKE INCLUDING COMPRESSION
+CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
+\d+ cmdata2
+DROP TABLE cmdata2;
+
+-- try setting compression for incompressible data type
+CREATE TABLE cmdata2 (f1 int COMPRESSION pglz);
+
+-- update using datum from different table
+CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
+INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmmove2;
+UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
+SELECT pg_column_compression(f1) FROM cmmove2;
+
+-- test externally stored compressed data
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
+CREATE TABLE cmdata2 (f1 text COMPRESSION pglz);
+INSERT INTO cmdata2 SELECT large_val() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata2;
+INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata1;
+SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
+SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
+DROP TABLE cmdata2;
+
+--test column type update varlena/non-varlena
+CREATE TABLE cmdata2 (f1 int);
+\d+ cmdata2
+ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
+\d+ cmdata2
+ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
+\d+ cmdata2
+
+--changing column storage should not impact the compression method
+--but the data should not be compressed
+ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
+ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz;
+\d+ cmdata2
+ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain;
+\d+ cmdata2
+INSERT INTO cmdata2 VALUES (repeat('123456789', 800));
+SELECT pg_column_compression(f1) FROM cmdata2;
+
+-- test compression with materialized view
+CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
+\d+ compressmv
+SELECT pg_column_compression(f1) FROM cmdata1;
+SELECT pg_column_compression(x) FROM compressmv;
+
+-- test compression with partition
+CREATE TABLE cmpart(f1 text COMPRESSION zstd) PARTITION BY HASH(f1);
+CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
+
+ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+SELECT pg_column_compression(f1) FROM cmpart2;
+
+-- test compression with inheritance
+CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
+CREATE TABLE cminh(f1 TEXT COMPRESSION zstd) INHERITS(cmdata); -- error
+CREATE TABLE cmdata3(f1 text);
+CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
+
+-- test default_toast_compression GUC
+SET default_toast_compression = 'zstd';
+
+-- test alter compression method
+ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION zstd;
+INSERT INTO cmdata VALUES (repeat('123456789', 4004));
+\d+ cmdata
+SELECT pg_column_compression(f1) FROM cmdata;
+
+ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
+\d+ cmdata2
+
+-- test alter compression method for materialized views
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION zstd;
+\d+ compressmv
+
+-- test alter compression method for partitioned tables
+ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION zstd;
+
+-- new data should be compressed with the current compression method
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+SELECT pg_column_compression(f1) FROM cmpart2;
+
+-- VACUUM FULL does not recompress
+SELECT pg_column_compression(f1) FROM cmdata;
+VACUUM FULL cmdata;
+SELECT pg_column_compression(f1) FROM cmdata;
+
+-- test expression index
+DROP TABLE cmdata2;
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION zstd);
+CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
+INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
+generate_series(1, 50) g), VERSION());
+
+-- check data is ok
+SELECT length(f1) FROM cmdata;
+SELECT length(f1) FROM cmdata1;
+SELECT length(f1) FROM cmmove1;
+SELECT length(f1) FROM cmmove2;
+SELECT length(f1) FROM cmmove3;
+
+CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails
+CREATE TABLE badcompresstbl (a text);
+ALTER TABLE badcompresstbl ALTER a SET COMPRESSION I_Do_Not_Exist_Compression; -- fails
+DROP TABLE badcompresstbl;
+
+\set HIDE_TOAST_COMPRESSION true
-- 
2.47.1

v25-0001-Add-pg_compression_available-and-split-sql-compr.patchapplication/octet-stream; name=v25-0001-Add-pg_compression_available-and-split-sql-compr.patchDownload
From 747814af420e69647c6cbc0447d1c37ec53db80b Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Tue, 3 Jun 2025 11:35:24 +0000
Subject: [PATCH v25 1/3] Add pg_compression_available and split sql
 compression tests - Introduce new SQL function
 `pg_compression_available(text)`: - Returns true/false depending on whether
 the given TOAST compression method (e.g. 'pglz', 'lz4', 'zstd') is supported
 in the current build.

---
 doc/src/sgml/func.sgml                        |  20 ++
 src/backend/access/common/toast_compression.c |  32 +++
 src/include/catalog/pg_proc.dat               |   5 +
 src/test/regress/expected/compression.out     |  93 ++++----
 ...{compression_1.out => compression_lz4.out} | 216 +++++++++---------
 .../regress/expected/compression_lz4_1.out    |   5 +
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression.sql          |  20 +-
 src/test/regress/sql/compression_lz4.sql      | 161 +++++++++++++
 9 files changed, 384 insertions(+), 170 deletions(-)
 rename src/test/regress/expected/{compression_1.out => compression_lz4.out} (66%)
 create mode 100644 src/test/regress/expected/compression_lz4_1.out
 create mode 100644 src/test/regress/sql/compression_lz4.sql

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index c67688cbf5f..56284237df7 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25288,6 +25288,26 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
         This is equivalent to <function>current_user</function>.
        </para></entry>
       </row>
+      <row>
+        <entry role="func_table_entry">
+          <para role="func_signature">
+            <indexterm>
+              <primary>pg_compression_available</primary>
+            </indexterm>
+            <function>pg_compression_available</function> ( <type>text</type> )
+            <returnvalue>boolean</returnvalue>
+          </para>
+          <para>
+            Returns <literal>true</literal> if the given compression method name is supported in this PostgreSQL build.
+            The built-in compression method <literal>pglz</literal> is always available because it is included
+            in the core server code and does not require any additional compile-time option or external library.
+            In contrast, <literal>lz4</literal> will return <literal>true</literal> only if PostgreSQL was
+            compiled with the <literal>--with-lz4</literal> flag, and <literal>zstd</literal> will return
+            <literal>true</literal> only if compiled with the <literal>--with-zstd</literal> flag.
+            If the input name does not match any supported method, the function returns <literal>false</literal>.
+          </para>
+        </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c
index 21f2f4af97e..fb93555bdb0 100644
--- a/src/backend/access/common/toast_compression.c
+++ b/src/backend/access/common/toast_compression.c
@@ -21,6 +21,9 @@
 #include "access/toast_compression.h"
 #include "common/pg_lzcompress.h"
 #include "varatt.h"
+#include "fmgr.h"
+#include "parser/scansup.h"
+#include "utils/builtins.h"
 
 /* GUC */
 int			default_toast_compression = TOAST_PGLZ_COMPRESSION;
@@ -314,3 +317,32 @@ GetCompressionMethodName(char method)
 			return NULL;		/* keep compiler quiet */
 	}
 }
+
+/*
+ * pg_compression_available(text) → bool
+ *
+ * True if the named TOAST compressor method was compiled into this server.
+ */
+Datum
+pg_compression_available(PG_FUNCTION_ARGS)
+{
+	text	   *name = PG_GETARG_TEXT_PP(0);
+	char	   *cname = downcase_truncate_identifier(text_to_cstring(name),
+													 NAMEDATALEN, false);
+
+	/* pglz is always there */
+	if (strcmp(cname, "pglz") == 0)
+		PG_RETURN_BOOL(true);
+
+#ifdef USE_LZ4
+	if (strcmp(cname, "lz4") == 0)
+		PG_RETURN_BOOL(true);
+#endif
+
+#ifdef USE_ZSTD
+	if (strcmp(cname, "zstd") == 0)
+		PG_RETURN_BOOL(true);
+#endif
+
+	PG_RETURN_BOOL(false);
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d3d28a263fa..0a1ef15b481 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12556,4 +12556,9 @@
   proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}',
   prosrc => 'pg_get_aios' },
 
+{ oid =>  9474, descr => 'is toast compression method available?',
+  proname => 'pg_compression_available', prokind => 'f',
+  provolatile => 'i', prorettype => 'bool', proargtypes => 'text',
+  prosrc => 'pg_compression_available' },
+
 ]
diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 4dd9ee7200d..84b4d0b39d4 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -1,3 +1,5 @@
+CREATE SCHEMA pglz;
+SET search_path TO pglz, public;
 \set HIDE_TOAST_COMPRESSION false
 -- ensure we get stable results regardless of installation's default
 SET default_toast_compression = 'pglz';
@@ -6,20 +8,20 @@ CREATE TABLE cmdata(f1 text COMPRESSION pglz);
 CREATE INDEX idx ON cmdata(f1);
 INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
 \d+ cmdata
-                                        Table "public.cmdata"
+                                         Table "pglz.cmdata"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | text |           |          |         | extended | pglz        |              | 
 Indexes:
     "idx" btree (f1)
 
-CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4);
+CREATE TABLE cmdata1(f1 TEXT COMPRESSION pglz);
 INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
 \d+ cmdata1
-                                        Table "public.cmdata1"
+                                         Table "pglz.cmdata1"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended | lz4         |              | 
+ f1     | text |           |          |         | extended | pglz        |              | 
 
 -- verify stored compression method in the data
 SELECT pg_column_compression(f1) FROM cmdata;
@@ -31,7 +33,7 @@ SELECT pg_column_compression(f1) FROM cmdata;
 SELECT pg_column_compression(f1) FROM cmdata1;
  pg_column_compression 
 -----------------------
- lz4
+ pglz
 (1 row)
 
 -- decompress data slice
@@ -50,7 +52,7 @@ SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
 -- copy with table creation
 SELECT * INTO cmmove1 FROM cmdata;
 \d+ cmmove1
-                                        Table "public.cmmove1"
+                                         Table "pglz.cmmove1"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | text |           |          |         | extended |             |              | 
@@ -69,16 +71,16 @@ SELECT pg_column_compression(f1) FROM cmmove3;
  pg_column_compression 
 -----------------------
  pglz
- lz4
+ pglz
 (2 rows)
 
 -- test LIKE INCLUDING COMPRESSION
 CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
 \d+ cmdata2
-                                        Table "public.cmdata2"
+                                         Table "pglz.cmdata2"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended | lz4         |              | 
+ f1     | text |           |          |         | extended | pglz        |              | 
 
 DROP TABLE cmdata2;
 -- try setting compression for incompressible data type
@@ -97,7 +99,7 @@ UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
 SELECT pg_column_compression(f1) FROM cmmove2;
  pg_column_compression 
 -----------------------
- lz4
+ pglz
 (1 row)
 
 -- test externally stored compressed data
@@ -115,8 +117,8 @@ INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
 SELECT pg_column_compression(f1) FROM cmdata1;
  pg_column_compression 
 -----------------------
- lz4
- lz4
+ pglz
+ pglz
 (2 rows)
 
 SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
@@ -136,21 +138,21 @@ DROP TABLE cmdata2;
 --test column type update varlena/non-varlena
 CREATE TABLE cmdata2 (f1 int);
 \d+ cmdata2
-                                         Table "public.cmdata2"
+                                          Table "pglz.cmdata2"
  Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | integer |           |          |         | plain   |             |              | 
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "pglz.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | character varying |           |          |         | extended |             |              | 
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
 \d+ cmdata2
-                                         Table "public.cmdata2"
+                                          Table "pglz.cmdata2"
  Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | integer |           |          |         | plain   |             |              | 
@@ -160,14 +162,14 @@ ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
 ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "pglz.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | character varying |           |          |         | extended | pglz        |              | 
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "pglz.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | character varying |           |          |         | plain   | pglz        |              | 
@@ -182,7 +184,7 @@ SELECT pg_column_compression(f1) FROM cmdata2;
 -- test compression with materialized view
 CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
 \d+ compressmv
-                                Materialized view "public.compressmv"
+                                 Materialized view "pglz.compressmv"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
  x      | text |           |          |         | extended |             |              | 
@@ -193,19 +195,19 @@ View definition:
 SELECT pg_column_compression(f1) FROM cmdata1;
  pg_column_compression 
 -----------------------
- lz4
- lz4
+ pglz
+ pglz
 (2 rows)
 
 SELECT pg_column_compression(x) FROM compressmv;
  pg_column_compression 
 -----------------------
- lz4
- lz4
+ pglz
+ pglz
 (2 rows)
 
 -- test compression with partition
-CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1);
+CREATE TABLE cmpart(f1 text COMPRESSION pglz) PARTITION BY HASH(f1);
 CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
 CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
 ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
@@ -214,7 +216,7 @@ INSERT INTO cmpart VALUES (repeat('123456789', 4004));
 SELECT pg_column_compression(f1) FROM cmpart1;
  pg_column_compression 
 -----------------------
- lz4
+ pglz
 (1 row)
 
 SELECT pg_column_compression(f1) FROM cmpart2;
@@ -224,34 +226,19 @@ SELECT pg_column_compression(f1) FROM cmpart2;
 (1 row)
 
 -- test compression with inheritance
-CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
-NOTICE:  merging multiple inherited definitions of column "f1"
-ERROR:  column "f1" has a compression method conflict
-DETAIL:  pglz versus lz4
-CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error
-NOTICE:  merging column "f1" with inherited definition
-ERROR:  column "f1" has a compression method conflict
-DETAIL:  pglz versus lz4
 CREATE TABLE cmdata3(f1 text);
 CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
-SET default_toast_compression = '';
-ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz, lz4.
-SET default_toast_compression = 'I do not exist compression';
-ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz, lz4.
-SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 -- test alter compression method
-ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4;
+ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION pglz;
 INSERT INTO cmdata VALUES (repeat('123456789', 4004));
 \d+ cmdata
-                                        Table "public.cmdata"
+                                         Table "pglz.cmdata"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended | lz4         |              | 
+ f1     | text |           |          |         | extended | pglz        |              | 
 Indexes:
     "idx" btree (f1)
 Child tables: cminh
@@ -260,37 +247,37 @@ SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
 -----------------------
  pglz
- lz4
+ pglz
 (2 rows)
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "pglz.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | character varying |           |          |         | plain   |             |              | 
 
 -- test alter compression method for materialized views
-ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4;
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION pglz;
 \d+ compressmv
-                                Materialized view "public.compressmv"
+                                 Materialized view "pglz.compressmv"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- x      | text |           |          |         | extended | lz4         |              | 
+ x      | text |           |          |         | extended | pglz        |              | 
 View definition:
  SELECT f1 AS x
    FROM cmdata1;
 
 -- test alter compression method for partitioned tables
 ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
-ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION pglz;
 -- new data should be compressed with the current compression method
 INSERT INTO cmpart VALUES (repeat('123456789', 1004));
 INSERT INTO cmpart VALUES (repeat('123456789', 4004));
 SELECT pg_column_compression(f1) FROM cmpart1;
  pg_column_compression 
 -----------------------
- lz4
+ pglz
  pglz
 (2 rows)
 
@@ -298,7 +285,7 @@ SELECT pg_column_compression(f1) FROM cmpart2;
  pg_column_compression 
 -----------------------
  pglz
- lz4
+ pglz
 (2 rows)
 
 -- VACUUM FULL does not recompress
@@ -306,7 +293,7 @@ SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
 -----------------------
  pglz
- lz4
+ pglz
 (2 rows)
 
 VACUUM FULL cmdata;
@@ -314,12 +301,12 @@ SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
 -----------------------
  pglz
- lz4
+ pglz
 (2 rows)
 
 -- test expression index
 DROP TABLE cmdata2;
-CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4);
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION pglz);
 CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
 INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
 generate_series(1, 50) g), VERSION());
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_lz4.out
similarity index 66%
rename from src/test/regress/expected/compression_1.out
rename to src/test/regress/expected/compression_lz4.out
index 7bd7642b4b9..d4970ffe5d4 100644
--- a/src/test/regress/expected/compression_1.out
+++ b/src/test/regress/expected/compression_lz4.out
@@ -1,12 +1,19 @@
+SELECT NOT(pg_compression_available('lz4')) AS skip_test \gset
+\if :skip_test
+   \echo '*** skipping lz4 tests (lz4 not available) ***'
+   \quit
+\endif
+CREATE SCHEMA lz4;
+SET search_path TO lz4, public;
 \set HIDE_TOAST_COMPRESSION false
 -- ensure we get stable results regardless of installation's default
-SET default_toast_compression = 'pglz';
+SET default_toast_compression = 'lz4';
 -- test creating table with compression method
 CREATE TABLE cmdata(f1 text COMPRESSION pglz);
 CREATE INDEX idx ON cmdata(f1);
 INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
 \d+ cmdata
-                                        Table "public.cmdata"
+                                          Table "lz4.cmdata"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | text |           |          |         | extended | pglz        |              | 
@@ -14,13 +21,13 @@ Indexes:
     "idx" btree (f1)
 
 CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4);
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
 INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
-ERROR:  relation "cmdata1" does not exist
-LINE 1: INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
-                    ^
 \d+ cmdata1
+                                         Table "lz4.cmdata1"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | lz4         |              | 
+
 -- verify stored compression method in the data
 SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
@@ -29,9 +36,11 @@ SELECT pg_column_compression(f1) FROM cmdata;
 (1 row)
 
 SELECT pg_column_compression(f1) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmdata1;
-                                              ^
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
 -- decompress data slice
 SELECT SUBSTR(f1, 200, 5) FROM cmdata;
  substr 
@@ -40,13 +49,15 @@ SELECT SUBSTR(f1, 200, 5) FROM cmdata;
 (1 row)
 
 SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
-                                         ^
+                       substr                       
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+(1 row)
+
 -- copy with table creation
 SELECT * INTO cmmove1 FROM cmdata;
 \d+ cmmove1
-                                        Table "public.cmmove1"
+                                         Table "lz4.cmmove1"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | text |           |          |         | extended |             |              | 
@@ -61,23 +72,22 @@ SELECT pg_column_compression(f1) FROM cmmove1;
 CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
 INSERT INTO cmmove3 SELECT * FROM cmdata;
 INSERT INTO cmmove3 SELECT * FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: INSERT INTO cmmove3 SELECT * FROM cmdata1;
-                                          ^
 SELECT pg_column_compression(f1) FROM cmmove3;
  pg_column_compression 
 -----------------------
  pglz
-(1 row)
+ lz4
+(2 rows)
 
 -- test LIKE INCLUDING COMPRESSION
 CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
-ERROR:  relation "cmdata1" does not exist
-LINE 1: CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
-                                   ^
 \d+ cmdata2
+                                         Table "lz4.cmdata2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | lz4         |              | 
+
 DROP TABLE cmdata2;
-ERROR:  table "cmdata2" does not exist
 -- try setting compression for incompressible data type
 CREATE TABLE cmdata2 (f1 int COMPRESSION pglz);
 ERROR:  column data type integer does not support compression
@@ -91,13 +101,10 @@ SELECT pg_column_compression(f1) FROM cmmove2;
 (1 row)
 
 UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
-                                                ^
 SELECT pg_column_compression(f1) FROM cmmove2;
  pg_column_compression 
 -----------------------
- pglz
+ lz4
 (1 row)
 
 -- test externally stored compressed data
@@ -112,17 +119,20 @@ SELECT pg_column_compression(f1) FROM cmdata2;
 (1 row)
 
 INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
-ERROR:  relation "cmdata1" does not exist
-LINE 1: INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
-                    ^
 SELECT pg_column_compression(f1) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmdata1;
-                                              ^
+ pg_column_compression 
+-----------------------
+ lz4
+ lz4
+(2 rows)
+
 SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
-                                       ^
+ substr 
+--------
+ 01234
+ 79026
+(2 rows)
+
 SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
  substr 
 --------
@@ -133,21 +143,21 @@ DROP TABLE cmdata2;
 --test column type update varlena/non-varlena
 CREATE TABLE cmdata2 (f1 int);
 \d+ cmdata2
-                                         Table "public.cmdata2"
+                                          Table "lz4.cmdata2"
  Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | integer |           |          |         | plain   |             |              | 
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                                Table "lz4.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | character varying |           |          |         | extended |             |              | 
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
 \d+ cmdata2
-                                         Table "public.cmdata2"
+                                          Table "lz4.cmdata2"
  Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | integer |           |          |         | plain   |             |              | 
@@ -157,14 +167,14 @@ ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
 ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                                Table "lz4.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | character varying |           |          |         | extended | pglz        |              | 
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "lz4.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | character varying |           |          |         | plain   | pglz        |              | 
@@ -178,47 +188,53 @@ SELECT pg_column_compression(f1) FROM cmdata2;
 
 -- test compression with materialized view
 CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: ...TE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
-                                                               ^
 \d+ compressmv
+                                  Materialized view "lz4.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata1;
+
 SELECT pg_column_compression(f1) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmdata1;
-                                              ^
+ pg_column_compression 
+-----------------------
+ lz4
+ lz4
+(2 rows)
+
 SELECT pg_column_compression(x) FROM compressmv;
-ERROR:  relation "compressmv" does not exist
-LINE 1: SELECT pg_column_compression(x) FROM compressmv;
-                                             ^
+ pg_column_compression 
+-----------------------
+ lz4
+ lz4
+(2 rows)
+
 -- test compression with partition
 CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1);
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
 CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
-ERROR:  relation "cmpart" does not exist
 CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
 ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
-ERROR:  relation "cmpart" does not exist
 INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-ERROR:  relation "cmpart" does not exist
-LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-                    ^
 INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-ERROR:  relation "cmpart" does not exist
-LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-                    ^
 SELECT pg_column_compression(f1) FROM cmpart1;
-ERROR:  relation "cmpart1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmpart1;
-                                              ^
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
 SELECT pg_column_compression(f1) FROM cmpart2;
  pg_column_compression 
 -----------------------
-(0 rows)
+ pglz
+(1 row)
 
 -- test compression with inheritance
 CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
-ERROR:  relation "cmdata1" does not exist
+NOTICE:  merging multiple inherited definitions of column "f1"
+ERROR:  column "f1" has a compression method conflict
+DETAIL:  pglz versus lz4
 CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error
 NOTICE:  merging column "f1" with inherited definition
 ERROR:  column "f1" has a compression method conflict
@@ -227,26 +243,15 @@ CREATE TABLE cmdata3(f1 text);
 CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
-SET default_toast_compression = '';
-ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz.
-SET default_toast_compression = 'I do not exist compression';
-ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz.
 SET default_toast_compression = 'lz4';
-ERROR:  invalid value for parameter "default_toast_compression": "lz4"
-HINT:  Available values: pglz.
-SET default_toast_compression = 'pglz';
 -- test alter compression method
 ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4;
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
 INSERT INTO cmdata VALUES (repeat('123456789', 4004));
 \d+ cmdata
-                                        Table "public.cmdata"
+                                          Table "lz4.cmdata"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended | pglz        |              | 
+ f1     | text |           |          |         | extended | lz4         |              | 
 Indexes:
     "idx" btree (f1)
 Child tables: cminh
@@ -255,50 +260,53 @@ SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
 -----------------------
  pglz
- pglz
+ lz4
 (2 rows)
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "lz4.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | character varying |           |          |         | plain   |             |              | 
 
 -- test alter compression method for materialized views
 ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4;
-ERROR:  relation "compressmv" does not exist
 \d+ compressmv
+                                  Materialized view "lz4.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended | lz4         |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata1;
+
 -- test alter compression method for partitioned tables
 ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
-ERROR:  relation "cmpart1" does not exist
 ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4;
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
 -- new data should be compressed with the current compression method
 INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-ERROR:  relation "cmpart" does not exist
-LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-                    ^
 INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-ERROR:  relation "cmpart" does not exist
-LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-                    ^
 SELECT pg_column_compression(f1) FROM cmpart1;
-ERROR:  relation "cmpart1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmpart1;
-                                              ^
+ pg_column_compression 
+-----------------------
+ lz4
+ pglz
+(2 rows)
+
 SELECT pg_column_compression(f1) FROM cmpart2;
  pg_column_compression 
 -----------------------
-(0 rows)
+ pglz
+ lz4
+(2 rows)
 
 -- VACUUM FULL does not recompress
 SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
 -----------------------
  pglz
- pglz
+ lz4
 (2 rows)
 
 VACUUM FULL cmdata;
@@ -306,21 +314,15 @@ SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
 -----------------------
  pglz
- pglz
+ lz4
 (2 rows)
 
 -- test expression index
 DROP TABLE cmdata2;
 CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4);
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
 CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
-ERROR:  relation "cmdata2" does not exist
 INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
 generate_series(1, 50) g), VERSION());
-ERROR:  relation "cmdata2" does not exist
-LINE 1: INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEX...
-                    ^
 -- check data is ok
 SELECT length(f1) FROM cmdata;
  length 
@@ -330,9 +332,12 @@ SELECT length(f1) FROM cmdata;
 (2 rows)
 
 SELECT length(f1) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT length(f1) FROM cmdata1;
-                               ^
+ length 
+--------
+  10040
+  12449
+(2 rows)
+
 SELECT length(f1) FROM cmmove1;
  length 
 --------
@@ -349,7 +354,8 @@ SELECT length(f1) FROM cmmove3;
  length 
 --------
   10000
-(1 row)
+  10040
+(2 rows)
 
 CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails
 ERROR:  invalid compression method "i_do_not_exist_compression"
diff --git a/src/test/regress/expected/compression_lz4_1.out b/src/test/regress/expected/compression_lz4_1.out
new file mode 100644
index 00000000000..199b4c4abd0
--- /dev/null
+++ b/src/test/regress/expected/compression_lz4_1.out
@@ -0,0 +1,5 @@
+SELECT NOT(pg_compression_available('lz4')) AS skip_test \gset
+\if :skip_test
+   \echo '*** skipping lz4 tests (lz4 not available) ***'
+*** skipping lz4 tests (lz4 not available) ***
+   \quit
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a424be2a6bf..fbffc67ae60 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index 490595fcfb2..a21491456a7 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -1,3 +1,6 @@
+CREATE SCHEMA pglz;
+SET search_path TO pglz, public;
+
 \set HIDE_TOAST_COMPRESSION false
 
 -- ensure we get stable results regardless of installation's default
@@ -8,7 +11,7 @@ CREATE TABLE cmdata(f1 text COMPRESSION pglz);
 CREATE INDEX idx ON cmdata(f1);
 INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
 \d+ cmdata
-CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4);
+CREATE TABLE cmdata1(f1 TEXT COMPRESSION pglz);
 INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
 \d+ cmdata1
 
@@ -83,7 +86,7 @@ SELECT pg_column_compression(f1) FROM cmdata1;
 SELECT pg_column_compression(x) FROM compressmv;
 
 -- test compression with partition
-CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1);
+CREATE TABLE cmpart(f1 text COMPRESSION pglz) PARTITION BY HASH(f1);
 CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
 CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
 
@@ -94,19 +97,14 @@ SELECT pg_column_compression(f1) FROM cmpart1;
 SELECT pg_column_compression(f1) FROM cmpart2;
 
 -- test compression with inheritance
-CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
-CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error
 CREATE TABLE cmdata3(f1 text);
 CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 
 -- test default_toast_compression GUC
-SET default_toast_compression = '';
-SET default_toast_compression = 'I do not exist compression';
-SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
 
 -- test alter compression method
-ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4;
+ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION pglz;
 INSERT INTO cmdata VALUES (repeat('123456789', 4004));
 \d+ cmdata
 SELECT pg_column_compression(f1) FROM cmdata;
@@ -115,12 +113,12 @@ ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
 \d+ cmdata2
 
 -- test alter compression method for materialized views
-ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4;
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION pglz;
 \d+ compressmv
 
 -- test alter compression method for partitioned tables
 ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
-ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION pglz;
 
 -- new data should be compressed with the current compression method
 INSERT INTO cmpart VALUES (repeat('123456789', 1004));
@@ -135,7 +133,7 @@ SELECT pg_column_compression(f1) FROM cmdata;
 
 -- test expression index
 DROP TABLE cmdata2;
-CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4);
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION pglz);
 CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
 INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
 generate_series(1, 50) g), VERSION());
diff --git a/src/test/regress/sql/compression_lz4.sql b/src/test/regress/sql/compression_lz4.sql
new file mode 100644
index 00000000000..c801adfa557
--- /dev/null
+++ b/src/test/regress/sql/compression_lz4.sql
@@ -0,0 +1,161 @@
+SELECT NOT(pg_compression_available('lz4')) AS skip_test \gset
+\if :skip_test
+   \echo '*** skipping lz4 tests (lz4 not available) ***'
+   \quit
+\endif
+
+CREATE SCHEMA lz4;
+SET search_path TO lz4, public;
+
+\set HIDE_TOAST_COMPRESSION false
+
+-- ensure we get stable results regardless of installation's default
+SET default_toast_compression = 'lz4';
+
+-- test creating table with compression method
+CREATE TABLE cmdata(f1 text COMPRESSION pglz);
+CREATE INDEX idx ON cmdata(f1);
+INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
+\d+ cmdata
+CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4);
+INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
+\d+ cmdata1
+
+-- verify stored compression method in the data
+SELECT pg_column_compression(f1) FROM cmdata;
+SELECT pg_column_compression(f1) FROM cmdata1;
+
+-- decompress data slice
+SELECT SUBSTR(f1, 200, 5) FROM cmdata;
+SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
+
+-- copy with table creation
+SELECT * INTO cmmove1 FROM cmdata;
+\d+ cmmove1
+SELECT pg_column_compression(f1) FROM cmmove1;
+
+-- copy to existing table
+CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
+INSERT INTO cmmove3 SELECT * FROM cmdata;
+INSERT INTO cmmove3 SELECT * FROM cmdata1;
+SELECT pg_column_compression(f1) FROM cmmove3;
+
+-- test LIKE INCLUDING COMPRESSION
+CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
+\d+ cmdata2
+DROP TABLE cmdata2;
+
+-- try setting compression for incompressible data type
+CREATE TABLE cmdata2 (f1 int COMPRESSION pglz);
+
+-- update using datum from different table
+CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
+INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmmove2;
+UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
+SELECT pg_column_compression(f1) FROM cmmove2;
+
+-- test externally stored compressed data
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
+CREATE TABLE cmdata2 (f1 text COMPRESSION pglz);
+INSERT INTO cmdata2 SELECT large_val() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata2;
+INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata1;
+SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
+SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
+DROP TABLE cmdata2;
+
+--test column type update varlena/non-varlena
+CREATE TABLE cmdata2 (f1 int);
+\d+ cmdata2
+ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
+\d+ cmdata2
+ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
+\d+ cmdata2
+
+--changing column storage should not impact the compression method
+--but the data should not be compressed
+ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
+ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz;
+\d+ cmdata2
+ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain;
+\d+ cmdata2
+INSERT INTO cmdata2 VALUES (repeat('123456789', 800));
+SELECT pg_column_compression(f1) FROM cmdata2;
+
+-- test compression with materialized view
+CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
+\d+ compressmv
+SELECT pg_column_compression(f1) FROM cmdata1;
+SELECT pg_column_compression(x) FROM compressmv;
+
+-- test compression with partition
+CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1);
+CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
+
+ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+SELECT pg_column_compression(f1) FROM cmpart2;
+
+-- test compression with inheritance
+CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
+CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error
+CREATE TABLE cmdata3(f1 text);
+CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
+
+-- test default_toast_compression GUC
+SET default_toast_compression = 'lz4';
+
+-- test alter compression method
+ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4;
+INSERT INTO cmdata VALUES (repeat('123456789', 4004));
+\d+ cmdata
+SELECT pg_column_compression(f1) FROM cmdata;
+
+ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
+\d+ cmdata2
+
+-- test alter compression method for materialized views
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4;
+\d+ compressmv
+
+-- test alter compression method for partitioned tables
+ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4;
+
+-- new data should be compressed with the current compression method
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+SELECT pg_column_compression(f1) FROM cmpart2;
+
+-- VACUUM FULL does not recompress
+SELECT pg_column_compression(f1) FROM cmdata;
+VACUUM FULL cmdata;
+SELECT pg_column_compression(f1) FROM cmdata;
+
+-- test expression index
+DROP TABLE cmdata2;
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4);
+CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
+INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
+generate_series(1, 50) g), VERSION());
+
+-- check data is ok
+SELECT length(f1) FROM cmdata;
+SELECT length(f1) FROM cmdata1;
+SELECT length(f1) FROM cmmove1;
+SELECT length(f1) FROM cmmove2;
+SELECT length(f1) FROM cmmove3;
+
+CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails
+CREATE TABLE badcompresstbl (a text);
+ALTER TABLE badcompresstbl ALTER a SET COMPRESSION I_Do_Not_Exist_Compression; -- fails
+DROP TABLE badcompresstbl;
+
+\set HIDE_TOAST_COMPRESSION true

base-commit: b87163e5f3847730ee5f59718d215c6e63e13bff
-- 
2.47.1

v25-0002-Design-to-extend-the-varattrib_4b-varatt_externa.patchapplication/octet-stream; name=v25-0002-Design-to-extend-the-varattrib_4b-varatt_externa.patchDownload
From b9d6f5ab89ea9f4e8565bdeb286751d3270bf32c Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Thu, 5 Jun 2025 03:44:47 +0000
Subject: [PATCH v25 2/3] Design to extend the varattrib_4b/varatt_external to
 support of multiple TOAST compression algorithms.

---
 contrib/amcheck/verify_heapam.c             |   2 +-
 src/backend/access/common/detoast.c         |   6 +-
 src/backend/access/common/toast_internals.c |  10 +-
 src/backend/access/table/toast_helper.c     |   4 +-
 src/include/access/detoast.h                |  10 +-
 src/include/access/toast_compression.h      |  26 ++--
 src/include/access/toast_internals.h        |  36 ++---
 src/include/varatt.h                        | 145 +++++++++++++++++---
 src/tools/pgindent/typedefs.list            |   1 -
 9 files changed, 176 insertions(+), 64 deletions(-)

diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index aa9cccd1da4..2161d129502 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -1786,7 +1786,7 @@ check_tuple_attribute(HeapCheckContext *ctx)
 		bool		valid = false;
 
 		/* Compressed attributes should have a valid compression method */
-		cmid = TOAST_COMPRESS_METHOD(&toast_pointer);
+		cmid = VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer);
 		switch (cmid)
 		{
 				/* List of all valid compression method IDs */
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 62651787742..01419d1c65f 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -478,7 +478,7 @@ toast_decompress_datum(struct varlena *attr)
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
@@ -514,14 +514,14 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 	 * have been seen to give wrong results if passed an output size that is
 	 * more than the data's true decompressed size.
 	 */
-	if ((uint32) slicelength >= TOAST_COMPRESS_EXTSIZE(attr))
+	if ((uint32) slicelength >= VARDATA_COMPRESSED_GET_EXTSIZE(attr))
 		return toast_decompress_datum(attr);
 
 	/*
 	 * Fetch the compression method id stored in the compression header and
 	 * decompress the data slice using the appropriate decompression routine.
 	 */
-	cmid = TOAST_COMPRESS_METHOD(attr);
+	cmid = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(attr);
 	switch (cmid)
 	{
 		case TOAST_PGLZ_COMPRESSION_ID:
diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c
index 7d8be8346ce..32653af2e9e 100644
--- a/src/backend/access/common/toast_internals.c
+++ b/src/backend/access/common/toast_internals.c
@@ -143,6 +143,7 @@ toast_save_datum(Relation rel, Datum value,
 	Pointer		dval = DatumGetPointer(value);
 	int			num_indexes;
 	int			validIndex;
+	ToastCompressionId cm = TOAST_INVALID_COMPRESSION_ID;
 
 	Assert(!VARATT_IS_EXTERNAL(value));
 
@@ -183,10 +184,11 @@ toast_save_datum(Relation rel, Datum value,
 		data_todo = VARSIZE(dval) - VARHDRSZ;
 		/* rawsize in a compressed datum is just the size of the payload */
 		toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
+		cm = VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval);
 
 		/* set external size and compression method */
-		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
-													 VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
+		VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo, cm);
+
 		/* Assert that the numbers look like it's compressed */
 		Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
 	}
@@ -368,9 +370,9 @@ toast_save_datum(Relation rel, Datum value,
 	/*
 	 * Create the TOAST pointer value that we'll return
 	 */
-	result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
+	result = (struct varlena *) palloc(TOAST_POINTER_SIZE(cm));
 	SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
-	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
+	memcpy(VARDATA_EXTERNAL(result), &toast_pointer, TOAST_POINTER_SIZE(cm) - VARHDRSZ_EXTERNAL);
 
 	return PointerGetDatum(result);
 }
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index b60fab0a4d2..5a52bb1b67f 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -171,7 +171,7 @@ toast_tuple_init(ToastTupleContext *ttc)
  * The column must have attstorage EXTERNAL or EXTENDED if check_main is
  * false, and must have attstorage MAIN if check_main is true.
  *
- * The column must have a minimum size of MAXALIGN(TOAST_POINTER_SIZE);
+ * The column must have a minimum size of MAXALIGN(TOAST_POINTER_NOEXT_SIZE);
  * if not, no benefit is to be expected by compressing it.
  *
  * The return value is the index of the biggest suitable column, or
@@ -184,7 +184,7 @@ toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
 	int			biggest_attno = -1;
-	int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
+	int32		biggest_size = MAXALIGN(TOAST_POINTER_NOEXT_SIZE);
 	int32		skip_colflags = TOASTCOL_IGNORE;
 	int			i;
 
diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index e603a2276c3..ca8abaad644 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -23,12 +23,16 @@
 do { \
 	varattrib_1b_e *attre = (varattrib_1b_e *) (attr); \
 	Assert(VARATT_IS_EXTERNAL(attre)); \
-	Assert(VARSIZE_EXTERNAL(attre) == sizeof(toast_pointer) + VARHDRSZ_EXTERNAL); \
-	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), sizeof(toast_pointer)); \
+	memset(&(toast_pointer), 0, sizeof(toast_pointer)); \
+	memcpy(&(toast_pointer), VARDATA_EXTERNAL(attre), VARSIZE_EXTERNAL(attre) - VARHDRSZ_EXTERNAL); \
 } while (0)
 
 /* Size of an EXTERNAL datum that contains a standard TOAST pointer */
-#define TOAST_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_external))
+#define TOAST_POINTER_NOEXT_SIZE (VARHDRSZ_EXTERNAL + offsetof(varatt_external, extended))
+#define TOAST_POINTER_EXT_SIZE (TOAST_POINTER_NOEXT_SIZE + MEMBER_SIZE(varatt_external, extended.cmp))
+
+#define TOAST_POINTER_SIZE(cm)	\
+	(TOAST_CMPID_EXTENDED(cm) ? TOAST_POINTER_EXT_SIZE : TOAST_POINTER_NOEXT_SIZE)
 
 /* Size of an EXTERNAL datum that contains an indirection pointer */
 #define INDIRECT_POINTER_SIZE (VARHDRSZ_EXTERNAL + sizeof(varatt_indirect))
diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h
index 13c4612ceed..62b77edf372 100644
--- a/src/include/access/toast_compression.h
+++ b/src/include/access/toast_compression.h
@@ -23,16 +23,21 @@
 extern PGDLLIMPORT int default_toast_compression;
 
 /*
- * Built-in compression method ID.  The toast compression header will store
- * this in the first 2 bits of the raw length.  These built-in compression
- * method IDs are directly mapped to the built-in compression methods.
+ * Built-in compression method ID.
  *
- * Don't use these values for anything other than understanding the meaning
- * of the raw bits from a varlena; in particular, if the goal is to identify
- * a compression method, use the constants TOAST_PGLZ_COMPRESSION, etc.
- * below. We might someday support more than 4 compression methods, but
- * we can never have more than 4 values in this enum, because there are
- * only 2 bits available in the places where this is stored.
+ * For TOAST-compressed values:
+ *   - If using a non-extended method, the first 2 bits of the raw length
+ *     field store this ID.
+ *   - If using an extended method, it is stored in the extended 1-byte header.
+ *
+ * For varlena attributes using extended compression (varatt_external and varattr_4b):
+ *   - The compression method ID occupies the first seven bits of va_extinfo.
+ *
+ * These IDs map directly to the built-in compression methods.
+ *
+ * Note: Do not use these values for anything other than interpreting the
+ * raw bits from a varlena. To identify a compression method in code, use
+ * the named constants (e.g., TOAST_PGLZ_COMPRESSION) instead.
  */
 typedef enum ToastCompressionId
 {
@@ -51,6 +56,9 @@ typedef enum ToastCompressionId
 #define InvalidCompressionMethod		'\0'
 
 #define CompressionMethodIsValid(cm)  ((cm) != InvalidCompressionMethod)
+#define TOAST_CMPID_EXTENDED(cmpid)	(!(cmpid == TOAST_PGLZ_COMPRESSION_ID ||	\
+										cmpid == TOAST_LZ4_COMPRESSION_ID ||	\
+										cmpid == TOAST_INVALID_COMPRESSION_ID))
 
 
 /* pglz compression/decompression routines */
diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h
index 06ae8583c1e..857b53431c8 100644
--- a/src/include/access/toast_internals.h
+++ b/src/include/access/toast_internals.h
@@ -17,32 +17,24 @@
 #include "utils/relcache.h"
 #include "utils/snapshot.h"
 
-/*
- *	The information at the start of the compressed toast data.
- */
-typedef struct toast_compress_header
-{
-	int32		vl_len_;		/* varlena header (do not touch directly!) */
-	uint32		tcinfo;			/* 2 bits for compression method and 30 bits
-								 * external size; see va_extinfo */
-} toast_compress_header;
-
 /*
  * Utilities for manipulation of header information for compressed
  * toast entries.
  */
-#define TOAST_COMPRESS_EXTSIZE(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo & VARLENA_EXTSIZE_MASK)
-#define TOAST_COMPRESS_METHOD(ptr) \
-	(((toast_compress_header *) (ptr))->tcinfo >> VARLENA_EXTSIZE_BITS)
-
-#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method) \
-	do { \
-		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK); \
-		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm_method) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_compress_header *) (ptr))->tcinfo = \
-			(len) | ((uint32) (cm_method) << VARLENA_EXTSIZE_BITS); \
+#define TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(ptr, len, cm_method)														\
+	do {																														\
+		Assert((len) > 0 && (len) <= VARLENA_EXTSIZE_MASK);																		\
+		Assert((cm_method) == TOAST_PGLZ_COMPRESSION_ID ||																		\
+				(cm_method) == TOAST_LZ4_COMPRESSION_ID);																		\
+		if (!TOAST_CMPID_EXTENDED((cm_method)))																					\
+			((varattrib_4b *)(ptr))->va_compressed.va_tcinfo = ((uint32)(len)) | ((uint32)(cm_method) << VARLENA_EXTSIZE_BITS);	\
+		else																													\
+		{																														\
+			/* extended path: mark EXT flag in tcinfo */																		\
+			((varattrib_4b *)(ptr))->va_compressed_ext.va_tcinfo =																\
+				((uint32)(len)) | ((uint32)(VARATT_4BCE_EXTFLAG) << VARLENA_EXTSIZE_BITS);										\
+			VARATT_4BCE_SET_COMPRESS_METHOD(((varattrib_4b *)(ptr))->va_compressed_ext.va_ecinfo, (cm_method));					\
+		}																														\
 	} while (0)
 
 extern Datum toast_compress_datum(Datum value, char cmethod);
diff --git a/src/include/varatt.h b/src/include/varatt.h
index 2e8564d4998..39dcfc4b4b8 100644
--- a/src/include/varatt.h
+++ b/src/include/varatt.h
@@ -28,14 +28,28 @@
  * you need to memcpy from the tuple into a local struct variable before
  * you can look at these fields!  (The reason we use memcmp is to avoid
  * having to do that just to detect equality of two TOAST pointers...)
+ *
+ * Optional trailer (only when va_extinfo top bits = 11):
+ *	extended.cmp.va_ecinfo – 1 byte where:
+ *		1. Bits 7–1 encode (cmid − 2), so cmid ∈ [2…129].
+ *		2. Bit 0 is a flag indicating if the algorithm expects extra metadata.
  */
 typedef struct varatt_external
 {
 	int32		va_rawsize;		/* Original data size (includes header) */
 	uint32		va_extinfo;		/* External saved size (without header) and
-								 * compression method */
+								 * compression method or VARATT_4BCE_EXTFLAG
+								 * flag */
 	Oid			va_valueid;		/* Unique ID of value within TOAST table */
 	Oid			va_toastrelid;	/* RelID of TOAST table containing it */
+	/* -------- optional trailer -------- */
+	union
+	{
+		struct					/* compression-method trailer */
+		{
+			uint8		va_ecinfo;	/* Extended compression methods info */
+		}			cmp;
+	}			extended;		/* "extended" = optional bytes */
 }			varatt_external;
 
 /*
@@ -93,11 +107,18 @@ typedef enum vartag_external
 #define VARTAG_IS_EXPANDED(tag) \
 	(((tag) & ~1) == VARTAG_EXPANDED_RO)
 
-#define VARTAG_SIZE(tag) \
-	((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \
-	 VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \
-	 (tag) == VARTAG_ONDISK ? sizeof(varatt_external) : \
-	 (AssertMacro(false), 0))
+#define MEMBER_SIZE(type, member)  sizeof( ((type *)0)->member )
+
+#define VARTAG_SIZE(PTR)																				\
+(																										\
+	VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT ? sizeof(varatt_indirect) :									\
+	VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR)) ? sizeof(varatt_expanded) :								\
+	VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK ?																\
+		(offsetof(varatt_external, extended) +															\
+			((READ_U32_UNALIGNED((const uint8 *)(PTR) + VARHDRSZ_EXTERNAL +								\
+				offsetof(varatt_external, va_extinfo)) >> VARLENA_EXTSIZE_BITS) == VARATT_4BCE_EXTFLAG	\
+				? MEMBER_SIZE(varatt_external, extended.cmp) : 0)) : (AssertMacro(false), 0)			\
+)
 
 /*
  * These structs describe the header of a varlena object that may have been
@@ -122,6 +143,17 @@ typedef union
 								 * compression method; see va_extinfo */
 		char		va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
 	}			va_compressed;
+	struct
+	{
+		uint32		va_header;
+		uint32		va_tcinfo;	/* Original data size (excludes header) and
+								 * compression method or VARATT_4BCE_EXTFLAG
+								 * flag; see va_extinfo */
+		uint8		va_ecinfo;	/** va_ecinfo – 1 byte where:
+								 * 1. Bits 7–1 encode (cmid − 2), so cmid ∈ [2…129].
+								 * 2. Bit 0 is a flag indicating if the algorithm expects extra metadata. */
+		char		va_data[FLEXIBLE_ARRAY_MEMBER];
+	}			va_compressed_ext;
 } varattrib_4b;
 
 typedef struct
@@ -206,6 +238,18 @@ typedef struct
 	(((varattrib_1b_e *) (PTR))->va_header = 0x80, \
 	 ((varattrib_1b_e *) (PTR))->va_tag = (tag))
 
+/**
+ * Safely read a 32-bit unsigned integer from *any* address, even when
+ * that address is **not** naturally aligned to 4 bytes.  We do the load
+ * one byte at a time and re-assemble the word in *host* byte order.
+ * For BIG ENDIAN systems.
+ */
+#define READ_U32_UNALIGNED(ptr)						\
+	( (uint32) (((const uint8 *)(ptr))[3])			\
+	| ((uint32)(((const uint8 *)(ptr))[2]) <<  8)	\
+	| ((uint32)(((const uint8 *)(ptr))[1]) << 16)	\
+	| ((uint32)(((const uint8 *)(ptr))[0]) << 24) )
+
 #else							/* !WORDS_BIGENDIAN */
 
 #define VARATT_IS_4B(PTR) \
@@ -238,6 +282,17 @@ typedef struct
 #define SET_VARTAG_1B_E(PTR,tag) \
 	(((varattrib_1b_e *) (PTR))->va_header = 0x01, \
 	 ((varattrib_1b_e *) (PTR))->va_tag = (tag))
+/**
+ * Safely read a 32-bit unsigned integer from *any* address, even when
+ * that address is **not** naturally aligned to 4 bytes.  We do the load
+ * one byte at a time and re-assemble the word in *host* byte order.
+ * For LITTLE ENDIAN systems
+ */
+#define READ_U32_UNALIGNED(ptr)						\
+	( (uint32) (((const uint8 *)(ptr))[0])			\
+	| ((uint32)(((const uint8 *)(ptr))[1]) <<  8)	\
+	| ((uint32)(((const uint8 *)(ptr))[2]) << 16)	\
+	| ((uint32)(((const uint8 *)(ptr))[3]) << 24) )
 
 #endif							/* WORDS_BIGENDIAN */
 
@@ -282,7 +337,7 @@ typedef struct
 #define VARDATA_SHORT(PTR)					VARDATA_1B(PTR)
 
 #define VARTAG_EXTERNAL(PTR)				VARTAG_1B_E(PTR)
-#define VARSIZE_EXTERNAL(PTR)				(VARHDRSZ_EXTERNAL + VARTAG_SIZE(VARTAG_EXTERNAL(PTR)))
+#define VARSIZE_EXTERNAL(PTR)				(VARHDRSZ_EXTERNAL + VARTAG_SIZE(PTR))
 #define VARDATA_EXTERNAL(PTR)				VARDATA_1B_E(PTR)
 
 #define VARATT_IS_COMPRESSED(PTR)			VARATT_IS_4B_C(PTR)
@@ -325,23 +380,38 @@ typedef struct
 	 (VARATT_IS_1B(PTR) ? VARDATA_1B(PTR) : VARDATA_4B(PTR))
 
 /* Decompressed size and compression method of a compressed-in-line Datum */
-#define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK)
+#define VARDATA_COMPRESSED_GET_EXTSIZE(PTR)														\
+	(																							\
+		(VARATT_IS_4BCE(PTR))																	\
+			? ( ((varattrib_4b *)(PTR))->va_compressed_ext.va_tcinfo & VARLENA_EXTSIZE_MASK )	\
+			: ( ((varattrib_4b *)(PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK )		\
+	)
 #define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \
-	(((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS)
+	( (VARATT_IS_4BCE(PTR)) ? VARATT_4BCE_GET_COMPRESS_METHOD(((varattrib_4b *) (PTR))->va_compressed_ext.va_ecinfo) \
+	: (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS))
 
 /* Same for external Datums; but note argument is a struct varatt_external */
 #define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \
 	((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK)
-#define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \
-	((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS)
-
-#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \
-	do { \
-		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \
-			   (cm) == TOAST_LZ4_COMPRESSION_ID); \
-		((toast_pointer).va_extinfo = \
-			(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS)); \
+#define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer)							\
+	( ((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS) == VARATT_4BCE_EXTFLAG	\
+		? VARATT_4BCE_GET_COMPRESS_METHOD((toast_pointer).extended.cmp.va_ecinfo)	\
+			: (toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS )
+
+#define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm)						\
+	do {																							\
+		Assert((cm) == TOAST_PGLZ_COMPRESSION_ID ||													\
+				(cm) == TOAST_LZ4_COMPRESSION_ID);													\
+		if (!TOAST_CMPID_EXTENDED((cm)))															\
+			/* method fits in the low bits of va_extinfo */											\
+			(toast_pointer).va_extinfo = (uint32)(len) | ((uint32) (cm) << VARLENA_EXTSIZE_BITS);	\
+		else																						\
+		{																							\
+			/* set “extended” flag and store the extra byte */										\
+			(toast_pointer).va_extinfo = (uint32)(len) |											\
+				(VARATT_4BCE_EXTFLAG << VARLENA_EXTSIZE_BITS);										\
+			VARATT_4BCE_SET_COMPRESS_METHOD((toast_pointer).extended.cmp.va_ecinfo, (cm));			\
+		}																							\
 	} while (0)
 
 /*
@@ -355,4 +425,41 @@ typedef struct
 	(VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \
 	 (toast_pointer).va_rawsize - VARHDRSZ)
 
+/* Upper-two-bit pattern 0b11 marks “extended compression methods used. */
+#define VARATT_4BCE_EXTFLAG             0x3
+
+/*
+ * Layout of the extra 1-byte trailer for extended compression info:
+ *
+ *   bit 7   6   5   4   3   2   1   0
+ *  +---+---+---+---+---+---+---+---+
+ *  |      cmid_minus2          | F |
+ *  +---+---+---+---+---+---+---+---+
+ *
+ * • Bits 7–1 (cmid_minus2):
+ *     7-bit field holding (cmid − 2). The actual compression‐method ID (cmid)
+ *     is (raw + 2), so raw ∈ [0…127] maps to cmid ∈ [2…129].
+ *
+ * • Bit 0 (F):
+ *     Single flag bit reserved for indicating whether this compression method has associated metadata.
+ */
+#define VARATT_4BCE_SET_COMPRESS_METHOD(va_ecinfo, cmid)				\
+	do {																\
+		bool meta = false;												\
+		(va_ecinfo) = (uint8)((((cmid) - 2) << 1) | ((meta) & 0x01));	\
+	} while (0)
+
+#define VARATT_4BCE_GET_COMPRESS_METHOD(raw)	((((raw) >> 1) & 0x7F) + 2)
+
+/* Does this varattrib use the “compressed-extended” format? */
+#define VARATT_IS_4BCE(ptr) \
+	((((varattrib_4b *)(ptr))->va_compressed_ext.va_tcinfo >> VARLENA_EXTSIZE_BITS) \
+		== VARATT_4BCE_EXTFLAG)
+
+/* Access the start of the compressed payload */
+#define VARDATA_4BCE(ptr) \
+	(((varattrib_4b *)(ptr))->va_compressed_ext.va_data)
+
+#define VARHDRSZ_4BCE	(offsetof(varattrib_4b, va_compressed_ext.va_data))
+
 #endif
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a8346cda633..eb53118b72b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4097,7 +4097,6 @@ timeout_handler_proc
 timeout_params
 timerCA
 tlist_vinfo
-toast_compress_header
 tokenize_error_callback_arg
 transferMode
 transfer_thread_arg
-- 
2.47.1

#43Michael Paquier
michael@paquier.xyz
In reply to: Nikhil Kumar Veldanda (#42)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Thu, Jun 05, 2025 at 12:03:49AM -0700, Nikhil Kumar Veldanda wrote:

Agreed. I introduced pg_compression_available(text) and refactored the
SQL tests accordingly. I split out LZ4 tests into compression_lz4.sql
and created compression_zstd.sql with the appropriate differences.

v25-0001-Add-pg_compression_available-and-split-sql-compr.patch -
Introduced pg_compression_available function and split sql tests
related to compression

I like that as an independent piece because it's going to help a lot
in having new compression methods, so I'm looking forward to getting
that merged into the tree for v19. It can be split into two
independent pieces:
- One patch for the addition of the new function
pg_compression_available(), to detect which compression are supported
at binary level to skip the tests.
- One patch to split the LZ4-only tests into its own file.

The split of the tests is not completely clean as presented in your
patch, though. Your patch only does a copy-paste of the original
file. Some of the basic tests of compression.sql check the
interactions between the use of two compression methods, and the
"basic" compression.sql could just cut them and rely on the LZ4
scripts to do the job, because we want two active different
compression methods for these scenarios. For example, cmdata1
switched to use pglz has little uses. The trick is to have a minimal
set of tests to minimize the run time, while we don't lose in
coverage. Coverage report numbers are useful to compile when it comes
to such exercises, even if it can be an ant's work sometimes.

+ * pg_compression_available(text) → bool

Non-ASCII characters added in the code comments.

+#include "fmgr.h"
+#include "parser/scansup.h"
+#include "utils/builtins.h"

Include file order.

v25-0002-Design-to-extend-the-varattrib_4b-varatt_externa.patch -
Design proposal for varattrib_4b & varatt_external
v25-0003-Implement-Zstd-compression-no-dictionary-support.patch - zstd
no dictionary compression implementation

About this part, I am not sure yet. TBH, I've been working on this
the code for a different proposal in this area, because I've been
reminded during pgconf.dev that we still depend on 4-byte OIDs for
toast values, and we have done nothing about that for a long time.

If I'm able to pull this off correctly, modernizing the code on the
way, it should make additions related to the handling of different
on-disk varatt_external easier; the compression handling is a part of
that. So yes, that's related to varatt_external, and how we handle
it in the core code in the toasting and detoasting layers. The
difficult part is finding out how a good layer should look like,
because there's a bunch of hardcoded knowledge related to on-disk
TOAST Datums and entries, like the maximum chunk size (control file)
that depends on the toast_pointer, pointer alignment when inserting
the TOAST datums, etc. A lot of these things are close to 20 years
old, we have to maintain on-disk compatibility while attempting to
extend the varatt_external compatibility and there have been many
proposals that did not make it. None of them were really mature
enough in terms of layer deinision. Probably what I'm doing is going
to be flat-out rejected, but we'll see.
--
Michael

#44Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#43)
2 attachment(s)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Wed, Jun 11, 2025 at 11:42:02AM +0900, Michael Paquier wrote:

The split of the tests is not completely clean as presented in your
patch, though. Your patch only does a copy-paste of the original
file. Some of the basic tests of compression.sql check the
interactions between the use of two compression methods, and the
"basic" compression.sql could just cut them and rely on the LZ4
scripts to do the job, because we want two active different
compression methods for these scenarios. For example, cmdata1
switched to use pglz has little uses. The trick is to have a minimal
set of tests to minimize the run time, while we don't lose in
coverage. Coverage report numbers are useful to compile when it comes
to such exercises, even if it can be an ant's work sometimes.

I have no idea yet about the fate of the other TOAST patches I have
proposed for this commit fest, but let's move on with this part of the
refactoring by splitting the TOAST regression tests for LZ4 and pglz,
with the new pg_compression_available() that would reduce the diffs
with the alternate outputs.

This has required a bit more work than I suspected. Based on my
notes, first for pg_compression_available():
- Code moved to misc.c, with comments related to TOAST removed.
- Addition of gzip as an acceptable value.
- Error if the compression method is unknown.
- Some regression tests.
- Documentation should list the functions alphabetically.

Then for the refactoring of the tests, a few notes:
- There is no need for cmdata1 in compression.sql, using the same
compression method as cmdata, aka pglz. So we can trim down the
tests.
- In compression.sql, we can remove cmmove2, cmmove3 and cmdata2 which
have a compression method of pglz, and that we want to check where the
origin has LZ4 data. These should be only in compression_lz4.sql,
perhaps also in the zstd portion if needed later for your patch.
- The error cases with I_Do_Not_Exist_Compression at the bottom of
compression.sql can be kept, we don't need them in
compression_lz4.sql.
- It would be tempting to keep the test for LIKE INCLUDING COMPRESSION
in compression.sql, but we cannot do that as there is a dependency
with default_toast_compression so we want the GUC at pglz but the
table we are copying the data from at LZ4.
compression.sql, there is no need for it to depend on LZ4.
- The tests related to cmdata2 depend on LZ4 TOAST, which were a bit
duplicated.
- "test column type update varlena/non-varlena" was duplicated. Same
for "changing column storage should not impact the compression
method".
- The materialized view test in compression.sql depends on LZ4, can be
moved to compression_lz4.sql.
- The test with partitions and compression methods expects multiple
compression methods, can be moved to compression_lz4.sql
- "test alter compression method" expects two compression methods, can
be moved to compression_lz4.sql.
- The tests with SET default_toast_compression report a hint with the
list of values supported. This is not portable because the list of
values depends on what the build supports. We should use a trick
based on "\set VERBOSITY terse", removing the HINT to reduce the
noise.
- The tables specific to pglz and lz4 data are both still required in
compression_lz4.sql, for one test with inheritance. I have renamed
both to cmdata_pglz and cmdata_lz4, for clarity.

At the end, the gain in diffs is here per the following numbers in
the attached 0002 as we remove the alternal output of compression.sql
when lz4 is disabled:
7 files changed, 319 insertions(+), 724 deletions(-)

Attached are two patches for all that:
- 0001: Introduction of the new function pg_compression_available().
- 0002: Refactoring of the TOAST compression tests.

With this infrastructure in place, the addition of a new TOAST
compression method becomes easier for the test part: no more
cross-build specific diffs.

Thought, comments or objections?
--
Michael

Attachments:

0001-Add-function-pg_compression_available.patchtext/x-diff; charset=us-asciiDownload
From 776ef0cfffa2a774bf255c4707da4f2af2b7bd06 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 16 Jul 2025 12:06:57 +0900
Subject: [PATCH 1/2] Add function pg_compression_available

This is able to check if compression methods are available in a given
build, in a platform and build-transparent manner.  The values that can
be checked in the backend are: gzip, pglz, lz4 and zstd.

This function will be useful in an upcoming patch that refactors the
TOAST tests.
---
 src/include/catalog/pg_proc.dat              |  4 ++
 src/backend/utils/adt/misc.c                 | 51 ++++++++++++++++++++
 src/test/regress/expected/misc_functions.out | 15 ++++++
 src/test/regress/sql/misc_functions.sql      |  5 ++
 doc/src/sgml/func.sgml                       | 18 +++++++
 5 files changed, 93 insertions(+)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 1fc19146f467..6b3aaba4a90e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6876,6 +6876,10 @@
 { oid => '315', descr => 'Is JIT compilation available in this session?',
   proname => 'pg_jit_available', provolatile => 'v', prorettype => 'bool',
   proargtypes => '', prosrc => 'pg_jit_available' },
+{ oid =>  9474, descr => 'Is compression method available?',
+  proname => 'pg_compression_available', prokind => 'f',
+  provolatile => 'i', prorettype => 'bool', proargtypes => 'text',
+  prosrc => 'pg_compression_available' },
 
 { oid => '2971', descr => 'convert boolean to text',
   proname => 'text', prorettype => 'text', proargtypes => 'bool',
diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c
index 6fcfd031428e..a41b19c6250a 100644
--- a/src/backend/utils/adt/misc.c
+++ b/src/backend/utils/adt/misc.c
@@ -1122,3 +1122,54 @@ any_value_transfn(PG_FUNCTION_ARGS)
 {
 	PG_RETURN_DATUM(PG_GETARG_DATUM(0));
 }
+
+/*
+ * pg_compression_available
+ *
+ * True if the named compression method is available in this server.
+ */
+Datum
+pg_compression_available(PG_FUNCTION_ARGS)
+{
+	text	   *name = PG_GETARG_TEXT_PP(0);
+	char	   *cname = downcase_truncate_identifier(text_to_cstring(name),
+													 NAMEDATALEN, false);
+
+	/* pglz is always there */
+	if (strcmp(cname, "pglz") == 0)
+		PG_RETURN_BOOL(true);
+
+	if (strcmp(cname, "gzip") == 0)
+	{
+#ifdef HAVE_LIBZ
+		PG_RETURN_BOOL(true);
+#else
+		PG_RETURN_BOOL(false);
+#endif
+	}
+
+	if (strcmp(cname, "lz4") == 0)
+	{
+#ifdef USE_LZ4
+		PG_RETURN_BOOL(true);
+#else
+		PG_RETURN_BOOL(false);
+#endif
+	}
+
+	if (strcmp(cname, "zstd") == 0)
+	{
+#ifdef USE_ZSTD
+		PG_RETURN_BOOL(true);
+#else
+		PG_RETURN_BOOL(false);
+#endif
+	}
+
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("%s compression is not supported by this build",
+					cname)));
+
+	PG_RETURN_BOOL(false);	/* keep compiler quiet */
+}
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index c3b2b9d86034..8158839fc6f2 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -918,3 +918,18 @@ SELECT test_relpath();
 SELECT pg_replication_origin_create('regress_' || repeat('a', 505));
 ERROR:  replication origin name is too long
 DETAIL:  Replication origin names must be no longer than 512 bytes.
+-- pg_compression_available
+SELECT pg_compression_available(NULL);
+ pg_compression_available 
+--------------------------
+ 
+(1 row)
+
+SELECT pg_compression_available('pglz');
+ pg_compression_available 
+--------------------------
+ t
+(1 row)
+
+SELECT pg_compression_available('non_existent');
+ERROR:  non_existent compression is not supported by this build
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 23792c4132a1..5a59eb35197b 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -414,3 +414,8 @@ SELECT test_relpath();
 
 -- pg_replication_origin.roname limit
 SELECT pg_replication_origin_create('regress_' || repeat('a', 505));
+
+-- pg_compression_available
+SELECT pg_compression_available(NULL);
+SELECT pg_compression_available('pglz');
+SELECT pg_compression_available('non_existent');
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index f5a0e0954a15..ddeda431b2be 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25020,6 +25020,24 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n);
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_compression_available</primary>
+        </indexterm>
+        <function>pg_compression_available</function> ( <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Returns <literal>true</literal> if the given compression method name
+         is supported in the <productname>PostgreSQL</productname> build.
+         The supported compression methods that can be checked are
+         <literal>gzip</literal>, <literal>pglz</literal> (always available),
+         <literal>lz4</literal> and <literal>zstd</literal>.
+        </para>
+       </entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
-- 
2.50.0

0002-Split-TOAST-compression-tests-into-two-files.patchtext/x-diff; charset=us-asciiDownload
From 11dbd68b454db53721f21f3b7624b9f95fb64fc6 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 16 Jul 2025 13:42:54 +0900
Subject: [PATCH 2/2] Split TOAST compression tests into two files

The parts specific to LZ4 and interactions between two different TOAST
compression methods are moved to a new file, called compression_lz4.
The new test is skipped if the build does not support LZ4 compression.

Both files are independent, with the same coverage as previously.
---
 src/test/regress/expected/compression.out     | 235 +-----------
 src/test/regress/expected/compression_1.out   | 360 ------------------
 src/test/regress/expected/compression_lz4.out | 247 ++++++++++++
 .../regress/expected/compression_lz4_1.out    |   6 +
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/compression.sql          |  84 +---
 .../{compression.sql => compression_lz4.sql}  | 116 +++---
 7 files changed, 326 insertions(+), 724 deletions(-)
 delete mode 100644 src/test/regress/expected/compression_1.out
 create mode 100644 src/test/regress/expected/compression_lz4.out
 create mode 100644 src/test/regress/expected/compression_lz4_1.out
 copy src/test/regress/sql/{compression.sql => compression_lz4.sql} (50%)

diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out
index 4dd9ee7200d1..09f198149aa4 100644
--- a/src/test/regress/expected/compression.out
+++ b/src/test/regress/expected/compression.out
@@ -1,3 +1,7 @@
+-- Default set of tests for TOAST compression, independent on compression
+-- methods supported by the build.
+CREATE SCHEMA pglz;
+SET search_path TO pglz, public;
 \set HIDE_TOAST_COMPRESSION false
 -- ensure we get stable results regardless of installation's default
 SET default_toast_compression = 'pglz';
@@ -6,21 +10,13 @@ CREATE TABLE cmdata(f1 text COMPRESSION pglz);
 CREATE INDEX idx ON cmdata(f1);
 INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
 \d+ cmdata
-                                        Table "public.cmdata"
+                                         Table "pglz.cmdata"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | text |           |          |         | extended | pglz        |              | 
 Indexes:
     "idx" btree (f1)
 
-CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4);
-INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
-\d+ cmdata1
-                                        Table "public.cmdata1"
- Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended | lz4         |              | 
-
 -- verify stored compression method in the data
 SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
@@ -28,12 +24,6 @@ SELECT pg_column_compression(f1) FROM cmdata;
  pglz
 (1 row)
 
-SELECT pg_column_compression(f1) FROM cmdata1;
- pg_column_compression 
------------------------
- lz4
-(1 row)
-
 -- decompress data slice
 SELECT SUBSTR(f1, 200, 5) FROM cmdata;
  substr 
@@ -41,16 +31,10 @@ SELECT SUBSTR(f1, 200, 5) FROM cmdata;
  01234
 (1 row)
 
-SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
-                       substr                       
-----------------------------------------------------
- 01234567890123456789012345678901234567890123456789
-(1 row)
-
 -- copy with table creation
 SELECT * INTO cmmove1 FROM cmdata;
 \d+ cmmove1
-                                        Table "public.cmmove1"
+                                         Table "pglz.cmmove1"
  Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | text |           |          |         | extended |             |              | 
@@ -61,45 +45,9 @@ SELECT pg_column_compression(f1) FROM cmmove1;
  pglz
 (1 row)
 
--- copy to existing table
-CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
-INSERT INTO cmmove3 SELECT * FROM cmdata;
-INSERT INTO cmmove3 SELECT * FROM cmdata1;
-SELECT pg_column_compression(f1) FROM cmmove3;
- pg_column_compression 
------------------------
- pglz
- lz4
-(2 rows)
-
--- test LIKE INCLUDING COMPRESSION
-CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
-\d+ cmdata2
-                                        Table "public.cmdata2"
- Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended | lz4         |              | 
-
-DROP TABLE cmdata2;
 -- try setting compression for incompressible data type
 CREATE TABLE cmdata2 (f1 int COMPRESSION pglz);
 ERROR:  column data type integer does not support compression
--- update using datum from different table
-CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
-INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
-SELECT pg_column_compression(f1) FROM cmmove2;
- pg_column_compression 
------------------------
- pglz
-(1 row)
-
-UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
-SELECT pg_column_compression(f1) FROM cmmove2;
- pg_column_compression 
------------------------
- lz4
-(1 row)
-
 -- test externally stored compressed data
 CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
 'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
@@ -111,21 +59,6 @@ SELECT pg_column_compression(f1) FROM cmdata2;
  pglz
 (1 row)
 
-INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
-SELECT pg_column_compression(f1) FROM cmdata1;
- pg_column_compression 
------------------------
- lz4
- lz4
-(2 rows)
-
-SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
- substr 
---------
- 01234
- 79026
-(2 rows)
-
 SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
  substr 
 --------
@@ -136,21 +69,21 @@ DROP TABLE cmdata2;
 --test column type update varlena/non-varlena
 CREATE TABLE cmdata2 (f1 int);
 \d+ cmdata2
-                                         Table "public.cmdata2"
+                                          Table "pglz.cmdata2"
  Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | integer |           |          |         | plain   |             |              | 
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "pglz.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | character varying |           |          |         | extended |             |              | 
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
 \d+ cmdata2
-                                         Table "public.cmdata2"
+                                          Table "pglz.cmdata2"
  Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | integer |           |          |         | plain   |             |              | 
@@ -160,14 +93,14 @@ ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
 ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "pglz.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
  f1     | character varying |           |          |         | extended | pglz        |              | 
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "pglz.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | character varying |           |          |         | plain   | pglz        |              | 
@@ -179,164 +112,47 @@ SELECT pg_column_compression(f1) FROM cmdata2;
  
 (1 row)
 
--- test compression with materialized view
-CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
-\d+ compressmv
-                                Materialized view "public.compressmv"
- Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- x      | text |           |          |         | extended |             |              | 
-View definition:
- SELECT f1 AS x
-   FROM cmdata1;
-
-SELECT pg_column_compression(f1) FROM cmdata1;
- pg_column_compression 
------------------------
- lz4
- lz4
-(2 rows)
-
-SELECT pg_column_compression(x) FROM compressmv;
- pg_column_compression 
------------------------
- lz4
- lz4
-(2 rows)
-
--- test compression with partition
-CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1);
-CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
-CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
-ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
-INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-SELECT pg_column_compression(f1) FROM cmpart1;
- pg_column_compression 
------------------------
- lz4
-(1 row)
-
-SELECT pg_column_compression(f1) FROM cmpart2;
- pg_column_compression 
------------------------
- pglz
-(1 row)
-
 -- test compression with inheritance
-CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
-NOTICE:  merging multiple inherited definitions of column "f1"
-ERROR:  column "f1" has a compression method conflict
-DETAIL:  pglz versus lz4
-CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error
-NOTICE:  merging column "f1" with inherited definition
-ERROR:  column "f1" has a compression method conflict
-DETAIL:  pglz versus lz4
 CREATE TABLE cmdata3(f1 text);
 CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 NOTICE:  merging multiple inherited definitions of column "f1"
 -- test default_toast_compression GUC
+-- suppress machine-dependent details
+\set VERBOSITY terse
 SET default_toast_compression = '';
 ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz, lz4.
 SET default_toast_compression = 'I do not exist compression';
 ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz, lz4.
-SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
--- test alter compression method
-ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4;
-INSERT INTO cmdata VALUES (repeat('123456789', 4004));
-\d+ cmdata
-                                        Table "public.cmdata"
- Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended | lz4         |              | 
-Indexes:
-    "idx" btree (f1)
-Child tables: cminh
-
-SELECT pg_column_compression(f1) FROM cmdata;
- pg_column_compression 
------------------------
- pglz
- lz4
-(2 rows)
-
+\set VERBOSITY default
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
 \d+ cmdata2
-                                              Table "public.cmdata2"
+                                               Table "pglz.cmdata2"
  Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
 --------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
  f1     | character varying |           |          |         | plain   |             |              | 
 
--- test alter compression method for materialized views
-ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4;
-\d+ compressmv
-                                Materialized view "public.compressmv"
- Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- x      | text |           |          |         | extended | lz4         |              | 
-View definition:
- SELECT f1 AS x
-   FROM cmdata1;
-
--- test alter compression method for partitioned tables
-ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
-ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4;
--- new data should be compressed with the current compression method
-INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-SELECT pg_column_compression(f1) FROM cmpart1;
- pg_column_compression 
------------------------
- lz4
- pglz
-(2 rows)
-
-SELECT pg_column_compression(f1) FROM cmpart2;
- pg_column_compression 
------------------------
- pglz
- lz4
-(2 rows)
-
+DROP TABLE cmdata2;
 -- VACUUM FULL does not recompress
 SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
 -----------------------
  pglz
- lz4
-(2 rows)
+(1 row)
 
 VACUUM FULL cmdata;
 SELECT pg_column_compression(f1) FROM cmdata;
  pg_column_compression 
 -----------------------
  pglz
- lz4
-(2 rows)
+(1 row)
 
--- test expression index
-DROP TABLE cmdata2;
-CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4);
-CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
-INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
-generate_series(1, 50) g), VERSION());
 -- check data is ok
 SELECT length(f1) FROM cmdata;
  length 
 --------
   10000
-  36036
-(2 rows)
-
-SELECT length(f1) FROM cmdata1;
- length 
---------
-  10040
-  12449
-(2 rows)
+(1 row)
 
 SELECT length(f1) FROM cmmove1;
  length 
@@ -344,19 +160,6 @@ SELECT length(f1) FROM cmmove1;
   10000
 (1 row)
 
-SELECT length(f1) FROM cmmove2;
- length 
---------
-  10040
-(1 row)
-
-SELECT length(f1) FROM cmmove3;
- length 
---------
-  10000
-  10040
-(2 rows)
-
 CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails
 ERROR:  invalid compression method "i_do_not_exist_compression"
 CREATE TABLE badcompresstbl (a text);
diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out
deleted file mode 100644
index 7bd7642b4b94..000000000000
--- a/src/test/regress/expected/compression_1.out
+++ /dev/null
@@ -1,360 +0,0 @@
-\set HIDE_TOAST_COMPRESSION false
--- ensure we get stable results regardless of installation's default
-SET default_toast_compression = 'pglz';
--- test creating table with compression method
-CREATE TABLE cmdata(f1 text COMPRESSION pglz);
-CREATE INDEX idx ON cmdata(f1);
-INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
-\d+ cmdata
-                                        Table "public.cmdata"
- Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended | pglz        |              | 
-Indexes:
-    "idx" btree (f1)
-
-CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4);
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
-INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
-ERROR:  relation "cmdata1" does not exist
-LINE 1: INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
-                    ^
-\d+ cmdata1
--- verify stored compression method in the data
-SELECT pg_column_compression(f1) FROM cmdata;
- pg_column_compression 
------------------------
- pglz
-(1 row)
-
-SELECT pg_column_compression(f1) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmdata1;
-                                              ^
--- decompress data slice
-SELECT SUBSTR(f1, 200, 5) FROM cmdata;
- substr 
---------
- 01234
-(1 row)
-
-SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
-                                         ^
--- copy with table creation
-SELECT * INTO cmmove1 FROM cmdata;
-\d+ cmmove1
-                                        Table "public.cmmove1"
- Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended |             |              | 
-
-SELECT pg_column_compression(f1) FROM cmmove1;
- pg_column_compression 
------------------------
- pglz
-(1 row)
-
--- copy to existing table
-CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
-INSERT INTO cmmove3 SELECT * FROM cmdata;
-INSERT INTO cmmove3 SELECT * FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: INSERT INTO cmmove3 SELECT * FROM cmdata1;
-                                          ^
-SELECT pg_column_compression(f1) FROM cmmove3;
- pg_column_compression 
------------------------
- pglz
-(1 row)
-
--- test LIKE INCLUDING COMPRESSION
-CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
-ERROR:  relation "cmdata1" does not exist
-LINE 1: CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
-                                   ^
-\d+ cmdata2
-DROP TABLE cmdata2;
-ERROR:  table "cmdata2" does not exist
--- try setting compression for incompressible data type
-CREATE TABLE cmdata2 (f1 int COMPRESSION pglz);
-ERROR:  column data type integer does not support compression
--- update using datum from different table
-CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
-INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
-SELECT pg_column_compression(f1) FROM cmmove2;
- pg_column_compression 
------------------------
- pglz
-(1 row)
-
-UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
-                                                ^
-SELECT pg_column_compression(f1) FROM cmmove2;
- pg_column_compression 
------------------------
- pglz
-(1 row)
-
--- test externally stored compressed data
-CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
-'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
-CREATE TABLE cmdata2 (f1 text COMPRESSION pglz);
-INSERT INTO cmdata2 SELECT large_val() || repeat('a', 4000);
-SELECT pg_column_compression(f1) FROM cmdata2;
- pg_column_compression 
------------------------
- pglz
-(1 row)
-
-INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
-ERROR:  relation "cmdata1" does not exist
-LINE 1: INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
-                    ^
-SELECT pg_column_compression(f1) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmdata1;
-                                              ^
-SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
-                                       ^
-SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
- substr 
---------
- 79026
-(1 row)
-
-DROP TABLE cmdata2;
---test column type update varlena/non-varlena
-CREATE TABLE cmdata2 (f1 int);
-\d+ cmdata2
-                                         Table "public.cmdata2"
- Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
---------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
- f1     | integer |           |          |         | plain   |             |              | 
-
-ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
-\d+ cmdata2
-                                              Table "public.cmdata2"
- Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | character varying |           |          |         | extended |             |              | 
-
-ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
-\d+ cmdata2
-                                         Table "public.cmdata2"
- Column |  Type   | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
---------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
- f1     | integer |           |          |         | plain   |             |              | 
-
---changing column storage should not impact the compression method
---but the data should not be compressed
-ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
-ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz;
-\d+ cmdata2
-                                              Table "public.cmdata2"
- Column |       Type        | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+-------------------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | character varying |           |          |         | extended | pglz        |              | 
-
-ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain;
-\d+ cmdata2
-                                              Table "public.cmdata2"
- Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
---------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
- f1     | character varying |           |          |         | plain   | pglz        |              | 
-
-INSERT INTO cmdata2 VALUES (repeat('123456789', 800));
-SELECT pg_column_compression(f1) FROM cmdata2;
- pg_column_compression 
------------------------
- 
-(1 row)
-
--- test compression with materialized view
-CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: ...TE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
-                                                               ^
-\d+ compressmv
-SELECT pg_column_compression(f1) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmdata1;
-                                              ^
-SELECT pg_column_compression(x) FROM compressmv;
-ERROR:  relation "compressmv" does not exist
-LINE 1: SELECT pg_column_compression(x) FROM compressmv;
-                                             ^
--- test compression with partition
-CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1);
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
-CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
-ERROR:  relation "cmpart" does not exist
-CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
-ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
-ERROR:  relation "cmpart" does not exist
-INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-ERROR:  relation "cmpart" does not exist
-LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-                    ^
-INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-ERROR:  relation "cmpart" does not exist
-LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-                    ^
-SELECT pg_column_compression(f1) FROM cmpart1;
-ERROR:  relation "cmpart1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmpart1;
-                                              ^
-SELECT pg_column_compression(f1) FROM cmpart2;
- pg_column_compression 
------------------------
-(0 rows)
-
--- test compression with inheritance
-CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
-ERROR:  relation "cmdata1" does not exist
-CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error
-NOTICE:  merging column "f1" with inherited definition
-ERROR:  column "f1" has a compression method conflict
-DETAIL:  pglz versus lz4
-CREATE TABLE cmdata3(f1 text);
-CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
-NOTICE:  merging multiple inherited definitions of column "f1"
--- test default_toast_compression GUC
-SET default_toast_compression = '';
-ERROR:  invalid value for parameter "default_toast_compression": ""
-HINT:  Available values: pglz.
-SET default_toast_compression = 'I do not exist compression';
-ERROR:  invalid value for parameter "default_toast_compression": "I do not exist compression"
-HINT:  Available values: pglz.
-SET default_toast_compression = 'lz4';
-ERROR:  invalid value for parameter "default_toast_compression": "lz4"
-HINT:  Available values: pglz.
-SET default_toast_compression = 'pglz';
--- test alter compression method
-ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4;
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
-INSERT INTO cmdata VALUES (repeat('123456789', 4004));
-\d+ cmdata
-                                        Table "public.cmdata"
- Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
---------+------+-----------+----------+---------+----------+-------------+--------------+-------------
- f1     | text |           |          |         | extended | pglz        |              | 
-Indexes:
-    "idx" btree (f1)
-Child tables: cminh
-
-SELECT pg_column_compression(f1) FROM cmdata;
- pg_column_compression 
------------------------
- pglz
- pglz
-(2 rows)
-
-ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
-\d+ cmdata2
-                                              Table "public.cmdata2"
- Column |       Type        | Collation | Nullable | Default | Storage | Compression | Stats target | Description 
---------+-------------------+-----------+----------+---------+---------+-------------+--------------+-------------
- f1     | character varying |           |          |         | plain   |             |              | 
-
--- test alter compression method for materialized views
-ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4;
-ERROR:  relation "compressmv" does not exist
-\d+ compressmv
--- test alter compression method for partitioned tables
-ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
-ERROR:  relation "cmpart1" does not exist
-ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4;
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
--- new data should be compressed with the current compression method
-INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-ERROR:  relation "cmpart" does not exist
-LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-                    ^
-INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-ERROR:  relation "cmpart" does not exist
-LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-                    ^
-SELECT pg_column_compression(f1) FROM cmpart1;
-ERROR:  relation "cmpart1" does not exist
-LINE 1: SELECT pg_column_compression(f1) FROM cmpart1;
-                                              ^
-SELECT pg_column_compression(f1) FROM cmpart2;
- pg_column_compression 
------------------------
-(0 rows)
-
--- VACUUM FULL does not recompress
-SELECT pg_column_compression(f1) FROM cmdata;
- pg_column_compression 
------------------------
- pglz
- pglz
-(2 rows)
-
-VACUUM FULL cmdata;
-SELECT pg_column_compression(f1) FROM cmdata;
- pg_column_compression 
------------------------
- pglz
- pglz
-(2 rows)
-
--- test expression index
-DROP TABLE cmdata2;
-CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4);
-ERROR:  compression method lz4 not supported
-DETAIL:  This functionality requires the server to be built with lz4 support.
-CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
-ERROR:  relation "cmdata2" does not exist
-INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
-generate_series(1, 50) g), VERSION());
-ERROR:  relation "cmdata2" does not exist
-LINE 1: INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEX...
-                    ^
--- check data is ok
-SELECT length(f1) FROM cmdata;
- length 
---------
-  10000
-  36036
-(2 rows)
-
-SELECT length(f1) FROM cmdata1;
-ERROR:  relation "cmdata1" does not exist
-LINE 1: SELECT length(f1) FROM cmdata1;
-                               ^
-SELECT length(f1) FROM cmmove1;
- length 
---------
-  10000
-(1 row)
-
-SELECT length(f1) FROM cmmove2;
- length 
---------
-  10040
-(1 row)
-
-SELECT length(f1) FROM cmmove3;
- length 
---------
-  10000
-(1 row)
-
-CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails
-ERROR:  invalid compression method "i_do_not_exist_compression"
-CREATE TABLE badcompresstbl (a text);
-ALTER TABLE badcompresstbl ALTER a SET COMPRESSION I_Do_Not_Exist_Compression; -- fails
-ERROR:  invalid compression method "i_do_not_exist_compression"
-DROP TABLE badcompresstbl;
-\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_lz4.out b/src/test/regress/expected/compression_lz4.out
new file mode 100644
index 000000000000..6c8309bc7b14
--- /dev/null
+++ b/src/test/regress/expected/compression_lz4.out
@@ -0,0 +1,247 @@
+-- Tests for TOAST compression with lz4
+SELECT NOT(pg_compression_available('lz4')) AS skip_test \gset
+\if :skip_test
+   \echo '*** skipping lz4 tests (lz4 not available) ***'
+   \quit
+\endif
+CREATE SCHEMA lz4;
+SET search_path TO lz4, public;
+\set HIDE_TOAST_COMPRESSION false
+-- Ensure we get stable results regardless of installation's default
+-- We rely on this GUC value for a few tests.
+SET default_toast_compression = 'pglz';
+-- test creating table with compression method
+CREATE TABLE cmdata_pglz(f1 text COMPRESSION pglz);
+CREATE INDEX idx ON cmdata_pglz(f1);
+INSERT INTO cmdata_pglz VALUES(repeat('1234567890', 1000));
+\d+ cmdata
+CREATE TABLE cmdata_lz4(f1 TEXT COMPRESSION lz4);
+INSERT INTO cmdata_lz4 VALUES(repeat('1234567890', 1004));
+\d+ cmdata1
+-- verify stored compression method in the data
+SELECT pg_column_compression(f1) FROM cmdata_lz4;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+-- decompress data slice
+SELECT SUBSTR(f1, 200, 5) FROM cmdata_pglz;
+ substr 
+--------
+ 01234
+(1 row)
+
+SELECT SUBSTR(f1, 2000, 50) FROM cmdata_lz4;
+                       substr                       
+----------------------------------------------------
+ 01234567890123456789012345678901234567890123456789
+(1 row)
+
+-- copy with table creation
+SELECT * INTO cmmove1 FROM cmdata_lz4;
+\d+ cmmove1
+                                         Table "lz4.cmmove1"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended |             |              | 
+
+SELECT pg_column_compression(f1) FROM cmmove1;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+-- test LIKE INCLUDING COMPRESSION.  The GUC default_toast_compression
+-- has no effect, the compression method from the table being copied.
+CREATE TABLE cmdata2 (LIKE cmdata_lz4 INCLUDING COMPRESSION);
+\d+ cmdata2
+                                         Table "lz4.cmdata2"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ f1     | text |           |          |         | extended | lz4         |              | 
+
+DROP TABLE cmdata2;
+-- copy to existing table
+CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
+INSERT INTO cmmove3 SELECT * FROM cmdata_pglz;
+INSERT INTO cmmove3 SELECT * FROM cmdata_lz4;
+SELECT pg_column_compression(f1) FROM cmmove3;
+ pg_column_compression 
+-----------------------
+ pglz
+ lz4
+(2 rows)
+
+-- update using datum from different table with LZ4 data.
+CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
+INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
+SELECT pg_column_compression(f1) FROM cmmove2;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+UPDATE cmmove2 SET f1 = cmdata_lz4.f1 FROM cmdata_lz4;
+SELECT pg_column_compression(f1) FROM cmmove2;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+-- test externally stored compressed data
+CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
+'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
+CREATE TABLE cmdata2 (f1 text COMPRESSION lz4);
+INSERT INTO cmdata2 SELECT large_val() || repeat('a', 4000);
+SELECT pg_column_compression(f1) FROM cmdata2;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
+ substr 
+--------
+ 79026
+(1 row)
+
+DROP TABLE cmdata2;
+-- test compression with materialized view
+CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata_lz4;
+\d+ compressmv
+                                  Materialized view "lz4.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended |             |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata_lz4;
+
+SELECT pg_column_compression(f1) FROM cmdata_lz4;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+SELECT pg_column_compression(x) FROM compressmv;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+-- test compression with partition
+CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1);
+CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
+ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+ pg_column_compression 
+-----------------------
+ lz4
+(1 row)
+
+SELECT pg_column_compression(f1) FROM cmpart2;
+ pg_column_compression 
+-----------------------
+ pglz
+(1 row)
+
+-- test compression with inheritance
+CREATE TABLE cminh() INHERITS(cmdata_pglz, cmdata_lz4); -- error
+NOTICE:  merging multiple inherited definitions of column "f1"
+ERROR:  column "f1" has a compression method conflict
+DETAIL:  pglz versus lz4
+CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata_pglz); -- error
+NOTICE:  merging column "f1" with inherited definition
+ERROR:  column "f1" has a compression method conflict
+DETAIL:  pglz versus lz4
+CREATE TABLE cmdata3(f1 text);
+CREATE TABLE cminh() INHERITS (cmdata_pglz, cmdata3);
+NOTICE:  merging multiple inherited definitions of column "f1"
+-- test default_toast_compression GUC
+SET default_toast_compression = 'lz4';
+-- test alter compression method
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION lz4;
+INSERT INTO cmdata_pglz VALUES (repeat('123456789', 4004));
+\d+ cmdata
+SELECT pg_column_compression(f1) FROM cmdata_pglz;
+ pg_column_compression 
+-----------------------
+ pglz
+ lz4
+(2 rows)
+
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION pglz;
+-- test alter compression method for materialized views
+ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4;
+\d+ compressmv
+                                  Materialized view "lz4.compressmv"
+ Column | Type | Collation | Nullable | Default | Storage  | Compression | Stats target | Description 
+--------+------+-----------+----------+---------+----------+-------------+--------------+-------------
+ x      | text |           |          |         | extended | lz4         |              | 
+View definition:
+ SELECT f1 AS x
+   FROM cmdata_lz4;
+
+-- test alter compression method for partitioned tables
+ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
+ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4;
+-- new data should be compressed with the current compression method
+INSERT INTO cmpart VALUES (repeat('123456789', 1004));
+INSERT INTO cmpart VALUES (repeat('123456789', 4004));
+SELECT pg_column_compression(f1) FROM cmpart1;
+ pg_column_compression 
+-----------------------
+ lz4
+ pglz
+(2 rows)
+
+SELECT pg_column_compression(f1) FROM cmpart2;
+ pg_column_compression 
+-----------------------
+ pglz
+ lz4
+(2 rows)
+
+-- test expression index
+CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4);
+CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
+INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
+generate_series(1, 50) g), VERSION());
+-- check data is ok
+SELECT length(f1) FROM cmdata_pglz;
+ length 
+--------
+  10000
+  36036
+(2 rows)
+
+SELECT length(f1) FROM cmdata_lz4;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove1;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove2;
+ length 
+--------
+  10040
+(1 row)
+
+SELECT length(f1) FROM cmmove3;
+ length 
+--------
+  10000
+  10040
+(2 rows)
+
+\set HIDE_TOAST_COMPRESSION true
diff --git a/src/test/regress/expected/compression_lz4_1.out b/src/test/regress/expected/compression_lz4_1.out
new file mode 100644
index 000000000000..daf83614fade
--- /dev/null
+++ b/src/test/regress/expected/compression_lz4_1.out
@@ -0,0 +1,6 @@
+-- Tests for TOAST compression with lz4
+SELECT NOT(pg_compression_available('lz4')) AS skip_test \gset
+\if :skip_test
+   \echo '*** skipping lz4 tests (lz4 not available) ***'
+*** skipping lz4 tests (lz4 not available) ***
+   \quit
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a424be2a6bf0..fbffc67ae601 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql
index 490595fcfb26..ce5ea37a660c 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression.sql
@@ -1,3 +1,8 @@
+-- Default set of tests for TOAST compression, independent on compression
+-- methods supported by the build.
+
+CREATE SCHEMA pglz;
+SET search_path TO pglz, public;
 \set HIDE_TOAST_COMPRESSION false
 
 -- ensure we get stable results regardless of installation's default
@@ -8,53 +13,27 @@ CREATE TABLE cmdata(f1 text COMPRESSION pglz);
 CREATE INDEX idx ON cmdata(f1);
 INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
 \d+ cmdata
-CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4);
-INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
-\d+ cmdata1
 
 -- verify stored compression method in the data
 SELECT pg_column_compression(f1) FROM cmdata;
-SELECT pg_column_compression(f1) FROM cmdata1;
 
 -- decompress data slice
 SELECT SUBSTR(f1, 200, 5) FROM cmdata;
-SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
 
 -- copy with table creation
 SELECT * INTO cmmove1 FROM cmdata;
 \d+ cmmove1
 SELECT pg_column_compression(f1) FROM cmmove1;
 
--- copy to existing table
-CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
-INSERT INTO cmmove3 SELECT * FROM cmdata;
-INSERT INTO cmmove3 SELECT * FROM cmdata1;
-SELECT pg_column_compression(f1) FROM cmmove3;
-
--- test LIKE INCLUDING COMPRESSION
-CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
-\d+ cmdata2
-DROP TABLE cmdata2;
-
 -- try setting compression for incompressible data type
 CREATE TABLE cmdata2 (f1 int COMPRESSION pglz);
 
--- update using datum from different table
-CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
-INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
-SELECT pg_column_compression(f1) FROM cmmove2;
-UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
-SELECT pg_column_compression(f1) FROM cmmove2;
-
 -- test externally stored compressed data
 CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
 'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
 CREATE TABLE cmdata2 (f1 text COMPRESSION pglz);
 INSERT INTO cmdata2 SELECT large_val() || repeat('a', 4000);
 SELECT pg_column_compression(f1) FROM cmdata2;
-INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
-SELECT pg_column_compression(f1) FROM cmdata1;
-SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
 SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
 DROP TABLE cmdata2;
 
@@ -76,76 +55,31 @@ ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain;
 INSERT INTO cmdata2 VALUES (repeat('123456789', 800));
 SELECT pg_column_compression(f1) FROM cmdata2;
 
--- test compression with materialized view
-CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
-\d+ compressmv
-SELECT pg_column_compression(f1) FROM cmdata1;
-SELECT pg_column_compression(x) FROM compressmv;
-
--- test compression with partition
-CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1);
-CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0);
-CREATE TABLE cmpart2(f1 text COMPRESSION pglz);
-
-ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1);
-INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-SELECT pg_column_compression(f1) FROM cmpart1;
-SELECT pg_column_compression(f1) FROM cmpart2;
-
 -- test compression with inheritance
-CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
-CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error
 CREATE TABLE cmdata3(f1 text);
 CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
 
 -- test default_toast_compression GUC
+-- suppress machine-dependent details
+\set VERBOSITY terse
 SET default_toast_compression = '';
 SET default_toast_compression = 'I do not exist compression';
-SET default_toast_compression = 'lz4';
 SET default_toast_compression = 'pglz';
-
--- test alter compression method
-ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4;
-INSERT INTO cmdata VALUES (repeat('123456789', 4004));
-\d+ cmdata
-SELECT pg_column_compression(f1) FROM cmdata;
+\set VERBOSITY default
 
 ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
 \d+ cmdata2
 
--- test alter compression method for materialized views
-ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4;
-\d+ compressmv
-
--- test alter compression method for partitioned tables
-ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz;
-ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4;
-
--- new data should be compressed with the current compression method
-INSERT INTO cmpart VALUES (repeat('123456789', 1004));
-INSERT INTO cmpart VALUES (repeat('123456789', 4004));
-SELECT pg_column_compression(f1) FROM cmpart1;
-SELECT pg_column_compression(f1) FROM cmpart2;
+DROP TABLE cmdata2;
 
 -- VACUUM FULL does not recompress
 SELECT pg_column_compression(f1) FROM cmdata;
 VACUUM FULL cmdata;
 SELECT pg_column_compression(f1) FROM cmdata;
 
--- test expression index
-DROP TABLE cmdata2;
-CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4);
-CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
-INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
-generate_series(1, 50) g), VERSION());
-
 -- check data is ok
 SELECT length(f1) FROM cmdata;
-SELECT length(f1) FROM cmdata1;
 SELECT length(f1) FROM cmmove1;
-SELECT length(f1) FROM cmmove2;
-SELECT length(f1) FROM cmmove3;
 
 CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails
 CREATE TABLE badcompresstbl (a text);
diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression_lz4.sql
similarity index 50%
copy from src/test/regress/sql/compression.sql
copy to src/test/regress/sql/compression_lz4.sql
index 490595fcfb26..37c247742c90 100644
--- a/src/test/regress/sql/compression.sql
+++ b/src/test/regress/sql/compression_lz4.sql
@@ -1,85 +1,73 @@
+-- Tests for TOAST compression with lz4
+
+SELECT NOT(pg_compression_available('lz4')) AS skip_test \gset
+\if :skip_test
+   \echo '*** skipping lz4 tests (lz4 not available) ***'
+   \quit
+\endif
+
+CREATE SCHEMA lz4;
+SET search_path TO lz4, public;
+
 \set HIDE_TOAST_COMPRESSION false
 
--- ensure we get stable results regardless of installation's default
+-- Ensure we get stable results regardless of installation's default
+-- We rely on this GUC value for a few tests.
 SET default_toast_compression = 'pglz';
 
 -- test creating table with compression method
-CREATE TABLE cmdata(f1 text COMPRESSION pglz);
-CREATE INDEX idx ON cmdata(f1);
-INSERT INTO cmdata VALUES(repeat('1234567890', 1000));
+CREATE TABLE cmdata_pglz(f1 text COMPRESSION pglz);
+CREATE INDEX idx ON cmdata_pglz(f1);
+INSERT INTO cmdata_pglz VALUES(repeat('1234567890', 1000));
 \d+ cmdata
-CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4);
-INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004));
+CREATE TABLE cmdata_lz4(f1 TEXT COMPRESSION lz4);
+INSERT INTO cmdata_lz4 VALUES(repeat('1234567890', 1004));
 \d+ cmdata1
 
 -- verify stored compression method in the data
-SELECT pg_column_compression(f1) FROM cmdata;
-SELECT pg_column_compression(f1) FROM cmdata1;
+SELECT pg_column_compression(f1) FROM cmdata_lz4;
 
 -- decompress data slice
-SELECT SUBSTR(f1, 200, 5) FROM cmdata;
-SELECT SUBSTR(f1, 2000, 50) FROM cmdata1;
+SELECT SUBSTR(f1, 200, 5) FROM cmdata_pglz;
+SELECT SUBSTR(f1, 2000, 50) FROM cmdata_lz4;
 
 -- copy with table creation
-SELECT * INTO cmmove1 FROM cmdata;
+SELECT * INTO cmmove1 FROM cmdata_lz4;
 \d+ cmmove1
 SELECT pg_column_compression(f1) FROM cmmove1;
 
--- copy to existing table
-CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
-INSERT INTO cmmove3 SELECT * FROM cmdata;
-INSERT INTO cmmove3 SELECT * FROM cmdata1;
-SELECT pg_column_compression(f1) FROM cmmove3;
-
--- test LIKE INCLUDING COMPRESSION
-CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION);
+-- test LIKE INCLUDING COMPRESSION.  The GUC default_toast_compression
+-- has no effect, the compression method from the table being copied.
+CREATE TABLE cmdata2 (LIKE cmdata_lz4 INCLUDING COMPRESSION);
 \d+ cmdata2
 DROP TABLE cmdata2;
 
--- try setting compression for incompressible data type
-CREATE TABLE cmdata2 (f1 int COMPRESSION pglz);
+-- copy to existing table
+CREATE TABLE cmmove3(f1 text COMPRESSION pglz);
+INSERT INTO cmmove3 SELECT * FROM cmdata_pglz;
+INSERT INTO cmmove3 SELECT * FROM cmdata_lz4;
+SELECT pg_column_compression(f1) FROM cmmove3;
 
--- update using datum from different table
+-- update using datum from different table with LZ4 data.
 CREATE TABLE cmmove2(f1 text COMPRESSION pglz);
 INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004));
 SELECT pg_column_compression(f1) FROM cmmove2;
-UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1;
+UPDATE cmmove2 SET f1 = cmdata_lz4.f1 FROM cmdata_lz4;
 SELECT pg_column_compression(f1) FROM cmmove2;
 
 -- test externally stored compressed data
 CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS
 'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g';
-CREATE TABLE cmdata2 (f1 text COMPRESSION pglz);
+CREATE TABLE cmdata2 (f1 text COMPRESSION lz4);
 INSERT INTO cmdata2 SELECT large_val() || repeat('a', 4000);
 SELECT pg_column_compression(f1) FROM cmdata2;
-INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000);
-SELECT pg_column_compression(f1) FROM cmdata1;
-SELECT SUBSTR(f1, 200, 5) FROM cmdata1;
 SELECT SUBSTR(f1, 200, 5) FROM cmdata2;
 DROP TABLE cmdata2;
 
---test column type update varlena/non-varlena
-CREATE TABLE cmdata2 (f1 int);
-\d+ cmdata2
-ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
-\d+ cmdata2
-ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer;
-\d+ cmdata2
-
---changing column storage should not impact the compression method
---but the data should not be compressed
-ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar;
-ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz;
-\d+ cmdata2
-ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain;
-\d+ cmdata2
-INSERT INTO cmdata2 VALUES (repeat('123456789', 800));
-SELECT pg_column_compression(f1) FROM cmdata2;
-
 -- test compression with materialized view
-CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1;
+CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata_lz4;
 \d+ compressmv
-SELECT pg_column_compression(f1) FROM cmdata1;
+SELECT pg_column_compression(f1) FROM cmdata_lz4;
 SELECT pg_column_compression(x) FROM compressmv;
 
 -- test compression with partition
@@ -94,25 +82,20 @@ SELECT pg_column_compression(f1) FROM cmpart1;
 SELECT pg_column_compression(f1) FROM cmpart2;
 
 -- test compression with inheritance
-CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error
-CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error
+CREATE TABLE cminh() INHERITS(cmdata_pglz, cmdata_lz4); -- error
+CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata_pglz); -- error
 CREATE TABLE cmdata3(f1 text);
-CREATE TABLE cminh() INHERITS (cmdata, cmdata3);
+CREATE TABLE cminh() INHERITS (cmdata_pglz, cmdata3);
 
 -- test default_toast_compression GUC
-SET default_toast_compression = '';
-SET default_toast_compression = 'I do not exist compression';
 SET default_toast_compression = 'lz4';
-SET default_toast_compression = 'pglz';
 
 -- test alter compression method
-ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4;
-INSERT INTO cmdata VALUES (repeat('123456789', 4004));
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION lz4;
+INSERT INTO cmdata_pglz VALUES (repeat('123456789', 4004));
 \d+ cmdata
-SELECT pg_column_compression(f1) FROM cmdata;
-
-ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default;
-\d+ cmdata2
+SELECT pg_column_compression(f1) FROM cmdata_pglz;
+ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION pglz;
 
 -- test alter compression method for materialized views
 ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4;
@@ -128,28 +111,17 @@ INSERT INTO cmpart VALUES (repeat('123456789', 4004));
 SELECT pg_column_compression(f1) FROM cmpart1;
 SELECT pg_column_compression(f1) FROM cmpart2;
 
--- VACUUM FULL does not recompress
-SELECT pg_column_compression(f1) FROM cmdata;
-VACUUM FULL cmdata;
-SELECT pg_column_compression(f1) FROM cmdata;
-
 -- test expression index
-DROP TABLE cmdata2;
 CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4);
 CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));
 INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM
 generate_series(1, 50) g), VERSION());
 
 -- check data is ok
-SELECT length(f1) FROM cmdata;
-SELECT length(f1) FROM cmdata1;
+SELECT length(f1) FROM cmdata_pglz;
+SELECT length(f1) FROM cmdata_lz4;
 SELECT length(f1) FROM cmmove1;
 SELECT length(f1) FROM cmmove2;
 SELECT length(f1) FROM cmmove3;
 
-CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails
-CREATE TABLE badcompresstbl (a text);
-ALTER TABLE badcompresstbl ALTER a SET COMPRESSION I_Do_Not_Exist_Compression; -- fails
-DROP TABLE badcompresstbl;
-
 \set HIDE_TOAST_COMPRESSION true
-- 
2.50.0

#45Nikhil Kumar Veldanda
veldanda.nikhilkumar17@gmail.com
In reply to: Michael Paquier (#44)
Re: ZStandard (with dictionaries) compression support for TOAST compression

Hi Michael,

On Tue, Jul 15, 2025 at 9:44 PM Michael Paquier <michael@paquier.xyz> wrote:

I have no idea yet about the fate of the other TOAST patches I have
proposed for this commit fest, but let's move on with this part of the
refactoring by splitting the TOAST regression tests for LZ4 and pglz,
with the new pg_compression_available() that would reduce the diffs
with the alternate outputs.

This has required a bit more work than I suspected. Based on my
notes, first for pg_compression_available():
- Code moved to misc.c, with comments related to TOAST removed.
- Addition of gzip as an acceptable value.
- Error if the compression method is unknown.
- Some regression tests.
- Documentation should list the functions alphabetically.

Then for the refactoring of the tests, a few notes:
- There is no need for cmdata1 in compression.sql, using the same
compression method as cmdata, aka pglz. So we can trim down the
tests.
- In compression.sql, we can remove cmmove2, cmmove3 and cmdata2 which
have a compression method of pglz, and that we want to check where the
origin has LZ4 data. These should be only in compression_lz4.sql,
perhaps also in the zstd portion if needed later for your patch.
- The error cases with I_Do_Not_Exist_Compression at the bottom of
compression.sql can be kept, we don't need them in
compression_lz4.sql.
- It would be tempting to keep the test for LIKE INCLUDING COMPRESSION
in compression.sql, but we cannot do that as there is a dependency
with default_toast_compression so we want the GUC at pglz but the
table we are copying the data from at LZ4.
compression.sql, there is no need for it to depend on LZ4.
- The tests related to cmdata2 depend on LZ4 TOAST, which were a bit
duplicated.
- "test column type update varlena/non-varlena" was duplicated. Same
for "changing column storage should not impact the compression
method".
- The materialized view test in compression.sql depends on LZ4, can be
moved to compression_lz4.sql.
- The test with partitions and compression methods expects multiple
compression methods, can be moved to compression_lz4.sql
- "test alter compression method" expects two compression methods, can
be moved to compression_lz4.sql.
- The tests with SET default_toast_compression report a hint with the
list of values supported. This is not portable because the list of
values depends on what the build supports. We should use a trick
based on "\set VERBOSITY terse", removing the HINT to reduce the
noise.
- The tables specific to pglz and lz4 data are both still required in
compression_lz4.sql, for one test with inheritance. I have renamed
both to cmdata_pglz and cmdata_lz4, for clarity.

At the end, the gain in diffs is here per the following numbers in
the attached 0002 as we remove the alternal output of compression.sql
when lz4 is disabled:
7 files changed, 319 insertions(+), 724 deletions(-)

Attached are two patches for all that:
- 0001: Introduction of the new function pg_compression_available().
- 0002: Refactoring of the TOAST compression tests.

With this infrastructure in place, the addition of a new TOAST
compression method becomes easier for the test part: no more
cross-build specific diffs.

Thought, comments or objections?

Thanks for driving this forward—both patches look good to me.

0001 – pg_compression_available()
pg_compression_available() in misc.c feels sensible;

0002 – test-suite split
The new compression.sql / compression_lz4.sql split makes the diffs
much easier to reason about.

Michael

--
Nikhil Veldanda

#46Michael Paquier
michael@paquier.xyz
In reply to: Nikhil Kumar Veldanda (#45)
Re: ZStandard (with dictionaries) compression support for TOAST compression

On Tue, Jul 15, 2025 at 10:37:02PM -0700, Nikhil Kumar Veldanda wrote:

0001 – pg_compression_available()
pg_compression_available() in misc.c feels sensible;

Actually, I have taken a step back on this one and recalled that the
list of values available for an enum GUC are already available in
pg_settings, so we can already do something without this function,
with the same result:
+SELECT NOT(enumvals @> '{lz4}') AS skip_test FROM pg_settings WHERE
+  name = 'default_toast_compression' \gset

0002 – test-suite split
The new compression.sql / compression_lz4.sql split makes the diffs
much easier to reason about.

Another thing that I have spent a lot of time on today while having a
second look was the code coverage after a make check. There was one
surprising result: lz4_compress_datum() for the incompressible data
case now has some coverage.

A second thing is AdjustUpgrade.pm, which has the matview compressmv
with a qual based on cmdata1, but I think we're OK as this is an
adjustment of the upgrade dumps for 74a3fc36f314, which exists in
v16~. I'll keep an eye on the buildfarm anyway, in case something
shows up.
--
Michael