v7-0004-icu_multilib-contrib-extension-for-managing-ICU-l.patch

text/x-patch

Filename: v7-0004-icu_multilib-contrib-extension-for-managing-ICU-l.patch
Type: text/x-patch
Part: 3
Message: Re: Collation version tracking for macOS

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v7-0004
Subject: icu_multilib: contrib extension for managing ICU libraries.
File+
contrib/icu_multilib/icu_multilib--1.0.sql 43 0
contrib/icu_multilib/icu_multilib.c 880 0
contrib/icu_multilib/icu_multilib.control 7 0
contrib/icu_multilib/Makefile 21 0
contrib/icu_multilib/meson.build 24 0
contrib/Makefile 6 0
contrib/meson.build 3 0
doc/src/sgml/contrib.sgml 1 0
doc/src/sgml/filelist.sgml 1 0
doc/src/sgml/icu-multilib.sgml 496 0
From cdcce0e1f8dcb28fae8b7ef8413e70bd71e4a44d Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Sat, 14 Jan 2023 12:17:05 -0800
Subject: [PATCH v7 4/4] icu_multilib: contrib extension for managing ICU
 libraries.

The icu_multilib module provides control over the version (or
versions) of the ICU provider library used by PostgreSQL, which can be
different from the version of ICU with which it was built.

Control over the specific ICU library is important, because any
differences in collation may result in corrupt indexes, and possibly
other problems.

The primary use case is the ability to stabilize the ICU version
("lock" it to a specific version), even if the built-in ICU library
changes. It can be "locked" to a specific major and minor version (the
safest option); or to a major version only, enabling non-disruptive
minor version updates of ICU (albeit with some risk of subtle
differences in collation behavior).

If enabled, icu_multilib also provides support for multiple ICU
libraries by using "search by collator version" logic. This logic
assumes that identical collator versions (as provided by ICU) mean
identical behavior, which also carries some risk.

Author: Jeff Davis, Thomas Munro
---
 contrib/Makefile                           |   6 +
 contrib/icu_multilib/Makefile              |  21 +
 contrib/icu_multilib/icu_multilib--1.0.sql |  43 +
 contrib/icu_multilib/icu_multilib.c        | 880 +++++++++++++++++++++
 contrib/icu_multilib/icu_multilib.control  |   7 +
 contrib/icu_multilib/meson.build           |  24 +
 contrib/meson.build                        |   3 +
 doc/src/sgml/contrib.sgml                  |   1 +
 doc/src/sgml/filelist.sgml                 |   1 +
 doc/src/sgml/icu-multilib.sgml             | 496 ++++++++++++
 10 files changed, 1482 insertions(+)
 create mode 100644 contrib/icu_multilib/Makefile
 create mode 100644 contrib/icu_multilib/icu_multilib--1.0.sql
 create mode 100644 contrib/icu_multilib/icu_multilib.c
 create mode 100644 contrib/icu_multilib/icu_multilib.control
 create mode 100644 contrib/icu_multilib/meson.build
 create mode 100644 doc/src/sgml/icu-multilib.sgml

diff --git a/contrib/Makefile b/contrib/Makefile
index bbf220407b..06447d9346 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -65,6 +65,12 @@ else
 ALWAYS_SUBDIRS += uuid-ossp
 endif
 
+ifeq ($(with_icu),yes)
+SUBDIRS += icu_multilib
+else
+ALWAYS_SUBDIRS += icu_multilib
+endif
+
 ifeq ($(with_libxml),yes)
 SUBDIRS += xml2
 else
diff --git a/contrib/icu_multilib/Makefile b/contrib/icu_multilib/Makefile
new file mode 100644
index 0000000000..4823007abe
--- /dev/null
+++ b/contrib/icu_multilib/Makefile
@@ -0,0 +1,21 @@
+# contrib/icu_multilib/Makefile
+
+MODULE_big = icu_multilib
+OBJS = \
+	$(WIN32RES) \
+	icu_multilib.o
+
+EXTENSION = icu_multilib
+DATA = icu_multilib--1.0.sql
+PGFILEDESC = "icu_multilib -- support multiple ICU library versions"
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/icu_multilib
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/icu_multilib/icu_multilib--1.0.sql b/contrib/icu_multilib/icu_multilib--1.0.sql
new file mode 100644
index 0000000000..6751242bb4
--- /dev/null
+++ b/contrib/icu_multilib/icu_multilib--1.0.sql
@@ -0,0 +1,43 @@
+/* contrib/icu_multilib/icu_multilib--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION icu_multilib" to load this file. \quit
+
+CREATE FUNCTION library_versions(
+    icu_version OUT TEXT,
+    unicode_version OUT TEXT,
+    cldr_version OUT TEXT,
+    libicui18n_name OUT TEXT,
+    libicuuc_name OUT TEXT
+  )
+  RETURNS SETOF RECORD LANGUAGE C
+  AS 'MODULE_PATHNAME', 'library_versions';
+
+CREATE FUNCTION library_collators(
+    major_version IN TEXT DEFAULT NULL,
+    locale OUT TEXT,
+    icu_version OUT TEXT,
+    uca_version OUT TEXT,
+    collator_version OUT TEXT
+  )
+  RETURNS SETOF RECORD LANGUAGE C
+  AS 'MODULE_PATHNAME', 'library_collators';
+
+CREATE FUNCTION collator_version_search (
+    locale IN TEXT,
+    requested_version IN TEXT DEFAULT NULL,
+    log_ok IN BOOLEAN DEFAULT FALSE,
+    icu_version OUT TEXT,
+    uca_version OUT TEXT,
+    collator_version OUT TEXT)
+  RETURNS RECORD LANGUAGE C
+  AS 'MODULE_PATHNAME', 'collator_version_search';
+
+CREATE FUNCTION collator_versions (
+    locale IN TEXT,
+    icu_version OUT TEXT,
+    uca_version OUT TEXT,
+    collator_version OUT TEXT
+  )
+  RETURNS SETOF RECORD LANGUAGE C
+  AS 'MODULE_PATHNAME', 'collator_versions';
diff --git a/contrib/icu_multilib/icu_multilib.c b/contrib/icu_multilib/icu_multilib.c
new file mode 100644
index 0000000000..2fde2a85d5
--- /dev/null
+++ b/contrib/icu_multilib/icu_multilib.c
@@ -0,0 +1,880 @@
+
+#include "postgres.h"
+
+#include <dlfcn.h>
+#include <limits.h>
+
+#include "common/string.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "nodes/execnodes.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/pg_locale_internal.h"
+
+#ifndef USE_ICU
+#error "ICU support is required to build icu_multilib"
+#endif
+
+/*
+ * We don't want to call into dlopen'd ICU libraries that are newer than the
+ * one we were compiled and linked against, just in case there is an
+ * incompatible API change.
+ */
+#define PG_MAX_ICU_MAJOR U_ICU_VERSION_MAJOR_NUM
+
+/*
+ * The oldest ICU release we're likely to encounter, and that has all the
+ * funcitons required.
+ */
+#define PG_MIN_ICU_MAJOR 50
+
+/*
+ * Enough to hold entries for minimum to maximum supported ICU versions, and
+ * also the builtin ICU (if icu_multilib.include_builtin_icu is set).
+ */
+#define ICU_LIB_TABLESIZE (PG_MAX_ICU_MAJOR - PG_MIN_ICU_MAJOR + 2)
+
+typedef struct icu_version
+{
+	int major;
+	int minor;
+} icu_version;
+
+static const struct config_enum_entry log_level_options[] = {
+	{"debug5", DEBUG5, false},
+	{"debug4", DEBUG4, false},
+	{"debug3", DEBUG3, false},
+	{"debug2", DEBUG2, false},
+	{"debug1", DEBUG1, false},
+	{"debug", DEBUG2, true},
+	{"log", LOG, false},
+	{"info", INFO, true},
+	{"notice", NOTICE, false},
+	{"warning", WARNING, false},
+	{"error", ERROR, false},
+	{NULL, 0, false}
+};
+
+PG_MODULE_MAGIC;
+
+static get_icu_library_hook_type prev_icu_library_hook = NULL;
+
+static pg_icu_library *icu_library_table[ICU_LIB_TABLESIZE] = {};
+
+static char *icu_library_path = "";
+static char *default_icu_version = "";
+static icu_version default_version = { .major = -1, .minor = -1 };
+static bool search_by_collator_version = false;
+static bool include_builtin = true;
+static int version_mismatch_log_level = WARNING;
+static int library_search_log_level = DEBUG1;
+
+static void initialize_gucs_1(void);
+static void initialize_gucs_2(void);
+static pg_icu_library *icu_multilib_hook(Oid collid, const char *locale,
+										 const char *version);
+static pg_icu_library *icu_multilib_search(Oid collid, const char *locale,
+										   const char *version,
+										   bool logOk);
+static void load_all_libraries(void);
+static pg_icu_library *load_icu_library(int major);
+static pg_icu_library *get_icu_by_major_version(int major);
+static void get_library_version(const pg_icu_library *lib, int *major,
+								int *minor);
+
+static bool check_string_version(char **newval, void **extra, GucSource source);
+static void assign_default_version(const char *newval, void *extra);
+
+void
+_PG_init()
+{
+	/*TODO: error messages */
+	if (!process_shared_preload_libraries_in_progress)
+		elog(ERROR, "icu_multilib must be loaded by shared_preload_libraries");
+
+	prev_icu_library_hook = get_icu_library_hook;
+	get_icu_library_hook = icu_multilib_hook;
+
+	initialize_gucs_1();
+	load_all_libraries();
+	initialize_gucs_2();
+}
+
+/* initialize GUCs before loading ICU libraries */
+static void
+initialize_gucs_1()
+{
+	/*
+	 * The library search path is initialized once per server start and not
+	 * changable -- this keeps the memory usage in TopMemoryContext bounded.
+	 */
+	DefineCustomStringVariable("icu_multilib.library_path",
+							   "Filesystem path where ICU libraries are installed.",
+							   NULL,
+							   &icu_library_path,
+							   "",
+							   PGC_POSTMASTER,
+							   0, NULL, NULL, NULL);
+	DefineCustomEnumVariable("icu_multilib.version_mismatch_log_level",
+							 "Level of log message when a collator version mismatch is detected.",
+							 NULL,
+							 &version_mismatch_log_level,
+							 WARNING,
+							 log_level_options,
+							 PGC_SUSET,
+							 0, NULL, NULL, NULL);
+	DefineCustomEnumVariable("icu_multilib.library_search_log_level",
+							 "Level of log messages related to searching for an ICU library.",
+							 NULL,
+							 &library_search_log_level,
+							 DEBUG1,
+							 log_level_options,
+							 PGC_SUSET,
+							 0, NULL, NULL, NULL);
+}
+
+/* initialize GUCs after loading ICU libraries */
+static void
+initialize_gucs_2()
+{
+	DefineCustomStringVariable("icu_multilib.default_icu_version",
+							   "The version of the default ICU library.",
+							   "Can be specified with major and minor versions, or major version only.",
+							   &default_icu_version,
+							   "",
+							   PGC_SUSET,
+							   0, check_string_version, assign_default_version, NULL);
+	DefineCustomBoolVariable("icu_multilib.include_builtin",
+							 "Include built-in ICU library when listing or searching libraries.",
+							 NULL,
+							 &include_builtin,
+							 true,
+							 PGC_SUSET,
+							 0, NULL, NULL, NULL);
+	DefineCustomBoolVariable("icu_multilib.search_by_collator_version",
+							 "Enable searching for the ICU library based on the collator version.",
+							 NULL,
+							 &search_by_collator_version,
+							 false,
+							 PGC_SUSET,
+							 0, NULL, NULL, NULL);
+}
+
+static bool
+parse_icu_version(const char *version, icu_version *out_version)
+{
+	size_t	input_len = strlen(version);
+	int		parse_len;
+	int		parse_major;
+	int		parse_minor;
+
+	if (sscanf(version, "%d.%d%n",
+			   &parse_major, &parse_minor, &parse_len) == 2 &&
+		input_len == parse_len)
+	{
+		out_version->major = parse_major;
+		out_version->minor = parse_minor;
+		return true;
+	}
+	else if (sscanf(version, "%d%n", &parse_major, &parse_len) == 1 &&
+			 input_len == parse_len)
+	{
+		out_version->major = parse_major;
+		out_version->minor = -1;
+		return true;
+	}
+
+	return false;
+}
+
+static bool
+check_string_version(char **newval, void **extra, GucSource source)
+{
+	pg_icu_library	*lib;
+	icu_version		*myextra;
+	icu_version		 parsed_version;
+
+	if (*newval == NULL || *newval[0] == '\0')
+		return true;
+
+	if (!parse_icu_version(*newval, &parsed_version))
+	{
+		GUC_check_errmsg("error parsing ICU version \"%s\"", *newval);
+		GUC_check_errhint("The ICU version should be either in the form \"major.minor\", "
+						  "or just be a plain major version number.");
+		return false;
+	}
+
+	if (parsed_version.major < PG_MIN_ICU_MAJOR ||
+		parsed_version.major > PG_MAX_ICU_MAJOR)
+	{
+		GUC_check_errmsg("major version %d is out of range",
+						 parsed_version.major);
+		GUC_check_errhint("ICU major version must be between %d and %d.",
+						  PG_MIN_ICU_MAJOR, PG_MAX_ICU_MAJOR);
+		return false;
+	}
+
+	lib = get_icu_by_major_version(parsed_version.major);
+	if (!lib)
+	{
+		GUC_check_errmsg("ICU library with major version %d not found",
+						 parsed_version.major);
+		return false;
+	}
+
+	if (parsed_version.minor != -1 && lib->minor_version != parsed_version.minor)
+	{
+		GUC_check_errmsg("ICU library with major version %d has minor version %d; "
+						 "expected minor version %d",
+						 lib->major_version, lib->minor_version,
+						 parsed_version.minor);
+		return false;
+	}
+
+	myextra = guc_malloc(ERROR, sizeof(icu_version));
+	*myextra = parsed_version;
+	*extra = myextra;
+
+	return true;
+}
+
+static void
+assign_default_version(const char *newval, void *extra)
+{
+	icu_version *myextra = (icu_version *) extra;
+
+	if (myextra == NULL)
+		return;
+
+	default_version = *myextra;
+}
+
+static void
+load_all_libraries()
+{
+	pg_icu_library *builtin = get_builtin_icu_library();
+
+	icu_library_table[ICU_LIB_TABLESIZE - 1] = builtin;
+
+	ereport(LOG, (errmsg("icu_multilib: retrieved built-in ICU version %d.%d",
+						 builtin->major_version, builtin->minor_version)));
+
+	for (int major = PG_MAX_ICU_MAJOR; major >= PG_MIN_ICU_MAJOR; major--)
+		icu_library_table[major - PG_MIN_ICU_MAJOR] = load_icu_library(major);
+}
+
+static pg_icu_library *
+get_icu_by_major_version(int major)
+{
+	pg_icu_library *lib;
+	if (major < PG_MIN_ICU_MAJOR || major > PG_MAX_ICU_MAJOR)
+		ereport(ERROR, (errmsg("major version %d is out of range", major)));
+
+	lib = icu_library_table[major - PG_MIN_ICU_MAJOR];
+	return lib;
+}
+
+static void
+get_library_version(const pg_icu_library *lib, int *major, int *minor)
+{
+	UVersionInfo version_info;
+	lib->getICUVersion(version_info);
+	*major = version_info[0];
+	*minor = version_info[1];
+	return;
+}
+
+/*
+ * Fill in out_version (which must have U_MAX_VERSION_STRING_LENGTH bytes
+ * available) with the collator version of the given locale in the given
+ * library. Return false if the collator is not found.
+ */
+static bool
+lib_collator_version(pg_icu_library *lib, const char *locale,
+					 char *out_version)
+{
+	UCollator		*collator;
+	UVersionInfo	 version_info;
+	UErrorCode		 status;
+
+	status = U_ZERO_ERROR;
+	collator = lib->openCollator(locale, &status);
+
+	if (!collator)
+		return false;
+
+	lib->getCollatorVersion(collator, version_info);
+	lib->versionToString(version_info, out_version);
+	lib->closeCollator(collator);
+
+	return true;
+}
+
+/*
+ * Find the right ICU library for the given locale and version. The resulting
+ * library may or may not provide a collator with an exactly-matching
+ * version.
+ *
+ * If search_by_collator_version is set, scan the table (first the built-in
+ * ICU library, then descending order of major versions) to find the first
+ * library that provides a collator of the given locale with a matching
+ * version.
+ *
+ * If no exactly matching version is found, and default_major_version is set,
+ * return the default library.
+ *
+ * Otherwise fall back to the built-in library.
+ */
+static pg_icu_library *
+icu_multilib_hook(Oid collid, const char *locale,
+				  const char *requested_version)
+{
+	return icu_multilib_search(collid, locale, requested_version, true);
+}
+
+/*
+ *
+ */
+static pg_icu_library *
+icu_multilib_search(Oid collid, const char *locale,
+					const char *requested_version, bool logOk)
+{
+	char			 actual_version[U_MAX_VERSION_STRING_LENGTH];
+	pg_icu_library	*found_lib = NULL;
+
+	/*
+	 * If another hook was set first, defer to that unless it returns NULL or
+	 * a library that doesn't contain the given collator at all. This may
+	 * result in a mismatching collator version, but we don't want to
+	 * speculate about what's better or worse in the presence of other hooks.
+	 */
+	if (prev_icu_library_hook)
+	{
+		pg_icu_library *tmp_lib;
+		tmp_lib = prev_icu_library_hook(collid, locale, requested_version);
+		if (tmp_lib && lib_collator_version(tmp_lib, locale, actual_version))
+			found_lib = tmp_lib;
+	}
+
+	if (!found_lib && search_by_collator_version && requested_version != NULL)
+	{
+		/*
+		 * Search from newest library to oldest for a matching version of the
+		 * collator with the given name.
+		 */
+		for (int i = ICU_LIB_TABLESIZE - 1; i >= 0; i--)
+		{
+			char			 tmp_version[U_MAX_VERSION_STRING_LENGTH];
+			pg_icu_library	*tmp_lib = icu_library_table[i];
+
+			if (tmp_lib == NULL)
+				continue;
+
+			if (!include_builtin && i == ICU_LIB_TABLESIZE - 1)
+				continue;
+
+			if (lib_collator_version(tmp_lib, locale, tmp_version) &&
+				strcmp(requested_version, tmp_version) == 0)
+			{
+				strcpy(actual_version, tmp_version);
+				found_lib = tmp_lib;
+				break;
+			}
+		}
+	}
+
+	if (!found_lib && default_version.major != -1)
+	{
+		pg_icu_library *tmp_lib;
+		tmp_lib = get_icu_by_major_version(default_version.major);
+		if (!tmp_lib)
+			ereport(ERROR,
+					(errmsg("icu_multilib: default major version %d not found",
+							default_version.major)));
+		if (lib_collator_version(tmp_lib, locale, actual_version))
+			found_lib = tmp_lib;
+		else if (logOk)
+			ereport(library_search_log_level,
+					(errmsg("icu_multilib: found default ICU %d.%d, but collator \"%s\" not found",
+							tmp_lib->major_version, tmp_lib->minor_version, locale)));
+	}
+
+	if (!found_lib && include_builtin)
+	{
+		pg_icu_library *tmp_lib;
+		tmp_lib = icu_library_table[ICU_LIB_TABLESIZE - 1];
+		if (lib_collator_version(tmp_lib, locale, actual_version))
+			found_lib = tmp_lib;
+		else if (logOk)
+			ereport(library_search_log_level,
+					(errmsg("icu_multilib: found built-in ICU %d.%d, but collator \"%s\" not found",
+							tmp_lib->major_version, tmp_lib->minor_version, locale)));
+	}
+
+	/* if not found, fall back to built-in or other hook */
+	if (!found_lib)
+		return NULL;
+
+	if (logOk)
+		ereport(library_search_log_level,
+				(errmsg("icu_multilib: found ICU version %d.%d providing collator version \"%s\" for locale \"%s\"",
+						found_lib->major_version, found_lib->minor_version,
+						actual_version, locale)));
+
+	/*
+	 * This is somewhat redundant with a similar warning in pg_locale.c, but
+	 * it provides details about the locale name and ICU version, which is
+	 * helpful when multiple ICU libraries are in use.
+	 */
+	if (requested_version && logOk &&
+		strcmp(requested_version, actual_version) != 0)
+	{
+		ereport(version_mismatch_log_level,
+				(errmsg("icu_multilib: collator version mismatch detected for locale \"%s\"",
+						locale),
+				 errdetail("ICU %d.%d provides collator version \"%s\" for locale \"%s\"; expected version \"%s\".",
+						   found_lib->major_version, found_lib->minor_version,
+						   actual_version, locale, requested_version)));
+	}
+
+	return found_lib;
+}
+
+PG_FUNCTION_INFO_V1(library_versions);
+Datum
+library_versions(PG_FUNCTION_ARGS)
+{
+#define LIBRARY_VERSIONS_COLS 5
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	Datum           values[LIBRARY_VERSIONS_COLS];
+	bool            nulls[LIBRARY_VERSIONS_COLS];
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	for (int i = ICU_LIB_TABLESIZE - 1; i >= 0; i--)
+	{
+		UErrorCode      status;
+		UVersionInfo version_info;
+		char            version_string[U_MAX_VERSION_STRING_LENGTH];
+		pg_icu_library	*lib = icu_library_table[i];
+
+		if (lib == NULL)
+			continue;
+
+		if (!include_builtin && i == ICU_LIB_TABLESIZE - 1)
+			continue;
+
+		lib->getICUVersion(version_info);
+		lib->versionToString(version_info, version_string);
+		values[0] = PointerGetDatum(cstring_to_text(version_string));
+		nulls[0] = false;
+
+		lib->getUnicodeVersion(version_info);
+		lib->versionToString(version_info, version_string);
+		values[1] = PointerGetDatum(cstring_to_text(version_string));
+		nulls[1] = false;
+		status = U_ZERO_ERROR;
+		lib->getCLDRVersion(version_info, &status);
+		if (U_SUCCESS(status))
+		{
+			lib->versionToString(version_info, version_string);
+			values[2] = PointerGetDatum(cstring_to_text(version_string));
+			nulls[2] = false;
+		}
+		else
+		{
+			nulls[2] = true;
+		}
+
+		values[3] = PointerGetDatum(cstring_to_text(lib->libicui18n_name));
+		nulls[3] = false;
+
+		values[4] = PointerGetDatum(cstring_to_text(lib->libicuuc_name));
+		nulls[4] = false;
+
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+	}
+
+	return (Datum) 0;
+}
+
+#define COLLATOR_DETAIL_COLS 3
+static void
+make_collator_detail_record(pg_icu_library *lib, const char *locale,
+							Datum *values, bool *nulls)
+{
+	UErrorCode		 status;
+	UCollator		*collator;
+	UVersionInfo	 version_info;
+	char			 version_string[U_MAX_VERSION_STRING_LENGTH];
+
+	status = U_ZERO_ERROR;
+	collator = lib->openCollator(locale, &status);
+
+	lib->getICUVersion(version_info);
+	lib->versionToString(version_info, version_string);
+	values[0] = PointerGetDatum(cstring_to_text(version_string));
+	nulls[0] = false;
+
+	if (collator)
+	{
+		lib->getUCAVersion(collator, version_info);
+		lib->versionToString(version_info, version_string);
+		values[1] = PointerGetDatum(cstring_to_text(version_string));
+		nulls[1] = false;
+	}
+	else
+		nulls[1] = true;
+
+	if (collator)
+	{
+		lib->getCollatorVersion(collator, version_info);
+		lib->versionToString(version_info, version_string);
+		values[2] = PointerGetDatum(cstring_to_text(version_string));
+		nulls[2] = false;
+	}
+	else
+		nulls[2] = true;
+
+	if (collator)
+		lib->closeCollator(collator);
+
+	return;
+}
+
+PG_FUNCTION_INFO_V1(library_collators);
+Datum
+library_collators(PG_FUNCTION_ARGS)
+{
+#define LIBRARY_COLLATORS_COLS (COLLATOR_DETAIL_COLS + 1)
+	pg_icu_library	*lib  = NULL;
+	ReturnSetInfo	*rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	Datum			 values[LIBRARY_COLLATORS_COLS];
+	bool			 nulls[LIBRARY_COLLATORS_COLS];
+
+	if (PG_ARGISNULL(0))
+	{
+		lib = icu_library_table[ICU_LIB_TABLESIZE - 1];
+	}
+	else
+	{
+		char *major_str = text_to_cstring(PG_GETARG_TEXT_PP(0));
+		int major = atoi(major_str); /* TODO? */
+
+		if (major < PG_MIN_ICU_MAJOR || major > PG_MAX_ICU_MAJOR)
+			ereport(ERROR, (errmsg("icu_multilib: major version %d is out of range", major)));
+		lib = get_icu_by_major_version(major);
+		if (!lib)
+			ereport(ERROR, (errmsg("icu_multilib: major version %d not found", major)));
+	}
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	for (int i = -1; i < lib->countAvailable(); i++)
+	{
+		const char	*name;
+		const char	*locale;
+		UErrorCode	 status;
+		char		 langtag[ULOC_FULLNAME_CAPACITY];
+
+		if (i == -1)
+			name = "";		/* ICU root locale */
+		else
+			name = lib->getAvailable(i);
+
+		status = U_ZERO_ERROR;
+		lib->toLanguageTag(name, langtag, sizeof(langtag), true, &status);
+
+		if (U_FAILURE(status))
+			continue;
+
+		locale = U_ICU_VERSION_MAJOR_NUM >= 54 ? langtag : name;
+
+		if (!pg_is_ascii(langtag) || !pg_is_ascii(locale))
+			continue;
+
+		values[0] = PointerGetDatum(cstring_to_text(locale));
+		nulls[0] = false;
+
+		make_collator_detail_record(lib, locale, values + 1, nulls + 1);
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+	}
+
+	return (Datum) 0;
+}
+
+PG_FUNCTION_INFO_V1(collator_version_search);
+Datum
+collator_version_search(PG_FUNCTION_ARGS)
+{
+#define COLLATOR_VERSION_SEARCH_COLS COLLATOR_DETAIL_COLS
+	const char		*locale;
+	const char		*requested_version = NULL;
+	bool			 logOk;
+	pg_icu_library	*lib;
+	int				 major, minor;
+	TupleDesc		 tupdesc;
+	HeapTuple		 tuple;
+	Datum			 values[COLLATOR_VERSION_SEARCH_COLS];
+	bool			 nulls[COLLATOR_VERSION_SEARCH_COLS];
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR, (errmsg("locale argument must be non-NULL")));
+	if (PG_ARGISNULL(2))
+		ereport(ERROR, (errmsg("log_ok argument must be non-NULL")));
+
+	locale = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	if (!PG_ARGISNULL(1))
+		requested_version = text_to_cstring(PG_GETARG_TEXT_PP(1));
+	logOk = PG_GETARG_BOOL(2);
+
+	lib = icu_multilib_search(InvalidOid, locale, requested_version, logOk);
+	get_library_version(lib, &major, &minor);
+
+	make_collator_detail_record(lib, locale, values, nulls);
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+
+	PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+}
+
+PG_FUNCTION_INFO_V1(collator_versions);
+Datum
+collator_versions(PG_FUNCTION_ARGS)
+{
+#define COLLATOR_VERSIONS_COLS COLLATOR_DETAIL_COLS
+	const char *locale;
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	Datum           values[COLLATOR_VERSIONS_COLS];
+	bool            nulls[COLLATOR_VERSIONS_COLS];
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR, (errmsg("locale argument must be non-NULL")));
+	locale = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	for (int i = ICU_LIB_TABLESIZE - 1; i >= 0; i--)
+	{
+		pg_icu_library	*lib = icu_library_table[i];
+
+		if (lib == NULL)
+			continue;
+
+		if (!include_builtin && i == ICU_LIB_TABLESIZE - 1)
+			continue;
+
+		make_collator_detail_record(lib, locale, values, nulls);
+
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+	}
+
+	return (Datum) 0;
+}
+
+static void
+make_icu_library_names(int major, char **libicui18n, char **libicuuc)
+{
+	char libicui18n_tmp[MAXPGPATH];
+	char libicuuc_tmp[MAXPGPATH];
+
+	/*
+	 * See
+	 * https://unicode-org.github.io/icu/userguide/icu4c/packaging.html#icu-versions
+	 * for conventions on library naming on POSIX and Windows systems.  Apple
+	 * isn't mentioned but varies in the usual way.
+	 *
+	 * Format 1 is expected to be a major version-only symlink pointing to a
+	 * specific minor version (or on Windows it may be the actual library).
+	 * Format 2 is expected to be an actual library.
+	 */
+#ifdef WIN32
+#define ICU_LIBRARY_NAME_FORMAT1 "%s%sicu%s%d" DLSUFFIX
+#elif defined(__darwin__)
+#define ICU_LIBRARY_NAME_FORMAT1 "%s%slibicu%s.%d" DLSUFFIX
+#else
+#define ICU_LIBRARY_NAME_FORMAT1 "%s%slibicu%s" DLSUFFIX ".%d"
+#endif
+
+#ifdef WIN32
+#define PATH_SEPARATOR "\\"
+#define ICU_I18N "in"
+#define ICU_UC "uc"
+#else
+#define PATH_SEPARATOR "/"
+#define ICU_I18N "i18n"
+#define ICU_UC "uc"
+#endif
+
+	snprintf(libicui18n_tmp,
+			 MAXPGPATH,
+			 ICU_LIBRARY_NAME_FORMAT1,
+			 icu_library_path, icu_library_path[0] ? PATH_SEPARATOR : "",
+			 "i18n", major);
+	snprintf(libicuuc_tmp,
+			 MAXPGPATH,
+			 ICU_LIBRARY_NAME_FORMAT1,
+			 icu_library_path, icu_library_path[0] ? PATH_SEPARATOR : "",
+			 "uc", major);
+
+	*libicui18n = pstrdup(libicui18n_tmp);
+	*libicuuc = pstrdup(libicuuc_tmp);
+}
+
+#define MAX_SYMBOL_LEN 128
+static void *
+load_icu_function(void *handle, const char *function, int major)
+{
+	char     function_with_version[MAX_SYMBOL_LEN];
+	int		 ret;
+	void    *result;
+
+	/*
+	 * Try to look up the symbol with the library major version as a suffix.
+	 */
+	ret = snprintf(function_with_version, sizeof(function_with_version),
+				   "%s_%d", function, major);
+	if (ret >= sizeof(function_with_version))
+		elog(ERROR, "icu_multilib: cannot construct symbol name");
+	result = dlsym(handle, function_with_version);
+
+	/*
+	 * Library may have been configured with --disable-renaming, try without
+	 * major version suffix.
+	 */
+	if (result == NULL)
+		result = dlsym(handle, function);
+
+	return result;
+}
+
+#define LOAD_FUNC(DEST, LIBHANDLE, LIBNAME, FNAME)	\
+	do {										\
+		DEST = load_icu_function((LIBHANDLE), (FNAME), major);	\
+		if (DEST == NULL)						\
+		{										\
+			ereport(WARNING,					\
+					(errmsg("icu_multilib: could not find symbol \"%s\" in library \"%s\"",	\
+							(FNAME), (LIBNAME))));	\
+			goto err;							\
+		}										\
+	} while(0)
+
+#define LOAD_FUNC_I18N(DEST, FNAME) \
+	LOAD_FUNC(DEST, libicui18n_handle, libicui18n, FNAME)
+#define LOAD_FUNC_UC(DEST, FNAME) \
+	LOAD_FUNC(DEST, libicuuc_handle, libicuuc, FNAME)
+
+static pg_icu_library *
+load_icu_library(int major)
+{
+	UVersionInfo	 version_info;
+	pg_icu_library	*lib;
+	char			*libicui18n;
+	char			*libicuuc;
+	void			*libicui18n_handle = NULL;
+	void			*libicuuc_handle   = NULL;
+
+	make_icu_library_names(major, &libicui18n, &libicuuc);
+
+	lib = MemoryContextAllocZero(TopMemoryContext, sizeof(pg_icu_library));
+
+	libicui18n_handle = dlopen(libicui18n, RTLD_NOW | RTLD_LOCAL);
+	if (!libicui18n_handle)
+		return NULL;
+
+	/* Load the common library. */
+	libicuuc_handle = dlopen(libicuuc, RTLD_NOW | RTLD_LOCAL);
+	if (!libicui18n_handle)
+	{
+		elog(WARNING, "found ICU library \"%s\" but not companion library \"%s\"",
+			 libicui18n, libicuuc);
+		dlclose(libicui18n_handle);
+		return NULL;
+	}
+
+	/*
+	 * We only allocate the pg_icu_library object after successfully
+	 * opening the libraries to minimize the work done in the ENOENT case,
+	 * when probing a range of versions.  That means we might need to
+	 * clean up on allocation failure.
+	 */
+	lib = MemoryContextAllocExtended(TopMemoryContext, sizeof(*lib),
+									 MCXT_ALLOC_NO_OOM);
+	lib->libicui18n_name = MemoryContextAllocExtended(
+		TopMemoryContext, strlen(libicui18n) + 1, MCXT_ALLOC_NO_OOM);
+	lib->libicuuc_name = MemoryContextAllocExtended(
+		TopMemoryContext, strlen(libicuuc) + 1, MCXT_ALLOC_NO_OOM);
+
+	if (!lib || !lib->libicui18n_name || !lib->libicuuc_name)
+	{
+		dlclose(libicui18n_handle);
+		dlclose(libicuuc_handle);
+		elog(ERROR, "out of memory");
+	}
+
+	strcpy(lib->libicui18n_name, libicui18n);
+	strcpy(lib->libicuuc_name, libicuuc);
+
+	pfree(libicui18n);
+	pfree(libicuuc);
+
+	/* try to find all the symbols we need from the i18n library */
+	LOAD_FUNC_I18N(lib->getICUVersion, "u_getVersion");
+	LOAD_FUNC_I18N(lib->getUnicodeVersion, "u_getUnicodeVersion");
+	LOAD_FUNC_I18N(lib->getCLDRVersion, "ulocdata_getCLDRVersion");
+	LOAD_FUNC_I18N(lib->openCollator, "ucol_open");
+	LOAD_FUNC_I18N(lib->closeCollator, "ucol_close");
+	LOAD_FUNC_I18N(lib->getCollatorVersion, "ucol_getVersion");
+	LOAD_FUNC_I18N(lib->getUCAVersion, "ucol_getUCAVersion");
+	LOAD_FUNC_I18N(lib->versionToString, "u_versionToString");
+	LOAD_FUNC_I18N(lib->strcoll, "ucol_strcoll");
+	LOAD_FUNC_I18N(lib->strcollUTF8, "ucol_strcollUTF8");
+	LOAD_FUNC_I18N(lib->getSortKey, "ucol_getSortKey");
+	LOAD_FUNC_I18N(lib->nextSortKeyPart, "ucol_nextSortKeyPart");
+	LOAD_FUNC_I18N(lib->setUTF8, "uiter_setUTF8");
+	LOAD_FUNC_I18N(lib->errorName, "u_errorName");
+	LOAD_FUNC_I18N(lib->setAttribute, "ucol_setAttribute");
+
+	/* try to find all the symbols we need from the uc library */
+	LOAD_FUNC_UC(lib->strToUpper, "u_strToUpper");
+	LOAD_FUNC_UC(lib->strToLower, "u_strToLower");
+	LOAD_FUNC_UC(lib->strToTitle, "u_strToTitle");
+	LOAD_FUNC_UC(lib->openConverter, "ucnv_open");
+	LOAD_FUNC_UC(lib->closeConverter, "ucnv_close");
+	LOAD_FUNC_UC(lib->fromUChars, "ucnv_fromUChars");
+	LOAD_FUNC_UC(lib->toUChars, "ucnv_toUChars");
+	LOAD_FUNC_UC(lib->toLanguageTag, "uloc_toLanguageTag");
+	LOAD_FUNC_UC(lib->getDisplayName, "uloc_getDisplayName");
+	LOAD_FUNC_UC(lib->countAvailable, "uloc_countAvailable");
+	LOAD_FUNC_UC(lib->getAvailable, "uloc_getAvailable");
+
+	lib->getICUVersion(version_info);
+	lib->major_version = version_info[0];
+	lib->minor_version = version_info[1];
+
+	if (lib->major_version != major)
+		ereport(version_mismatch_log_level,
+				(errmsg("loaded library for major version %d, but library reports major version %d",
+						major, lib->major_version)));
+
+	ereport(LOG, (errmsg("icu_multilib: loaded ICU version %d.%d",
+						 lib->major_version, lib->minor_version)));
+
+	return lib;
+
+err:
+	dlclose(libicui18n_handle);
+	dlclose(libicuuc_handle);
+	pfree(lib->libicui18n_name);
+	pfree(lib->libicuuc_name);
+	pfree(lib);
+	return NULL;
+}
diff --git a/contrib/icu_multilib/icu_multilib.control b/contrib/icu_multilib/icu_multilib.control
new file mode 100644
index 0000000000..8f2c546781
--- /dev/null
+++ b/contrib/icu_multilib/icu_multilib.control
@@ -0,0 +1,7 @@
+# icu_multlib extension
+comment = 'support multiple ICU library versions'
+default_version = '1.0'
+module_pathname = '$libdir/icu_multilib'
+relocatable = false
+schema = icu_multilib
+trusted = true
diff --git a/contrib/icu_multilib/meson.build b/contrib/icu_multilib/meson.build
new file mode 100644
index 0000000000..f2ea4088e4
--- /dev/null
+++ b/contrib/icu_multilib/meson.build
@@ -0,0 +1,24 @@
+# Copyright (c) 2022-2023, PostgreSQL Global Development Group
+
+icu_multilib_sources = files(
+  'icu_multilib.c',
+)
+
+if host_system == 'windows'
+  icu_multilib_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'icu_multilib',
+    '--FILEDESC', 'icu_multilib -- support multiple ICU library versions',])
+endif
+
+icu_multilib = shared_module('icu_multilib',
+  icu_multilib_sources,
+  kwargs: contrib_mod_args,
+)
+contrib_targets += icu_multilib
+
+install_data(
+  'icu_multilib.control',
+  'icu_multilib--1.0.sql',
+  kwargs: contrib_data_args,
+)
+
diff --git a/contrib/meson.build b/contrib/meson.build
index bd4a57c43c..03f72147cf 100644
--- a/contrib/meson.build
+++ b/contrib/meson.build
@@ -28,6 +28,9 @@ subdir('fuzzystrmatch')
 subdir('hstore')
 subdir('hstore_plperl')
 subdir('hstore_plpython')
+if icu.found()
+   subdir('icu_multilib')
+endif
 subdir('intagg')
 subdir('intarray')
 subdir('isn')
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index 4e7b87a42f..469a1746b9 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -113,6 +113,7 @@ CREATE EXTENSION <replaceable>module_name</replaceable>;
  &file-fdw;
  &fuzzystrmatch;
  &hstore;
+ &icu-multilib;
  &intagg;
  &intarray;
  &isn;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 0d6be9a2fa..091ce0a60b 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -131,6 +131,7 @@
 <!ENTITY file-fdw        SYSTEM "file-fdw.sgml">
 <!ENTITY fuzzystrmatch   SYSTEM "fuzzystrmatch.sgml">
 <!ENTITY hstore          SYSTEM "hstore.sgml">
+<!ENTITY icu-multilib    SYSTEM "icu-multilib.sgml">
 <!ENTITY intagg          SYSTEM "intagg.sgml">
 <!ENTITY intarray        SYSTEM "intarray.sgml">
 <!ENTITY isn             SYSTEM "isn.sgml">
diff --git a/doc/src/sgml/icu-multilib.sgml b/doc/src/sgml/icu-multilib.sgml
new file mode 100644
index 0000000000..838872c880
--- /dev/null
+++ b/doc/src/sgml/icu-multilib.sgml
@@ -0,0 +1,496 @@
+<!-- doc/src/sgml/icu-multilib.sgml -->
+
+<sect1 id="icu-multilib" xreflabel="icu_multilib">
+ <title>icu_multilib</title>
+
+ <indexterm zone="icu-multilib">
+  <primary>icu_multilib</primary>
+ </indexterm>
+
+ <para>
+  The <filename>icu_multilib</filename> module provides control over the
+  version (or versions) of the ICU provider library used by
+  <productname>PostgreSQL</productname>, which can be different from the
+  version of ICU with which it was built.
+ </para>
+
+ <para>
+  Collations are a product of natural language, and natural language evolves
+  over time; but <productname>PostgreSQL</productname> depends on stable
+  ordering for structures such as indexes. Newer versions of ICU update the
+  provided collators to adapt to changes in natural language, so it's
+  important to control when and how those new versions of ICU are used to
+  prevent problems such as index corruption.
+ </para>
+
+ <para>
+  This module assumes that the necessary versions of ICU are already
+  available, such as through the operating system's package manager; and
+  already properly installed in a single location accessible to
+  <productname>PostgreSQL</productname>. The configration variable
+  <literal>icu_multilib.library_path</literal> should be set to the location
+  where these ICU library versions are installed.
+ </para>
+
+ <para>
+  <filename>icu_multilib</filename> must be loaded via
+  <literal>shared_preload_libraries</literal>.
+  <filename>icu_multilib</filename> ignores any ICU library with a major
+  version greater than that with which <productname>PostgreSQL</productname>
+  was built.
+ </para>
+
+ <sect2 id="icu-multilib-version-stability">
+  <title>ICU Version Stability</title>
+
+  <para>
+   The simplest way to use <filename>icu_multilib</filename> is to force ICU
+   version stability. Version stability means that
+   <productname>PostgreSQL</productname> will use the same version of ICU for
+   all collations, even if the operating system or
+   <productname>PostgreSQL</productname> itself is upgraded or moved. The
+   stable version may or may not be the same as that with which
+   <productname>PostgreSQL</productname> was built.
+  </para>
+
+  <para>
+   Version stability may be based on both the major and minor version of ICU,
+   such as <literal>68.2</literal>; or on the major version alone, such as
+   <literal>68</literal>. Specifying both the major and minor versions more
+   closely controls exactly which ICU library is loaded, but applying minor
+   version upgrades of the ICU library may interfere with normal
+   operation. Specifying the major version alone makes applying ICU minor
+   version updates simpler, but carries a risk of subtle differences in
+   collation order if the library is updated.
+  </para>
+
+  <para>
+   To configure for version stability, first make sure that
+   <varname>icu_multilib.library_path</varname> is set correctly, which you
+   can observe by executing the
+   <literal>icu_multilib.library_versions()</literal> function. Then set
+   <varname>icu_multilib.default_icu_version</varname> to the stable version,
+   and set <varname>icu_multilib.search_by_collator_version</varname> to
+   <literal>false</literal>. These configuration variables should be set in
+   <filename>postgresql.conf</filename> to ensure that the stable version of
+   ICU is used consistently.
+  </para>
+
+  <note>
+   <para>
+    Ensure that the stable version of the ICU library is always available,
+    including on all replicas and through operating system and
+    <productname>PostgreSQL</productname> upgrades.
+   </para>
+   <para>
+    Similarly, keep the settings in <filename>postgresql.conf</filename>, and
+    keep them consistent across replicas and upgrades.
+   </para>
+  </note>
+
+  <para>
+   For best results, stabilize the ICU version immediately after
+   <command>initdb</command> is run, to ensure that initial collations are
+   loaded from the stabilized version of the ICU library. To do this, use the
+   option <xref linkend="app-initdb-option-no-import-collations"/> for
+   <command>initdb</command>, import the collations manually, and then refresh
+   the versions for the default collations. For instance:
+  </para>
+
+<programlisting>
+$ initdb --no-import-collations -D data --locale-provider=icu --icu-locale="en_US" --locale="en_US.UTF-8"
+$ cat - >> data/postgresql.conf
+shared_preload_libraries = 'icu_multilib'
+icu_multilib.library_path = '/path/to/icu/lib'
+icu_multilib.default_icu_version = '65.1'
+icu_multilib.search_by_collator_version = false
+icu_multilib.include_builtin = false
+^D
+$ pg_ctl -D data -l logfile start
+$ psql template1
+WARNING:  icu_multilib: collator version mismatch detected for locale "en_US"
+DETAIL:  ICU 65.1 provides collator version "153.97" for locale "en_US"; expected version "153.112".
+WARNING:  icu_multilib: collator version mismatch detected for locale "en_US"
+DETAIL:  ICU 65.1 provides collator version "153.97" for locale "en_US"; expected version "153.112".
+WARNING:  database "template1" has a collation version mismatch
+DETAIL:  The database was created using collation version 153.112, but the operating system provides version 153.97.
+HINT:  Rebuild all objects in this database that use the default collation and run ALTER DATABASE template1 REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
+=# ALTER DATABASE template1 REFRESH COLLATION VERSION;
+NOTICE:  changing version from 153.112 to 153.97
+ALTER DATABASE
+=# SELECT pg_import_system_collations('pg_catalog');
+ pg_import_system_collations
+-----------------------------
+                         792
+(1 row)
+
+=# \q
+$ psql postgres
+WARNING:  icu_multilib: collator version mismatch detected for locale "en_US"
+DETAIL:  ICU 65.1 provides collator version "153.97" for locale "en_US"; expected version "153.112".
+WARNING:  icu_multilib: collator version mismatch detected for locale "en_US"
+DETAIL:  ICU 65.1 provides collator version "153.97" for locale "en_US"; expected version "153.112".
+WARNING:  database "postgres" has a collation version mismatch
+DETAIL:  The database was created using collation version 153.112, but the operating system provides version 153.97.
+HINT:  Rebuild all objects in this database that use the default collation and run ALTER DATABASE postgres REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
+=# ALTER DATABASE postgres REFRESH COLLATION VERSION;
+NOTICE:  changing version from 153.112 to 153.97
+ALTER DATABASE
+=# SELECT pg_import_system_collations('pg_catalog');
+ pg_import_system_collations
+-----------------------------
+                         792
+(1 row)
+
+=# \q
+$
+</programlisting>
+
+  <para>
+   The result will be a system based on ICU version <literal>65.1</literal>, and
+   <productname>PostgreSQL</productname> will use that version of ICU for all
+   collations that use the ICU provider.
+  </para>
+ </sect2>
+
+ <sect2 id="icu-multi-version">
+  <title>Multiple ICU Version Support</title>
+  <para>
+   Support for multiple ICU library versions in the same database allows the
+   system to adapt to changes in natural language over time. As these changes
+   are introduced in new versions of ICU, those new versions can be brought
+   into <productname>PostgreSQL</productname> incrementally without
+   interfering with existing data (or structures like indexes).
+  </para>
+  <sect3>
+   <title>Search by Collator Version</title>
+
+   <para>
+    Collators provided by ICU also have a version which is distinct from the
+    version of ICU. For instance, in ICU version <literal>70.1</literal>, the
+    collator for the <literal>en_US</literal> locale has version
+    <literal>153.112</literal>. A collator for a given locale may or may not
+    be assigned a new version when the ICU library version is updated.
+   </para>
+
+   <para>
+    When a collation is created, <productname>PostgreSQL</productname> obtains
+    the version of the collator from the ICU library and records it in the
+    catalog. This recorded version can be updated using <literal>ALTER
+    COLLATION ... REFRESH VERSION</literal>; or <literal>ALTER DATABASE
+    ... REFRESH COLLATION VERSION</literal> if it's the database's default
+    collation. These <literal>ALTER</literal> commands obtain the new collator
+    version from the ICU library identified by
+    <varname>icu_multilib.default_icu_version</varname> if set; otherwise they
+    obtain the new collator version from the built-in ICU library.
+   </para>
+
+   <para>
+    Set <varname>icu_multilib.search_by_collator_version</varname> to
+    <literal>true</literal> to enable <filename>icu_multilib</filename> to
+    search (in descending order of ICU major version) among the
+    available libraries to find the first one that provides a collator with a
+    version that matches the one recorded in the catalog.
+   </para>
+
+   <para>
+    For example, ICU versions <literal>62.2</literal> and
+    <literal>63.2</literal> both provide collator version
+    <literal>153.88</literal> for the locale <literal>en_US</literal>; while
+    ICU version <literal>64.2</literal> provides collator version
+    <literal>153.97</literal> for the same locale. Searching for an ICU
+    library for a collation with a recorded version of
+    <literal>153.88</literal> will find the ICU library with version
+    <literal>63.2</literal>, because that's the ICU library with the highest
+    major version (<literal>63</literal>) that provides collator version
+    <literal>153.88</literal>. The function
+    <function>icu_multilib.collator_version_search</function> simulates this
+    search for the given locale and version:
+   </para>
+<programlisting>
+SELECT * FROM icu_multilib.collator_version_search('en_US', '153.88');
+ icu_version | uca_version | collator_version
+-------------+-------------+------------------
+ 63.2        | 11.0        | 153.88
+(1 row)
+
+</programlisting>
+
+   <note>
+    <para>
+     Searching by collator version relies on the assumption that collators
+     with the same version behave identically regardless of which ICU library
+     provides it. The administrator must weigh the risks of this assumption
+     against the convenience when managing multiple major versions of ICU.
+    </para>
+   </note>
+  </sect3>
+ </sect2>
+
+ <sect2 id="icu-multilib-configuration-parameters">
+  <title>Configuration Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term>
+     <varname>icu_multilib.library_path</varname> (<type>string</type>)
+     <indexterm>
+      <primary><varname>icu_multilib.library_path</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      The filesystem path in which to search for ICU libraries. Must be set
+      for <filename>icu_multilib</filename> to be useful. Multiple major
+      versions of ICU may be present; but for each major version, only a
+      single minor version can be present.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>icu_multilib.version_mismatch_log_level</varname> (<type>string</type>)
+     <indexterm>
+      <primary><varname>icu_multilib.version_mismatch_log_level</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      A version mismatch happens when the version of a collator provided by
+      the ICU library is different than then collator version recorded when
+      the collation was created (or last refreshed).
+     </para>
+     <para>
+      When a version mismatch is detected, a log message is emitted at this
+      level. <literal>WARNING</literal> is the default. If the level is set to
+      <literal>ERROR</literal>, it may interfere with normal operation.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>icu_multilib.library_search_log_level</varname> (<type>string</type>)
+     <indexterm>
+      <primary><varname>icu_multilib.library_search_log_level</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      <filename>icu_multilib</filename> emits messages at this level while
+      determining which ICU library to choose. The default is
+      <literal>DEBUG1</literal>. This setting is useful to diagnose problems
+      when a suitable ICU library is not found.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>icu_multilib.default_icu_version</varname> (<type>string</type>)
+     <indexterm>
+      <primary><varname>icu_multilib.default_icu_version</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Identifies the version of the ICU library to choose; or, if <varname>icu_multilib.search_by_collator_version</varname> is <literal>true</literal>, the ICU library to choose if no identical match to the collator version is found. The default is the empty string, meaning to use the built-in ICU library.
+     </para>
+     <para>
+      Valid values are either of the form <literal>major.minor</literal> such
+      as <literal>70.1</literal>; or a major version number alone, such as
+      <literal>70</literal>.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>icu_multilib.include_builtin</varname> (<type>string</type>)
+     <indexterm>
+      <primary><varname>icu_multilib.include_builtin</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Determines whether the built-in version of ICU is included when
+      searching for a suitable ICU library. The default is
+      <literal>true</literal>.
+     </para>
+     <para>
+      Even if set to <literal>false</literal>, the built-in ICU library may be
+      used if no other suitable ICU library is identified and
+      <varname>icu_multilib.default_icu_version</varname> is set to the empty
+      string.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <varname>icu_multilib.search_by_collator_version</varname> (<type>string</type>)
+     <indexterm>
+      <primary><varname>icu_multilib.search_by_collator_version</varname> configuration parameter</primary>
+     </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Causes <filename>icu_multilib</filename> to identify the ICU library
+      with the highest major version that offers the required collator with an
+      exactly-matching collator version. The default is <literal>false</literal>.
+     </para>
+     <para>
+      When set to <literal>true</literal>, <filename>icu_multilib</filename>
+      relies on equal collator versions to produce identical collation order.
+     </para>
+     <para>
+      If no ICU library is found with an exactly-matching collator version,
+      <filename>icu_multilib</filename> will fall back to the
+      <varname>icu_multilib.default_icu_version</varname>; or if not set, to
+      the built-in ICU library.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </sect2>
+
+ <sect2>
+  <title>Functions</title>
+
+  <table id="icu-multilib-functions">
+   <title><filename>icu_multilib</filename> Functions</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="func_table_entry"><para role="func_signature">
+       Function
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="func_table_entry"><para role="func_signature">
+       <function>icu_multilib.library_versions</function> ()
+       <returnvalue>setof record</returnvalue>
+       ( <parameter>icu_version</parameter> <type>text</type>,
+       <parameter>unicode_version</parameter> <type>text</type>,
+       <parameter>cldr_version</parameter> <type>text</type>,
+       <parameter>libicui18n_name</parameter> <type>text</type>,
+       <parameter>libicuuc_name</parameter> <type>text</type>)
+      </para>
+      <para>
+       Returns details for each available ICU library found in
+       <varname>icu_multilib.library_path</varname>. Also includes the
+       built-in ICU library iv <varname>icu_multilib.include_builtin</varname>
+       is <literal>true</literal>.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="func_table_entry"><para role="func_signature">
+       <function>icu_multilib.library_collators</function> ( <parameter>major_version</parameter> <type>text</type> <literal>DEFAULT</literal> <literal>null</literal> )
+       <returnvalue>setof record</returnvalue>
+       ( <parameter>locale</parameter> <type>text</type>,
+       <parameter>icu_version</parameter> <type>text</type>,
+       <parameter>uca_version</parameter> <type>text</type>,
+       <parameter>collator_version</parameter> <type>text</type> )
+      </para>
+      <para>
+       Returns details for all available collators provided by the ICU library
+       with the given <parameter>major_version</parameter>; or by the built-in
+       ICU library if <parameter>major_version</parameter> is
+       <literal>null</literal>.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="func_table_entry"><para role="func_signature">
+       <function>icu_multilib.collator_version_search</function> ( <parameter>locale</parameter> <type>text</type>, <parameter>requested_version</parameter> <type>text</type> <literal>DEFAULT</literal> <literal>null</literal>, <parameter>log_ok</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal> )
+       <returnvalue>record</returnvalue>
+       ( <parameter>icu_version</parameter> <type>text</type>,
+       <parameter>uca_version</parameter> <type>text</type>,
+       <parameter>collator_version</parameter> <type>text</type> )
+      </para>
+      <para>
+       Performs a search for the appropriate ICU library given the locale name
+       and requested collator version, and returns details about the ICU
+       library.
+      </para>
+      <para>
+       If <parameter>requested_version</parameter> is <literal>null</literal>,
+       it will return the ICU library identified by
+       <varname>icu_multilib.default_icu_version</varname> if set to
+       <literal>true</literal>; otherwise the built-in ICU library.
+      </para>
+      <para>
+       If <parameter>log_ok</parameter> is <literal>true</literal>, the search
+       may emit log messages at the level
+       <varname>icu_multilib.library_search_log_level</varname>, which may be
+       useful for diagnosing misconfiguration.
+      </para></entry>
+     </row>
+     <row>
+      <entry role="func_table_entry"><para role="func_signature">
+       <function>icu_multilib.collator_versions</function> ( <parameter>locale</parameter> <type>text</type> )
+       <returnvalue>setof record</returnvalue>
+       ( <parameter>icu_version</parameter> <type>text</type>,
+       <parameter>uca_version</parameter> <type>text</type>,
+       <parameter>collator_version</parameter> <type>text</type> )
+      </para>
+      <para>
+       Returns details about available collators for the given
+       <parameter>locale</parameter> from all available ICU libraries.
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect2>
+
+ <sect2 id="icu-multilib-examples">
+  <title>Examples</title>
+
+<screen>
+SELECT icu_version, unicode_version, cldr_version FROM icu_multilib.library_versions() LIMIT 3;
+ icu_version | unicode_version | cldr_version
+-------------+-----------------+--------------
+ 70.1        | 14.0            | 40.0
+ 69.1        | 13.0            | 39.0
+ 68.2        | 13.0            | 38.1
+(3 rows)
+</screen>
+
+<screen>
+SELECT * FROM icu_multilib.collator_versions('en_US') WHERE icu_version BETWEEN '60.0' and '65.0';
+ icu_version | uca_version | collator_version
+-------------+-------------+------------------
+ 64.2        | 12.1        | 153.97
+ 63.2        | 11.0        | 153.88
+ 62.2        | 11.0        | 153.88
+ 61.2        | 10.0        | 153.80
+ 60.3        | 10.0        | 153.80
+(5 rows)
+</screen>
+
+<screen>
+SELECT * FROM icu_multilib.collator_version_search('en_US', '153.97');
+ icu_version | uca_version | collator_version
+-------------+-------------+------------------
+ 65.1        | 12.1        | 153.97
+(1 row)
+</screen>
+
+ </sect2>
+
+ <sect2 id="icu-multilib-author">
+  <title>Authors</title>
+
+  <para>
+   Jeff Davis <email>jdavis@postgresql.org</email>, Thomas Munro <email>tmunro@postgresql.org</email>
+  </para>
+ </sect2>
+
+</sect1>
-- 
2.34.1