From 0a6e71bbc470fcd0b29e06865bf85d2c8ac148ca Mon Sep 17 00:00:00 2001
From: Vlad Medveditskov <vladmedveditskov@yandex.ru>
Date: Mon, 18 May 2026 12:50:13 +0300
Subject: [PATCH] Add contrib/anyarray: intarray-style operations and indexes
 for any array type

This new contrib module generalizes contrib/intarray to arrays of any
element type with a default btree opclass.  It provides:

* Set-style helpers for anyarray / anyelement: anyarray_sort
  (with optional asc/desc direction), anyarray_uniq, anyarray_idx,
  anyarray_subarray (2- and 3-arg), anyarray_icount,
  anyarray_intersect, anyarray_union, anyarray_union_elem,
  anyarray_difference, anyarray_difference_elem, plus the
  corresponding operators #, &, |, - on anyarray and anyelement.

* A textual boolean query type "anyquery" and the operators
  anyarray @@ anyquery (with commutator anyquery ~~ anyarray).
  Tokens are stored as text and parsed lazily through the
  element type's input function at match time, so the same
  query value can be used against arrays of different element
  types.

* A signature-based polymorphic GiST opclass "anyarray_gist_ops"
  on anyarray.  Each indexed array is hashed into a fixed-size
  bit vector; internal nodes store the bitwise union of their
  children, with an ALLISTRUE short-circuit when every bit is
  set.  Signature length is configurable via the siglen
  reloption (default 252 bytes).

* GIN opclasses for the three element types most commonly
  requested by users: int8_anyquery_gin_ops,
  uuid_anyquery_gin_ops and text_anyquery_gin_ops.  They are
  per-concrete-type because the GIN AM does not propagate
  fn_expr into extractQuery, so the element type needed to
  parse anyquery tokens cannot be discovered dynamically.
  Strategies 1-4 mirror the built-in array_ops; strategy 5
  is @@.

* doc/src/sgml/anyarray.sgml with descriptions of every
  function, operator and opclass, plus a usage example.

* Regression test (contrib/anyarray/sql/anyarray.sql,
  expected/anyarray.out) covering int8, uuid and text inputs,
  the error paths, and a cross-AM consistency block that
  asserts seqscan, GiST and GIN return the same row set for
  seven representative predicates.
---
 contrib/Makefile                       |   1 +
 contrib/anyarray/.gitignore            |   4 +
 contrib/anyarray/Makefile              |  27 +
 contrib/anyarray/anyarray--1.0.sql     | 336 +++++++++++
 contrib/anyarray/anyarray.c            | 116 ++++
 contrib/anyarray/anyarray.control      |   6 +
 contrib/anyarray/anyarray.h            | 184 ++++++
 contrib/anyarray/anyarray_bool.c       | 664 ++++++++++++++++++++++
 contrib/anyarray/anyarray_gin.c        | 374 +++++++++++++
 contrib/anyarray/anyarray_gist.c       | 742 +++++++++++++++++++++++++
 contrib/anyarray/anyarray_op.c         | 613 ++++++++++++++++++++
 contrib/anyarray/expected/anyarray.out | 727 ++++++++++++++++++++++++
 contrib/anyarray/meson.build           |  38 ++
 contrib/anyarray/sql/anyarray.sql      | 330 +++++++++++
 contrib/meson.build                    |   1 +
 doc/src/sgml/anyarray.sgml             | 482 ++++++++++++++++
 doc/src/sgml/contrib.sgml              |   1 +
 doc/src/sgml/filelist.sgml             |   1 +
 src/tools/pgindent/typedefs.list       |   8 +
 19 files changed, 4655 insertions(+)
 create mode 100644 contrib/anyarray/.gitignore
 create mode 100644 contrib/anyarray/Makefile
 create mode 100644 contrib/anyarray/anyarray--1.0.sql
 create mode 100644 contrib/anyarray/anyarray.c
 create mode 100644 contrib/anyarray/anyarray.control
 create mode 100644 contrib/anyarray/anyarray.h
 create mode 100644 contrib/anyarray/anyarray_bool.c
 create mode 100644 contrib/anyarray/anyarray_gin.c
 create mode 100644 contrib/anyarray/anyarray_gist.c
 create mode 100644 contrib/anyarray/anyarray_op.c
 create mode 100644 contrib/anyarray/expected/anyarray.out
 create mode 100644 contrib/anyarray/meson.build
 create mode 100644 contrib/anyarray/sql/anyarray.sql
 create mode 100644 doc/src/sgml/anyarray.sgml

diff --git a/contrib/Makefile b/contrib/Makefile
index 7d91fe77db3..b9586cb3a6b 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -6,6 +6,7 @@ include $(top_builddir)/src/Makefile.global
 
 SUBDIRS = \
 		amcheck		\
+		anyarray	\
 		auth_delay	\
 		auto_explain	\
 		basic_archive	\
diff --git a/contrib/anyarray/.gitignore b/contrib/anyarray/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/contrib/anyarray/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/anyarray/Makefile b/contrib/anyarray/Makefile
new file mode 100644
index 00000000000..d6eb9ae10ad
--- /dev/null
+++ b/contrib/anyarray/Makefile
@@ -0,0 +1,27 @@
+# contrib/anyarray/Makefile
+
+MODULE_big = anyarray
+OBJS = \
+	$(WIN32RES) \
+	anyarray.o \
+	anyarray_bool.o \
+	anyarray_gin.o \
+	anyarray_gist.o \
+	anyarray_op.o
+
+EXTENSION = anyarray
+DATA = anyarray--1.0.sql
+PGFILEDESC = "anyarray - operations and indexes for arrays of any type"
+
+REGRESS = anyarray
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/anyarray
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/anyarray/anyarray--1.0.sql b/contrib/anyarray/anyarray--1.0.sql
new file mode 100644
index 00000000000..84bcff34b5b
--- /dev/null
+++ b/contrib/anyarray/anyarray--1.0.sql
@@ -0,0 +1,336 @@
+/* contrib/anyarray/anyarray--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION anyarray" to load this file. \quit
+
+--
+-- Set-style operations for arrays of any btree-orderable element type.
+--
+
+CREATE FUNCTION anyarray_sort(anyarray)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_sort'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_sort(anyarray, text)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_sort_dir'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_uniq(anyarray)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_uniq'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_idx(anyarray, anyelement)
+RETURNS int4
+AS 'MODULE_PATHNAME', 'anyarray_idx'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_subarray(anyarray, int4, int4)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_subarray'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_subarray(anyarray, int4)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_subarray_to_end'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_icount(anyarray)
+RETURNS int4
+AS 'MODULE_PATHNAME', 'anyarray_icount'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_intersect(anyarray, anyarray)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_intersect'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_union(anyarray, anyarray)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_union'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_union_elem(anyarray, anyelement)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_union_elem'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_difference(anyarray, anyarray)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_difference'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_difference_elem(anyarray, anyelement)
+RETURNS anyarray
+AS 'MODULE_PATHNAME', 'anyarray_difference_elem'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+--
+-- Operators
+--
+
+-- Unary prefix # : cardinality (number of elements)
+CREATE OPERATOR # (
+	RIGHTARG = anyarray,
+	PROCEDURE = anyarray_icount
+);
+
+-- array # element : 1-based index of element, or 0 if not found
+CREATE OPERATOR # (
+	LEFTARG = anyarray,
+	RIGHTARG = anyelement,
+	PROCEDURE = anyarray_idx
+);
+
+-- Set intersection
+CREATE OPERATOR & (
+	LEFTARG = anyarray,
+	RIGHTARG = anyarray,
+	COMMUTATOR = &,
+	PROCEDURE = anyarray_intersect
+);
+
+-- Set union (array | array)
+CREATE OPERATOR | (
+	LEFTARG = anyarray,
+	RIGHTARG = anyarray,
+	COMMUTATOR = |,
+	PROCEDURE = anyarray_union
+);
+
+-- Append element (array | element)
+CREATE OPERATOR | (
+	LEFTARG = anyarray,
+	RIGHTARG = anyelement,
+	PROCEDURE = anyarray_union_elem
+);
+
+-- Set difference (array - array)
+CREATE OPERATOR - (
+	LEFTARG = anyarray,
+	RIGHTARG = anyarray,
+	PROCEDURE = anyarray_difference
+);
+
+-- Remove all occurrences of element (array - element)
+CREATE OPERATOR - (
+	LEFTARG = anyarray,
+	RIGHTARG = anyelement,
+	PROCEDURE = anyarray_difference_elem
+);
+
+--
+-- Boolean query type
+--
+
+CREATE FUNCTION anyquery_in(cstring)
+RETURNS anyquery
+AS 'MODULE_PATHNAME', 'anyquery_in'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyquery_out(anyquery)
+RETURNS cstring
+AS 'MODULE_PATHNAME', 'anyquery_out'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE TYPE anyquery (
+	INTERNALLENGTH = -1,
+	INPUT = anyquery_in,
+	OUTPUT = anyquery_out,
+	STORAGE = extended
+);
+
+CREATE FUNCTION anyquery_querytree(anyquery)
+RETURNS text
+AS 'MODULE_PATHNAME', 'anyquery_querytree'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_boolop(anyarray, anyquery)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'anyarray_boolop'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyquery_boolop_rev(anyquery, anyarray)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'anyquery_boolop_rev'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE OPERATOR @@ (
+	LEFTARG = anyarray,
+	RIGHTARG = anyquery,
+	PROCEDURE = anyarray_boolop,
+	COMMUTATOR = '~~',
+	RESTRICT = contsel,
+	JOIN = contjoinsel
+);
+
+CREATE OPERATOR ~~ (
+	LEFTARG = anyquery,
+	RIGHTARG = anyarray,
+	PROCEDURE = anyquery_boolop_rev,
+	COMMUTATOR = '@@',
+	RESTRICT = contsel,
+	JOIN = contjoinsel
+);
+
+--
+-- GiST opclass (signature-based)
+--
+
+CREATE FUNCTION anyarray_gist_key_in(cstring)
+RETURNS anyarray_gist_key
+AS 'MODULE_PATHNAME', 'anyarray_gist_key_in'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gist_key_out(anyarray_gist_key)
+RETURNS cstring
+AS 'MODULE_PATHNAME', 'anyarray_gist_key_out'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE TYPE anyarray_gist_key (
+	INTERNALLENGTH = -1,
+	INPUT = anyarray_gist_key_in,
+	OUTPUT = anyarray_gist_key_out
+);
+
+CREATE FUNCTION anyarray_gist_consistent(internal, anyarray, smallint, oid, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'anyarray_gist_consistent'
+LANGUAGE C IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gist_compress(internal)
+RETURNS internal
+AS 'MODULE_PATHNAME', 'anyarray_gist_compress'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gist_decompress(internal)
+RETURNS internal
+AS 'MODULE_PATHNAME', 'anyarray_gist_decompress'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gist_union(internal, internal)
+RETURNS anyarray_gist_key
+AS 'MODULE_PATHNAME', 'anyarray_gist_union'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gist_same(anyarray_gist_key, anyarray_gist_key, internal)
+RETURNS internal
+AS 'MODULE_PATHNAME', 'anyarray_gist_same'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gist_penalty(internal, internal, internal)
+RETURNS internal
+AS 'MODULE_PATHNAME', 'anyarray_gist_penalty'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gist_picksplit(internal, internal)
+RETURNS internal
+AS 'MODULE_PATHNAME', 'anyarray_gist_picksplit'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gist_options(internal)
+RETURNS void
+AS 'MODULE_PATHNAME', 'anyarray_gist_options'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR CLASS anyarray_gist_ops
+FOR TYPE anyarray USING gist
+AS
+	OPERATOR	3	&&,
+	OPERATOR	7	@>,
+	OPERATOR	8	<@,
+	OPERATOR	18	=,
+	OPERATOR	20	@@ (anyarray, anyquery),
+	FUNCTION	1	anyarray_gist_consistent(internal, anyarray, smallint, oid, internal),
+	FUNCTION	2	anyarray_gist_union(internal, internal),
+	FUNCTION	3	anyarray_gist_compress(internal),
+	FUNCTION	4	anyarray_gist_decompress(internal),
+	FUNCTION	5	anyarray_gist_penalty(internal, internal, internal),
+	FUNCTION	6	anyarray_gist_picksplit(internal, internal),
+	FUNCTION	7	anyarray_gist_same(anyarray_gist_key, anyarray_gist_key, internal),
+	FUNCTION	10	anyarray_gist_options(internal),
+	STORAGE		anyarray_gist_key;
+
+--
+-- GIN opclasses (per concrete element type, supporting standard ops + @@)
+--
+
+CREATE FUNCTION anyarray_gin_extract_query_int8(
+	int8[], internal, smallint, internal, internal, internal, internal)
+RETURNS internal
+AS 'MODULE_PATHNAME', 'anyarray_gin_extract_query_int8'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gin_consistent_int8(
+	internal, smallint, int8[], integer, internal, internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'anyarray_gin_consistent_int8'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR CLASS int8_anyquery_gin_ops
+FOR TYPE int8[] USING gin
+AS
+	OPERATOR	1	&& (anyarray, anyarray),
+	OPERATOR	2	@> (anyarray, anyarray),
+	OPERATOR	3	<@ (anyarray, anyarray),
+	OPERATOR	4	= (anyarray, anyarray),
+	OPERATOR	5	@@ (anyarray, anyquery),
+	FUNCTION	1	btint8cmp(int8, int8),
+	FUNCTION	2	ginarrayextract(anyarray, internal, internal),
+	FUNCTION	3	anyarray_gin_extract_query_int8(int8[], internal, smallint, internal, internal, internal, internal),
+	FUNCTION	4	anyarray_gin_consistent_int8(internal, smallint, int8[], integer, internal, internal, internal, internal),
+	STORAGE		int8;
+
+CREATE FUNCTION anyarray_gin_extract_query_uuid(
+	uuid[], internal, smallint, internal, internal, internal, internal)
+RETURNS internal
+AS 'MODULE_PATHNAME', 'anyarray_gin_extract_query_uuid'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gin_consistent_uuid(
+	internal, smallint, uuid[], integer, internal, internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'anyarray_gin_consistent_uuid'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR CLASS uuid_anyquery_gin_ops
+FOR TYPE uuid[] USING gin
+AS
+	OPERATOR	1	&& (anyarray, anyarray),
+	OPERATOR	2	@> (anyarray, anyarray),
+	OPERATOR	3	<@ (anyarray, anyarray),
+	OPERATOR	4	= (anyarray, anyarray),
+	OPERATOR	5	@@ (anyarray, anyquery),
+	FUNCTION	1	uuid_cmp(uuid, uuid),
+	FUNCTION	2	ginarrayextract(anyarray, internal, internal),
+	FUNCTION	3	anyarray_gin_extract_query_uuid(uuid[], internal, smallint, internal, internal, internal, internal),
+	FUNCTION	4	anyarray_gin_consistent_uuid(internal, smallint, uuid[], integer, internal, internal, internal, internal),
+	STORAGE		uuid;
+
+CREATE FUNCTION anyarray_gin_extract_query_text(
+	text[], internal, smallint, internal, internal, internal, internal)
+RETURNS internal
+AS 'MODULE_PATHNAME', 'anyarray_gin_extract_query_text'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE FUNCTION anyarray_gin_consistent_text(
+	internal, smallint, text[], integer, internal, internal, internal, internal)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'anyarray_gin_consistent_text'
+LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
+
+CREATE OPERATOR CLASS text_anyquery_gin_ops
+FOR TYPE text[] USING gin
+AS
+	OPERATOR	1	&& (anyarray, anyarray),
+	OPERATOR	2	@> (anyarray, anyarray),
+	OPERATOR	3	<@ (anyarray, anyarray),
+	OPERATOR	4	= (anyarray, anyarray),
+	OPERATOR	5	@@ (anyarray, anyquery),
+	FUNCTION	1	bttextcmp(text, text),
+	FUNCTION	2	ginarrayextract(anyarray, internal, internal),
+	FUNCTION	3	anyarray_gin_extract_query_text(text[], internal, smallint, internal, internal, internal, internal),
+	FUNCTION	4	anyarray_gin_consistent_text(internal, smallint, text[], integer, internal, internal, internal, internal),
+	STORAGE		text;
diff --git a/contrib/anyarray/anyarray.c b/contrib/anyarray/anyarray.c
new file mode 100644
index 00000000000..0a2ab2153c8
--- /dev/null
+++ b/contrib/anyarray/anyarray.c
@@ -0,0 +1,116 @@
+/*-------------------------------------------------------------------------
+ *
+ * anyarray.c
+ *		Common code for the anyarray extension.
+ *
+ * This extension extends PostgreSQL with intarray-style operations and
+ * index support that work for arrays of any element type with a default
+ * btree opclass.  It is a generalization of the contrib/intarray module.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		contrib/anyarray/anyarray.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "anyarray.h"
+#include "catalog/pg_type.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+
+PG_MODULE_MAGIC_EXT(
+					.name = "anyarray",
+					.version = PG_VERSION
+);
+
+/*
+ * anyarray_get_meta
+ *		Fetch (and cache) type metadata for the given element type.
+ *
+ * The cache lives in fcinfo->flinfo->fn_extra so we pay the catalog lookup
+ * cost only once per call site.  If "need_hash" is true the element type
+ * must additionally provide a default hash function; otherwise only a btree
+ * comparison function is required.
+ */
+AnyArrayTypeInfo *
+anyarray_get_meta(FunctionCallInfo fcinfo, Oid element_type, bool need_hash)
+{
+	AnyArrayTypeInfo *meta = (AnyArrayTypeInfo *) fcinfo->flinfo->fn_extra;
+	TypeCacheEntry *typentry;
+	uint32		flags;
+
+	if (meta == NULL)
+	{
+		meta = (AnyArrayTypeInfo *)
+			MemoryContextAllocZero(fcinfo->flinfo->fn_mcxt,
+								   sizeof(AnyArrayTypeInfo));
+		fcinfo->flinfo->fn_extra = meta;
+		meta->element_type = InvalidOid;
+	}
+
+	if (meta->element_type == element_type && (!need_hash || meta->have_hash))
+		return meta;
+
+	flags = TYPECACHE_CMP_PROC_FINFO | TYPECACHE_EQ_OPR_FINFO;
+	if (need_hash)
+		flags |= TYPECACHE_HASH_PROC_FINFO;
+
+	typentry = lookup_type_cache(element_type, flags);
+
+	if (!OidIsValid(typentry->cmp_proc_finfo.fn_oid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("could not identify a comparison function for type %s",
+						format_type_be(element_type))));
+
+	if (!OidIsValid(typentry->eq_opr_finfo.fn_oid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("could not identify an equality operator for type %s",
+						format_type_be(element_type))));
+
+	if (need_hash && !OidIsValid(typentry->hash_proc_finfo.fn_oid))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_FUNCTION),
+				 errmsg("could not identify a hash function for type %s",
+						format_type_be(element_type))));
+
+	meta->element_type = element_type;
+	get_typlenbyvalalign(element_type, &meta->typlen, &meta->typbyval,
+						 &meta->typalign);
+	meta->typcollation = typentry->typcollation;
+	fmgr_info_cxt(typentry->cmp_proc_finfo.fn_oid, &meta->cmp_proc,
+				  fcinfo->flinfo->fn_mcxt);
+	fmgr_info_cxt(typentry->eq_opr_finfo.fn_oid, &meta->eq_proc,
+				  fcinfo->flinfo->fn_mcxt);
+	if (need_hash)
+	{
+		fmgr_info_cxt(typentry->hash_proc_finfo.fn_oid, &meta->hash_proc,
+					  fcinfo->flinfo->fn_mcxt);
+		meta->have_hash = true;
+	}
+	else
+		meta->have_hash = false;
+
+	return meta;
+}
+
+/*
+ * anyarray_cmp_datum
+ *		qsort_arg-compatible comparator delegating to the type's btree cmp.
+ */
+int
+anyarray_cmp_datum(const void *a, const void *b, void *arg)
+{
+	AnyArrayTypeInfo *meta = (AnyArrayTypeInfo *) arg;
+	Datum		d1 = *((const Datum *) a);
+	Datum		d2 = *((const Datum *) b);
+	Datum		result;
+
+	result = FunctionCall2Coll(&meta->cmp_proc, meta->typcollation, d1, d2);
+	return DatumGetInt32(result);
+}
diff --git a/contrib/anyarray/anyarray.control b/contrib/anyarray/anyarray.control
new file mode 100644
index 00000000000..2478f2d93d7
--- /dev/null
+++ b/contrib/anyarray/anyarray.control
@@ -0,0 +1,6 @@
+# anyarray extension
+comment = 'Operations and indexes for arrays of any type'
+default_version = '1.0'
+module_pathname = '$libdir/anyarray'
+relocatable = true
+trusted = false
diff --git a/contrib/anyarray/anyarray.h b/contrib/anyarray/anyarray.h
new file mode 100644
index 00000000000..f59ac9de749
--- /dev/null
+++ b/contrib/anyarray/anyarray.h
@@ -0,0 +1,184 @@
+/*-------------------------------------------------------------------------
+ *
+ * anyarray.h
+ *		Shared declarations for the anyarray extension.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		contrib/anyarray/anyarray.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ANYARRAY_H
+#define ANYARRAY_H
+
+#include "access/gist.h"
+#include "fmgr.h"
+#include "utils/array.h"
+#include "utils/typcache.h"
+
+/*
+ * Per-call cached element type metadata.
+ *
+ * Stored in fcinfo->flinfo->fn_extra so we look up the type once per
+ * planner-level call site.  All entries are populated by anyarray_get_meta().
+ */
+typedef struct AnyArrayTypeInfo
+{
+	Oid			element_type;	/* element OID, or InvalidOid if not
+								 * initialized */
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+	Oid			typcollation;	/* default collation for comparisons */
+	FmgrInfo	cmp_proc;		/* btree comparison function for element_type */
+	FmgrInfo	eq_proc;		/* equality function for element_type */
+	FmgrInfo	hash_proc;		/* hash function for element_type (optional) */
+	bool		have_hash;		/* whether hash_proc was filled in */
+} AnyArrayTypeInfo;
+
+/* Reject the kinds of input we do not handle. */
+#define ANYARRAY_CHECK_ARRAY(arr) \
+	do { \
+		if (ARR_NDIM(arr) > 1) \
+			ereport(ERROR, \
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \
+					 errmsg("multidimensional arrays are not supported"))); \
+		if (ARR_HASNULL(arr) && array_contains_nulls(arr)) \
+			ereport(ERROR, \
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), \
+					 errmsg("array must not contain nulls"))); \
+	} while (0)
+
+#define ANYARRAY_NELEMS(arr) \
+	(ARR_NDIM(arr) > 0 ? ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr)) : 0)
+
+/*
+ * Initialize / refresh AnyArrayTypeInfo for a given element type.
+ *
+ * Allocates the struct in fcinfo->flinfo->fn_mcxt on first use; on subsequent
+ * calls reuses the cache unless the element type changed.  Reports an error
+ * via ereport() if the element type lacks a btree comparison operator
+ * (need_hash == true additionally requires a hash function).
+ */
+extern AnyArrayTypeInfo *anyarray_get_meta(FunctionCallInfo fcinfo,
+										   Oid element_type,
+										   bool need_hash);
+
+/*
+ * Comparison helper used both as a qsort_arg callback and directly.
+ * "arg" must be a valid AnyArrayTypeInfo *.
+ */
+extern int	anyarray_cmp_datum(const void *a, const void *b, void *arg);
+
+/*****************************************************************************
+ *  Boolean query type
+ *
+ *  An anyquery value is a boolean expression in postfix notation whose leaves
+ *  are textual representations of values.  The element type is not known at
+ *  parse time; it is supplied at @@-time by the array operand, and the value
+ *  strings are parsed lazily through the element type's text input function.
+ *
+ *  On-disk layout:
+ *
+ *     +-----------------------------+
+ *     | varlena header              |
+ *     | int32  size      (n items)  |
+ *     | int32  str_off              |  -- byte offset to string heap
+ *     | int32  str_len              |
+ *     | AnyQueryItem items[size]    |
+ *     | <padding>                   |
+ *     | char   strings[str_len]     |  -- null-terminated value strings
+ *     +-----------------------------+
+ *****************************************************************************/
+
+/* item.type values */
+#define ANYQ_VAL	1
+#define ANYQ_OPR	2
+
+/* operator codes (also used in the input grammar) */
+#define ANYQ_AND	'&'
+#define ANYQ_OR		'|'
+#define ANYQ_NOT	'!'
+
+typedef struct AnyQueryItem
+{
+	int16		type;			/* ANYQ_VAL or ANYQ_OPR */
+	int16		left;			/* offset to left-operand item (OPR only) */
+	int32		payload;		/* VAL: offset into string heap; OPR: one of
+								 * ANYQ_AND/OR/NOT */
+} AnyQueryItem;
+
+typedef struct AnyQuery
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int32		size;			/* number of items */
+	int32		str_off;		/* byte offset to string heap */
+	int32		str_len;		/* length of string heap */
+	AnyQueryItem items[FLEXIBLE_ARRAY_MEMBER];
+	/* followed by null-terminated value strings */
+} AnyQuery;
+
+#define ANYQUERY_HDRSIZE			offsetof(AnyQuery, items)
+#define ANYQUERY_ITEMS(q)			((q)->items)
+#define ANYQUERY_STRING(q, item)	((const char *) (q) + (q)->str_off + (item)->payload)
+#define ANYQUERY_MAXITEMS \
+	((MaxAllocSize - ANYQUERY_HDRSIZE) / sizeof(AnyQueryItem))
+
+#define DatumGetAnyQueryP(X)		((AnyQuery *) PG_DETOAST_DATUM(X))
+#define PG_GETARG_ANYQUERY_P(n)		DatumGetAnyQueryP(PG_GETARG_DATUM(n))
+
+/*****************************************************************************
+ *  GiST signature key
+ *
+ *  Each indexed array is summarized as a fixed-size bit vector ("signature").
+ *  Each element contributes a single bit, chosen as
+ *      hash(element) mod (siglen * 8).
+ *  Internal node keys are bitwise unions of their children; the ALLISTRUE
+ *  flag short-circuits storage when every bit would be set.
+ *****************************************************************************/
+
+#define ANYARRAY_SIGLEN_DEFAULT		(63 * 4)	/* 252 bytes -> 2016 bits */
+#define ANYARRAY_SIGLEN_MAX			GISTMaxIndexKeySize
+#define ANYARRAY_ALLISTRUE			0x01
+
+typedef struct AnyArrayGistKey
+{
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int32		flag;
+	char		data[FLEXIBLE_ARRAY_MEMBER];	/* bit vector, siglen bytes */
+} AnyArrayGistKey;
+
+typedef struct AnyArrayGistOptions
+{
+	int32		vl_len_;
+	int			siglen;			/* signature length in bytes */
+} AnyArrayGistOptions;
+
+#define ANYARRAY_GET_SIGLEN()		(PG_HAS_OPCLASS_OPTIONS() ? \
+									 ((AnyArrayGistOptions *) PG_GET_OPCLASS_OPTIONS())->siglen : \
+									 ANYARRAY_SIGLEN_DEFAULT)
+
+#define ANYARRAY_GKEY_HDR			(VARHDRSZ + sizeof(int32))
+#define ANYARRAY_GKEY_SIZE(flag, siglen) \
+	(ANYARRAY_GKEY_HDR + (((flag) & ANYARRAY_ALLISTRUE) ? 0 : (siglen)))
+#define ANYARRAY_GKEY_ISALLTRUE(k)	(((AnyArrayGistKey *) (k))->flag & ANYARRAY_ALLISTRUE)
+#define ANYARRAY_GKEY_SIGN(k)		((unsigned char *) (((AnyArrayGistKey *) (k))->data))
+
+/* GIN strategy numbers; the first four match core's gin/array_ops */
+#define ANYARRAY_GIN_OVERLAP_STRATEGY	1
+#define ANYARRAY_GIN_CONTAINS_STRATEGY	2
+#define ANYARRAY_GIN_CONTAINED_STRATEGY	3
+#define ANYARRAY_GIN_EQUAL_STRATEGY		4
+#define ANYARRAY_GIN_BOOLEAN_STRATEGY	5
+
+#define ANYARRAY_HASHVAL(h, siglen)	((unsigned int) (h) % ((siglen) * BITS_PER_BYTE))
+#define ANYARRAY_SETBIT(sig, h, siglen) \
+	((sig)[(ANYARRAY_HASHVAL((h), (siglen)) / BITS_PER_BYTE)] |= \
+		(1 << (ANYARRAY_HASHVAL((h), (siglen)) % BITS_PER_BYTE)))
+#define ANYARRAY_GETBIT(sig, h, siglen) \
+	(((sig)[(ANYARRAY_HASHVAL((h), (siglen)) / BITS_PER_BYTE)] >> \
+		(ANYARRAY_HASHVAL((h), (siglen)) % BITS_PER_BYTE)) & 1)
+
+#endif							/* ANYARRAY_H */
diff --git a/contrib/anyarray/anyarray_bool.c b/contrib/anyarray/anyarray_bool.c
new file mode 100644
index 00000000000..325619837ef
--- /dev/null
+++ b/contrib/anyarray/anyarray_bool.c
@@ -0,0 +1,664 @@
+/*-------------------------------------------------------------------------
+ *
+ * anyarray_bool.c
+ *		Boolean query type for anyarray.
+ *
+ * Provides the anyquery type and the @@ operator.  A query value is a
+ * boolean expression whose leaves are text tokens; the element type is
+ * supplied at @@-time by the array operand, and the leaves are parsed
+ * through the element type's text input function.
+ *
+ * Grammar:
+ *
+ *	  expr        ::= or_expr
+ *	  or_expr     ::= and_expr ( '|' and_expr )*
+ *	  and_expr    ::= unary_expr ( '&' unary_expr )*
+ *	  unary_expr  ::= '!' unary_expr | atom
+ *	  atom        ::= VALUE | '(' expr ')'
+ *	  VALUE       ::= bare_token | quoted_string
+ *	  bare_token  ::= any run of characters that are not whitespace,
+ *	                  '&', '|', '!', '(', ')', or '"'
+ *	  quoted_string := '"' chars-with-backslash-escapes '"'
+ *
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		contrib/anyarray/anyarray_bool.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "anyarray.h"
+
+#include "lib/stringinfo.h"
+#include "miscadmin.h"
+#include "nodes/miscnodes.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+
+PG_FUNCTION_INFO_V1(anyquery_in);
+PG_FUNCTION_INFO_V1(anyquery_out);
+PG_FUNCTION_INFO_V1(anyarray_boolop);
+PG_FUNCTION_INFO_V1(anyquery_boolop_rev);
+PG_FUNCTION_INFO_V1(anyquery_querytree);
+
+
+/* ------------------------------------------------------------------------
+ *  Parser
+ * ------------------------------------------------------------------------
+ */
+
+/*
+ * Working state used while building the postfix item list.
+ *
+ * Items are emitted in postfix (reverse polish) order directly into the
+ * "items" array.  Value strings are concatenated into "strs"; each VAL item
+ * stores the byte offset of its string within strs->data.
+ */
+typedef struct ParserState
+{
+	const char *cur;
+	const char *end;
+	AnyQueryItem *items;
+	int			nitems;
+	int			capitems;
+	StringInfoData strs;
+	struct Node *escontext;
+} ParserState;
+
+#define PARSE_FAIL(state, errc, ...) \
+	do { \
+		errsave((state)->escontext, \
+				(errcode(errc), \
+				 __VA_ARGS__)); \
+		return false; \
+	} while (0)
+
+static bool parse_expr(ParserState *state);
+
+static void
+skip_ws(ParserState *state)
+{
+	while (state->cur < state->end &&
+		   (*state->cur == ' ' || *state->cur == '\t' ||
+			*state->cur == '\r' || *state->cur == '\n' ||
+			*state->cur == '\v' || *state->cur == '\f'))
+		state->cur++;
+}
+
+static bool
+is_value_char(char c)
+{
+	if (c == '\0' || c == ' ' || c == '\t' || c == '\r' || c == '\n' ||
+		c == '\v' || c == '\f')
+		return false;
+	if (c == '&' || c == '|' || c == '!' || c == '(' || c == ')' || c == '"')
+		return false;
+	return true;
+}
+
+static bool
+emit_item(ParserState *state, int16 type, int32 payload)
+{
+	if (state->nitems == state->capitems)
+	{
+		if ((size_t) state->capitems * 2 > ANYQUERY_MAXITEMS)
+			PARSE_FAIL(state, ERRCODE_PROGRAM_LIMIT_EXCEEDED,
+					   errmsg("anyquery expression is too complex"));
+		state->capitems *= 2;
+		state->items = (AnyQueryItem *) repalloc(state->items,
+												 sizeof(AnyQueryItem) *
+												 state->capitems);
+	}
+	state->items[state->nitems].type = type;
+	state->items[state->nitems].left = 0;
+	state->items[state->nitems].payload = payload;
+	state->nitems++;
+	return true;
+}
+
+/*
+ * Consume a value token, append its string (with terminating NUL) to
+ * state->strs, and emit a VAL item whose payload is the byte offset of the
+ * string within strs->data.
+ */
+static bool
+parse_value(ParserState *state)
+{
+	int32		off;
+
+	skip_ws(state);
+
+	if (state->cur >= state->end)
+		PARSE_FAIL(state, ERRCODE_SYNTAX_ERROR,
+				   errmsg("unexpected end of input in anyquery"));
+
+	off = state->strs.len;
+
+	if (*state->cur == '"')
+	{
+		state->cur++;			/* opening quote */
+		while (state->cur < state->end && *state->cur != '"')
+		{
+			if (*state->cur == '\\' && state->cur + 1 < state->end)
+				state->cur++;
+			appendStringInfoChar(&state->strs, *state->cur);
+			state->cur++;
+		}
+		if (state->cur >= state->end)
+			PARSE_FAIL(state, ERRCODE_SYNTAX_ERROR,
+					   errmsg("unterminated quoted value in anyquery"));
+		state->cur++;			/* closing quote */
+	}
+	else if (is_value_char(*state->cur))
+	{
+		while (state->cur < state->end && is_value_char(*state->cur))
+		{
+			appendStringInfoChar(&state->strs, *state->cur);
+			state->cur++;
+		}
+	}
+	else
+		PARSE_FAIL(state, ERRCODE_SYNTAX_ERROR,
+				   errmsg("expected value at character \"%c\"", *state->cur));
+
+	appendStringInfoChar(&state->strs, '\0');
+	return emit_item(state, ANYQ_VAL, off);
+}
+
+static bool
+parse_atom(ParserState *state)
+{
+	skip_ws(state);
+	if (state->cur < state->end && *state->cur == '(')
+	{
+		state->cur++;
+		if (!parse_expr(state))
+			return false;
+		skip_ws(state);
+		if (state->cur >= state->end || *state->cur != ')')
+			PARSE_FAIL(state, ERRCODE_SYNTAX_ERROR,
+					   errmsg("missing closing parenthesis in anyquery"));
+		state->cur++;
+		return true;
+	}
+	return parse_value(state);
+}
+
+static bool
+parse_not(ParserState *state)
+{
+	skip_ws(state);
+	if (state->cur < state->end && *state->cur == '!')
+	{
+		state->cur++;
+		if (!parse_not(state))
+			return false;
+		return emit_item(state, ANYQ_OPR, ANYQ_NOT);
+	}
+	return parse_atom(state);
+}
+
+static bool
+parse_and(ParserState *state)
+{
+	if (!parse_not(state))
+		return false;
+	for (;;)
+	{
+		skip_ws(state);
+		if (state->cur >= state->end || *state->cur != '&')
+			return true;
+		state->cur++;
+		if (!parse_not(state))
+			return false;
+		if (!emit_item(state, ANYQ_OPR, ANYQ_AND))
+			return false;
+	}
+}
+
+static bool
+parse_expr(ParserState *state)
+{
+	check_stack_depth();
+
+	if (!parse_and(state))
+		return false;
+	for (;;)
+	{
+		skip_ws(state);
+		if (state->cur >= state->end || *state->cur != '|')
+			return true;
+		state->cur++;
+		if (!parse_and(state))
+			return false;
+		if (!emit_item(state, ANYQ_OPR, ANYQ_OR))
+			return false;
+	}
+}
+
+/*
+ * Compute the "left" field for every OPR item by walking the postfix array
+ * top-down.  Returns false on overflow of the int16 left field.
+ */
+static bool
+compute_lefts(ParserState *state, int *pos)
+{
+	int			mypos;
+
+	check_stack_depth();
+
+	mypos = (*pos)--;
+	Assert(mypos >= 0);
+
+	if (state->items[mypos].type == ANYQ_VAL)
+	{
+		state->items[mypos].left = 0;
+		return true;
+	}
+	else if (state->items[mypos].payload == ANYQ_NOT)
+	{
+		state->items[mypos].left = -1;
+		return compute_lefts(state, pos);
+	}
+	else
+	{
+		int			delta;
+
+		/* binary operator: walk right operand */
+		if (!compute_lefts(state, pos))
+			return false;
+		delta = *pos - mypos;
+		if (delta < PG_INT16_MIN)
+		{
+			errsave(state->escontext,
+					(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+					 errmsg("anyquery expression is too complex")));
+			return false;
+		}
+		state->items[mypos].left = (int16) delta;
+		return compute_lefts(state, pos);
+	}
+}
+
+
+/* ------------------------------------------------------------------------
+ *  Input / output / debug
+ * ------------------------------------------------------------------------
+ */
+
+Datum
+anyquery_in(PG_FUNCTION_ARGS)
+{
+	char	   *buf = PG_GETARG_CSTRING(0);
+	ParserState state;
+	AnyQuery   *out;
+	Size		items_bytes;
+	Size		total;
+	int			pos;
+
+	state.cur = buf;
+	state.end = buf + strlen(buf);
+	state.capitems = 16;
+	state.items = (AnyQueryItem *) palloc(sizeof(AnyQueryItem) *
+										  state.capitems);
+	state.nitems = 0;
+	initStringInfo(&state.strs);
+	state.escontext = fcinfo->context;
+
+	if (!parse_expr(&state))
+		PG_RETURN_NULL();
+
+	skip_ws(&state);
+	if (state.cur < state.end)
+		ereturn(state.escontext, (Datum) 0,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("unexpected trailing input in anyquery")));
+
+	if (state.nitems == 0)
+		ereturn(state.escontext, (Datum) 0,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty anyquery")));
+
+	if ((size_t) state.nitems > ANYQUERY_MAXITEMS)
+		ereturn(state.escontext, (Datum) 0,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("anyquery has too many items (%d, maximum %zu)",
+						state.nitems, ANYQUERY_MAXITEMS)));
+
+	pos = state.nitems - 1;
+	if (!compute_lefts(&state, &pos))
+		PG_RETURN_NULL();
+	Assert(pos == -1);
+
+	items_bytes = MAXALIGN(state.nitems * sizeof(AnyQueryItem));
+	total = ANYQUERY_HDRSIZE + items_bytes + state.strs.len;
+
+	out = (AnyQuery *) palloc0(total);
+	SET_VARSIZE(out, total);
+	out->size = state.nitems;
+	out->str_off = ANYQUERY_HDRSIZE + items_bytes;
+	out->str_len = state.strs.len;
+	memcpy(out->items, state.items, state.nitems * sizeof(AnyQueryItem));
+	memcpy((char *) out + out->str_off, state.strs.data, state.strs.len);
+
+	pfree(state.items);
+	pfree(state.strs.data);
+
+	PG_RETURN_POINTER(out);
+}
+
+/*
+ * Quote a value string if it contains anything the parser would consider
+ * special.  Otherwise emit it verbatim.
+ */
+static void
+append_value_token(StringInfo out, const char *s)
+{
+	const char *p;
+	bool		need_quote = (*s == '\0');
+
+	for (p = s; *p; p++)
+	{
+		if (!is_value_char(*p))
+		{
+			need_quote = true;
+			break;
+		}
+	}
+
+	if (!need_quote)
+	{
+		appendStringInfoString(out, s);
+		return;
+	}
+
+	appendStringInfoChar(out, '"');
+	for (p = s; *p; p++)
+	{
+		if (*p == '"' || *p == '\\')
+			appendStringInfoChar(out, '\\');
+		appendStringInfoChar(out, *p);
+	}
+	appendStringInfoChar(out, '"');
+}
+
+/*
+ * Infix walker for the output function.  Returns the postfix index that
+ * "cur" decremented to so the caller can chain.
+ */
+static int
+infix_walk(StringInfo out, AnyQuery *q, int cur, bool top)
+{
+	AnyQueryItem *it;
+
+	check_stack_depth();
+	Assert(cur >= 0);
+	it = &q->items[cur];
+
+	if (it->type == ANYQ_VAL)
+	{
+		append_value_token(out, ANYQUERY_STRING(q, it));
+		return cur - 1;
+	}
+	else if (it->payload == ANYQ_NOT)
+	{
+		AnyQueryItem *child = &q->items[cur - 1];
+		bool		paren = (child->type == ANYQ_OPR);
+
+		appendStringInfoChar(out, '!');
+		if (paren)
+			appendStringInfoString(out, "( ");
+		cur = infix_walk(out, q, cur - 1, false);
+		if (paren)
+			appendStringInfoString(out, " )");
+		return cur;
+	}
+	else
+	{
+		int			op = it->payload;
+		StringInfoData right;
+		int			next;
+
+		if (op == ANYQ_OR && !top)
+			appendStringInfoString(out, "( ");
+
+		/* right operand first into a side buffer */
+		initStringInfo(&right);
+		next = infix_walk(&right, q, cur - 1, false);
+		/* left operand into the main buffer */
+		cur = infix_walk(out, q, next, false);
+
+		appendStringInfo(out, " %c %s", op, right.data);
+		pfree(right.data);
+
+		if (op == ANYQ_OR && !top)
+			appendStringInfoString(out, " )");
+		return cur;
+	}
+}
+
+Datum
+anyquery_out(PG_FUNCTION_ARGS)
+{
+	AnyQuery   *q = PG_GETARG_ANYQUERY_P(0);
+	StringInfoData out;
+
+	if (q->size <= 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty anyquery")));
+
+	initStringInfo(&out);
+	(void) infix_walk(&out, q, q->size - 1, true);
+
+	PG_RETURN_CSTRING(out.data);
+}
+
+/*
+ * Debugging helper: produce a postfix string with operator codes spelled out.
+ * Mostly useful while writing tests.
+ */
+Datum
+anyquery_querytree(PG_FUNCTION_ARGS)
+{
+	AnyQuery   *q = PG_GETARG_ANYQUERY_P(0);
+	StringInfoData out;
+	int			i;
+
+	initStringInfo(&out);
+	for (i = 0; i < q->size; i++)
+	{
+		AnyQueryItem *it = &q->items[i];
+
+		if (i > 0)
+			appendStringInfoChar(&out, ' ');
+		if (it->type == ANYQ_VAL)
+			append_value_token(&out, ANYQUERY_STRING(q, it));
+		else
+			appendStringInfoChar(&out, (char) it->payload);
+	}
+	PG_RETURN_TEXT_P(cstring_to_text_with_len(out.data, out.len));
+}
+
+
+/* ------------------------------------------------------------------------
+ *  Matching: anyarray @@ anyquery
+ * ------------------------------------------------------------------------
+ */
+
+typedef struct MatchContext
+{
+	AnyArrayTypeInfo *meta;
+	Datum	   *sorted;			/* sorted array elements (owned) */
+	int			nelems;
+	/* cached parsed values for each VAL item in the query */
+	Datum	   *parsed;
+	bool	   *parsed_valid;
+} MatchContext;
+
+/*
+ * Look up val in the sorted element array via binary search.
+ */
+static bool
+contains_value(MatchContext *ctx, Datum val)
+{
+	int			lo = 0;
+	int			hi = ctx->nelems;
+
+	while (lo < hi)
+	{
+		int			mid = lo + (hi - lo) / 2;
+		int			c = DatumGetInt32(FunctionCall2Coll(&ctx->meta->cmp_proc,
+														ctx->meta->typcollation,
+														ctx->sorted[mid],
+														val));
+
+		if (c == 0)
+			return true;
+		if (c < 0)
+			lo = mid + 1;
+		else
+			hi = mid;
+	}
+	return false;
+}
+
+/*
+ * Evaluate one node of the postfix tree.  Recursive on operators.
+ */
+static bool
+eval_item(AnyQuery *q, int idx, MatchContext *ctx, Oid input_func,
+		  int input_typioparam, int32 input_typmod)
+{
+	AnyQueryItem *it;
+
+	check_stack_depth();
+	Assert(idx >= 0);
+	it = &q->items[idx];
+
+	if (it->type == ANYQ_VAL)
+	{
+		Datum		v;
+
+		if (!ctx->parsed_valid[idx])
+		{
+			const char *s = ANYQUERY_STRING(q, it);
+
+			v = OidInputFunctionCall(input_func, (char *) s,
+									 input_typioparam, input_typmod);
+			ctx->parsed[idx] = v;
+			ctx->parsed_valid[idx] = true;
+		}
+		return contains_value(ctx, ctx->parsed[idx]);
+	}
+	else if (it->payload == ANYQ_NOT)
+	{
+		return !eval_item(q, idx - 1, ctx, input_func,
+						  input_typioparam, input_typmod);
+	}
+	else if (it->payload == ANYQ_AND)
+	{
+		if (!eval_item(q, idx + it->left, ctx, input_func,
+					   input_typioparam, input_typmod))
+			return false;
+		return eval_item(q, idx - 1, ctx, input_func,
+						 input_typioparam, input_typmod);
+	}
+	else						/* ANYQ_OR */
+	{
+		if (eval_item(q, idx + it->left, ctx, input_func,
+					  input_typioparam, input_typmod))
+			return true;
+		return eval_item(q, idx - 1, ctx, input_func,
+						 input_typioparam, input_typmod);
+	}
+}
+
+static Datum
+do_boolop(FunctionCallInfo fcinfo, ArrayType *arr, AnyQuery *q)
+{
+	AnyArrayTypeInfo *meta;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+	Oid			input_func;
+	Oid			input_typioparam;
+	bool		result;
+	MatchContext ctx;
+
+	ANYARRAY_CHECK_ARRAY(arr);
+
+	if (q->size <= 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty anyquery")));
+
+	meta = anyarray_get_meta(fcinfo, ARR_ELEMTYPE(arr), false);
+
+	if (ARR_NDIM(arr) == 0)
+	{
+		nelems = 0;
+		values = NULL;
+	}
+	else
+	{
+		deconstruct_array(arr, meta->element_type, meta->typlen,
+						  meta->typbyval, meta->typalign,
+						  &values, &nulls, &nelems);
+		pfree(nulls);
+		if (nelems > 1)
+			qsort_arg(values, nelems, sizeof(Datum),
+					  anyarray_cmp_datum, meta);
+	}
+
+	getTypeInputInfo(meta->element_type, &input_func, &input_typioparam);
+
+	ctx.meta = meta;
+	ctx.sorted = values;
+	ctx.nelems = nelems;
+	ctx.parsed = (Datum *) palloc0(sizeof(Datum) * q->size);
+	ctx.parsed_valid = (bool *) palloc0(sizeof(bool) * q->size);
+
+	result = eval_item(q, q->size - 1, &ctx, input_func,
+					   input_typioparam, -1);
+
+	pfree(ctx.parsed);
+	pfree(ctx.parsed_valid);
+	if (values)
+		pfree(values);
+
+	PG_RETURN_BOOL(result);
+}
+
+Datum
+anyarray_boolop(PG_FUNCTION_ARGS)
+{
+	ArrayType  *arr = PG_GETARG_ARRAYTYPE_P(0);
+	AnyQuery   *q = PG_GETARG_ANYQUERY_P(1);
+	Datum		result = do_boolop(fcinfo, arr, q);
+
+	PG_FREE_IF_COPY(arr, 0);
+	PG_FREE_IF_COPY(q, 1);
+	return result;
+}
+
+/*
+ * Commutator: anyquery ~~ anyarray simply swaps the arguments.
+ */
+Datum
+anyquery_boolop_rev(PG_FUNCTION_ARGS)
+{
+	AnyQuery   *q = PG_GETARG_ANYQUERY_P(0);
+	ArrayType  *arr = PG_GETARG_ARRAYTYPE_P(1);
+	Datum		result = do_boolop(fcinfo, arr, q);
+
+	PG_FREE_IF_COPY(q, 0);
+	PG_FREE_IF_COPY(arr, 1);
+	return result;
+}
diff --git a/contrib/anyarray/anyarray_gin.c b/contrib/anyarray/anyarray_gin.c
new file mode 100644
index 00000000000..d9d201c12d5
--- /dev/null
+++ b/contrib/anyarray/anyarray_gin.c
@@ -0,0 +1,374 @@
+/*-------------------------------------------------------------------------
+ *
+ * anyarray_gin.c
+ *		GIN opclasses that add the @@ (anyarray, anyquery) boolean search
+ *		operator to the existing array_ops behaviour.
+ *
+ * GIN's extractQuery support function doesn't see fn_expr, so the element
+ * type cannot be derived dynamically when the query is an anyquery (which
+ * carries only text tokens).  We work around this by exposing one opclass
+ * per concrete element type; each is a thin C wrapper that dispatches on
+ * the strategy number, delegating standard operators to inline replicas of
+ * core's array_ops behaviour and parsing anyquery tokens via the type's
+ * input function for strategy 5.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		contrib/anyarray/anyarray_gin.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "anyarray.h"
+
+#include "access/gin.h"
+#include "access/stratnum.h"
+#include "catalog/pg_type.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+
+PG_FUNCTION_INFO_V1(anyarray_gin_extract_query_int8);
+PG_FUNCTION_INFO_V1(anyarray_gin_extract_query_uuid);
+PG_FUNCTION_INFO_V1(anyarray_gin_extract_query_text);
+PG_FUNCTION_INFO_V1(anyarray_gin_consistent_int8);
+PG_FUNCTION_INFO_V1(anyarray_gin_consistent_uuid);
+PG_FUNCTION_INFO_V1(anyarray_gin_consistent_text);
+
+
+/* ------------------------------------------------------------------------
+ *  Common: extractQuery dispatcher
+ * ------------------------------------------------------------------------
+ */
+
+/*
+ * Extract array elements as GIN keys (used for strategies 1-4).
+ *
+ * Mirrors core's ginqueryarrayextract logic, but inlined so we don't depend
+ * on it being callable.  Returns a freshly-palloc'd Datum vector.
+ */
+static Datum *
+extract_array_keys(ArrayType *arr, AnyArrayTypeInfo *meta,
+				   int32 *nentries, bool **nulls_out)
+{
+	Datum	   *values;
+	bool	   *nulls;
+	int			n;
+	int			i;
+	int			j = 0;
+
+	if (ARR_NDIM(arr) == 0)
+	{
+		*nentries = 0;
+		*nulls_out = NULL;
+		return NULL;
+	}
+
+	deconstruct_array(arr, meta->element_type, meta->typlen,
+					  meta->typbyval, meta->typalign,
+					  &values, &nulls, &n);
+
+	/* compact out NULL entries (we treat NULLs as never-present) */
+	for (i = 0; i < n; i++)
+	{
+		if (!nulls[i])
+		{
+			if (j != i)
+			{
+				values[j] = values[i];
+				nulls[j] = false;
+			}
+			j++;
+		}
+	}
+
+	*nentries = j;
+	*nulls_out = nulls;
+	return values;
+}
+
+/*
+ * Walk the anyquery: every VAL is a key that must be looked up in the GIN
+ * index.  Operators (& | !) do not contribute keys.  Returns true if at
+ * least one VAL must be present for the query to possibly match (i.e. the
+ * query is not e.g. "!foo").
+ */
+static bool
+anyquery_has_required(AnyQuery *q, int idx)
+{
+	AnyQueryItem *it;
+
+	if (idx < 0)
+		return false;
+	it = &q->items[idx];
+
+	if (it->type == ANYQ_VAL)
+		return true;
+	if (it->payload == ANYQ_NOT)
+		return false;			/* assume non-required under NOT */
+	if (it->payload == ANYQ_AND)
+		return anyquery_has_required(q, idx + it->left) ||
+			anyquery_has_required(q, idx - 1);
+	/* OR: both sides must contain required values */
+	return anyquery_has_required(q, idx + it->left) &&
+		anyquery_has_required(q, idx - 1);
+}
+
+static Datum
+do_gin_extract_query(FunctionCallInfo fcinfo, Oid elem_type)
+{
+	Datum		queryDatum = PG_GETARG_DATUM(0);
+	int32	   *nentries = (int32 *) PG_GETARG_POINTER(1);
+	StrategyNumber strat = PG_GETARG_UINT16(2);
+
+	/* PG_GETARG_POINTER(3): partial_matches -- not used */
+	/* PG_GETARG_POINTER(4): extra_data -- not used */
+	bool	  **nullFlags = (bool **) PG_GETARG_POINTER(5);
+	int32	   *searchMode = (int32 *) PG_GETARG_POINTER(6);
+	Datum	   *keys = NULL;
+
+	*nentries = 0;
+	*nullFlags = NULL;
+	*searchMode = GIN_SEARCH_MODE_DEFAULT;
+
+	if (strat == ANYARRAY_GIN_BOOLEAN_STRATEGY)
+	{
+		AnyQuery   *q = DatumGetAnyQueryP(queryDatum);
+		Oid			input_func;
+		Oid			input_typioparam;
+		int			i;
+		int			k = 0;
+
+		if (q->size <= 0)
+			PG_RETURN_POINTER(NULL);
+
+		/*
+		 * If the query has no required VAL (e.g. just "!foo"), we must scan
+		 * the whole index because rows containing NONE of the queried values
+		 * are valid matches.
+		 */
+		if (!anyquery_has_required(q, q->size - 1))
+			*searchMode = GIN_SEARCH_MODE_ALL;
+
+		getTypeInputInfo(elem_type, &input_func, &input_typioparam);
+		keys = (Datum *) palloc(sizeof(Datum) * q->size);
+
+		for (i = 0; i < q->size; i++)
+		{
+			AnyQueryItem *it = &q->items[i];
+
+			if (it->type != ANYQ_VAL)
+				continue;
+			keys[k++] = OidInputFunctionCall(input_func,
+											 (char *) ANYQUERY_STRING(q, it),
+											 input_typioparam, -1);
+		}
+		*nentries = k;
+		PG_RETURN_POINTER(keys);
+	}
+	else
+	{
+		ArrayType  *arr = DatumGetArrayTypeP(queryDatum);
+		AnyArrayTypeInfo *meta = anyarray_get_meta(fcinfo, elem_type, false);
+		bool	   *nulls;
+
+		ANYARRAY_CHECK_ARRAY(arr);
+		keys = extract_array_keys(arr, meta, nentries, &nulls);
+
+		switch (strat)
+		{
+			case ANYARRAY_GIN_OVERLAP_STRATEGY:
+				*searchMode = GIN_SEARCH_MODE_DEFAULT;
+				break;
+			case ANYARRAY_GIN_CONTAINED_STRATEGY:
+				*searchMode = GIN_SEARCH_MODE_INCLUDE_EMPTY;
+				break;
+			case ANYARRAY_GIN_EQUAL_STRATEGY:
+				*searchMode = (*nentries > 0)
+					? GIN_SEARCH_MODE_DEFAULT
+					: GIN_SEARCH_MODE_INCLUDE_EMPTY;
+				break;
+			case ANYARRAY_GIN_CONTAINS_STRATEGY:
+				*searchMode = (*nentries > 0)
+					? GIN_SEARCH_MODE_DEFAULT
+					: GIN_SEARCH_MODE_ALL;
+				break;
+			default:
+				elog(ERROR, "anyarray_gin: unknown strategy number: %d", strat);
+		}
+
+		(void) nulls;			/* swallowed by extract_array_keys */
+		PG_RETURN_POINTER(keys);
+	}
+}
+
+
+/* ------------------------------------------------------------------------
+ *  Common: consistent dispatcher
+ * ------------------------------------------------------------------------
+ */
+
+/*
+ * Evaluate an anyquery against an array of "key present" flags.  The i-th
+ * VAL in postfix order corresponds to check[i] (we mapped them in
+ * extractQuery, so the j-th VAL we emitted is check[j]).  We rebuild that
+ * VAL-only mapping here as we walk the postfix tree.
+ */
+typedef struct AnyQueryCheck
+{
+	const bool *check;			/* GIN's present-key flags */
+	int			next;			/* next index in check[] */
+} AnyQueryCheck;
+
+static bool
+eval_with_check(AnyQuery *q, int idx, bool *vals)
+{
+	AnyQueryItem *it = &q->items[idx];
+
+	if (it->type == ANYQ_VAL)
+		return vals[idx];
+	if (it->payload == ANYQ_NOT)
+		return !eval_with_check(q, idx - 1, vals);
+	if (it->payload == ANYQ_AND)
+		return eval_with_check(q, idx + it->left, vals) &&
+			eval_with_check(q, idx - 1, vals);
+	/* OR */
+	return eval_with_check(q, idx + it->left, vals) ||
+		eval_with_check(q, idx - 1, vals);
+}
+
+static Datum
+do_gin_consistent(FunctionCallInfo fcinfo, Oid elem_type)
+{
+	bool	   *check = (bool *) PG_GETARG_POINTER(0);
+	StrategyNumber strat = PG_GETARG_UINT16(1);
+	Datum		queryDatum = PG_GETARG_DATUM(2);
+	int32		nkeys = PG_GETARG_INT32(3);
+
+	/* PG_GETARG_POINTER(4): extra_data -- not used */
+	bool	   *recheck = (bool *) PG_GETARG_POINTER(5);
+	bool		result = false;
+	int			i;
+
+	(void) elem_type;
+
+	if (strat == ANYARRAY_GIN_BOOLEAN_STRATEGY)
+	{
+		AnyQuery   *q = DatumGetAnyQueryP(queryDatum);
+		bool	   *vals;
+		int			k = 0;
+
+		*recheck = false;
+
+		if (q->size <= 0)
+			PG_RETURN_BOOL(false);
+
+		/* Map each VAL postfix slot to its position in check[]. */
+		vals = (bool *) palloc(sizeof(bool) * q->size);
+		for (i = 0; i < q->size; i++)
+		{
+			if (q->items[i].type == ANYQ_VAL)
+				vals[i] = check[k++];
+		}
+		result = eval_with_check(q, q->size - 1, vals);
+		pfree(vals);
+		PG_RETURN_BOOL(result);
+	}
+
+	switch (strat)
+	{
+		case ANYARRAY_GIN_OVERLAP_STRATEGY:
+			*recheck = false;
+
+			/*
+			 * GIN guarantees at least one true entry on entry; safe to say
+			 * yes
+			 */
+			for (i = 0; i < nkeys; i++)
+			{
+				if (check[i])
+				{
+					result = true;
+					break;
+				}
+			}
+			break;
+		case ANYARRAY_GIN_CONTAINED_STRATEGY:
+			*recheck = true;
+			result = true;		/* must always recheck */
+			break;
+		case ANYARRAY_GIN_EQUAL_STRATEGY:
+			*recheck = true;
+			result = true;
+			for (i = 0; i < nkeys; i++)
+			{
+				if (!check[i])
+				{
+					result = false;
+					break;
+				}
+			}
+			break;
+		case ANYARRAY_GIN_CONTAINS_STRATEGY:
+			*recheck = false;
+			result = true;
+			for (i = 0; i < nkeys; i++)
+			{
+				if (!check[i])
+				{
+					result = false;
+					break;
+				}
+			}
+			break;
+		default:
+			elog(ERROR, "anyarray_gin: unknown strategy number: %d", strat);
+	}
+
+	PG_RETURN_BOOL(result);
+}
+
+
+/* ------------------------------------------------------------------------
+ *  Per-type wrappers
+ * ------------------------------------------------------------------------
+ */
+
+Datum
+anyarray_gin_extract_query_int8(PG_FUNCTION_ARGS)
+{
+	return do_gin_extract_query(fcinfo, INT8OID);
+}
+
+Datum
+anyarray_gin_extract_query_uuid(PG_FUNCTION_ARGS)
+{
+	return do_gin_extract_query(fcinfo, UUIDOID);
+}
+
+Datum
+anyarray_gin_extract_query_text(PG_FUNCTION_ARGS)
+{
+	return do_gin_extract_query(fcinfo, TEXTOID);
+}
+
+Datum
+anyarray_gin_consistent_int8(PG_FUNCTION_ARGS)
+{
+	return do_gin_consistent(fcinfo, INT8OID);
+}
+
+Datum
+anyarray_gin_consistent_uuid(PG_FUNCTION_ARGS)
+{
+	return do_gin_consistent(fcinfo, UUIDOID);
+}
+
+Datum
+anyarray_gin_consistent_text(PG_FUNCTION_ARGS)
+{
+	return do_gin_consistent(fcinfo, TEXTOID);
+}
diff --git a/contrib/anyarray/anyarray_gist.c b/contrib/anyarray/anyarray_gist.c
new file mode 100644
index 00000000000..89570c63ded
--- /dev/null
+++ b/contrib/anyarray/anyarray_gist.c
@@ -0,0 +1,742 @@
+/*-------------------------------------------------------------------------
+ *
+ * anyarray_gist.c
+ *		Signature-based GiST opclass for anyarray.
+ *
+ * Each indexed array is summarised as a fixed-size bit vector; each element
+ * contributes a single bit chosen by its hash modulo the signature length.
+ * Internal node keys are bitwise unions of their children, with an
+ * ALLISTRUE short-circuit when every bit would be set.
+ *
+ * The opclass supports the array operators &&, @>, <@, = and the anyarray
+ * extension's @@ operator.  Because signatures are lossy, all matches are
+ * rechecked by GiST.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		contrib/anyarray/anyarray_gist.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "anyarray.h"
+
+#include "access/gist.h"
+#include "access/reloptions.h"
+#include "access/stratnum.h"
+#include "catalog/pg_index.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "port/pg_bitutils.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+
+/* Strategy numbers; share R-tree conventions where applicable. */
+#define ANYARRAY_OVERLAP_STRATEGY		3
+#define ANYARRAY_CONTAINS_STRATEGY		7
+#define ANYARRAY_CONTAINED_STRATEGY		8
+#define ANYARRAY_EQUAL_STRATEGY			18
+#define ANYARRAY_BOOLEAN_STRATEGY		20
+
+PG_FUNCTION_INFO_V1(anyarray_gist_key_in);
+PG_FUNCTION_INFO_V1(anyarray_gist_key_out);
+PG_FUNCTION_INFO_V1(anyarray_gist_consistent);
+PG_FUNCTION_INFO_V1(anyarray_gist_compress);
+PG_FUNCTION_INFO_V1(anyarray_gist_decompress);
+PG_FUNCTION_INFO_V1(anyarray_gist_union);
+PG_FUNCTION_INFO_V1(anyarray_gist_same);
+PG_FUNCTION_INFO_V1(anyarray_gist_penalty);
+PG_FUNCTION_INFO_V1(anyarray_gist_picksplit);
+PG_FUNCTION_INFO_V1(anyarray_gist_options);
+
+
+/* ------------------------------------------------------------------------
+ *  Storage type stubs
+ *
+ *  The signature key is only ever constructed internally by the index AM,
+ *  so its text input/output functions reject all calls (mirroring
+ *  intbig_gkey in contrib/intarray).
+ * ------------------------------------------------------------------------
+ */
+
+Datum
+anyarray_gist_key_in(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot accept a value of type %s",
+					"anyarray_gist_key")));
+	PG_RETURN_VOID();
+}
+
+Datum
+anyarray_gist_key_out(PG_FUNCTION_ARGS)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("cannot display a value of type %s",
+					"anyarray_gist_key")));
+	PG_RETURN_VOID();
+}
+
+
+/* ------------------------------------------------------------------------
+ *  Signature helpers
+ * ------------------------------------------------------------------------
+ */
+
+static AnyArrayGistKey *
+alloc_key(bool allistrue, int siglen, const unsigned char *src)
+{
+	int32		flag = allistrue ? ANYARRAY_ALLISTRUE : 0;
+	Size		size = ANYARRAY_GKEY_SIZE(flag, siglen);
+	AnyArrayGistKey *k = (AnyArrayGistKey *) palloc(size);
+
+	SET_VARSIZE(k, size);
+	k->flag = flag;
+	if (!allistrue)
+	{
+		if (src)
+			memcpy(k->data, src, siglen);
+		else
+			memset(k->data, 0, siglen);
+	}
+	return k;
+}
+
+/*
+ * Set the bit corresponding to "val" (a btree hash value) in "sig", a bit
+ * vector of length "siglen" bytes.
+ */
+static inline void
+set_bit(unsigned char *sig, uint32 hashval, int siglen)
+{
+	uint32		bit = hashval % ((uint32) siglen * BITS_PER_BYTE);
+
+	sig[bit / BITS_PER_BYTE] |= (1U << (bit % BITS_PER_BYTE));
+}
+
+static inline bool
+get_bit(const unsigned char *sig, uint32 hashval, int siglen)
+{
+	uint32		bit = hashval % ((uint32) siglen * BITS_PER_BYTE);
+
+	return (sig[bit / BITS_PER_BYTE] >> (bit % BITS_PER_BYTE)) & 1;
+}
+
+/*
+ * Hash a single Datum of the given element type, returning an unsigned 32-bit
+ * value suitable for indexing the bit vector.
+ */
+static uint32
+hash_elem(AnyArrayTypeInfo *meta, Datum value)
+{
+	Datum		h = FunctionCall1Coll(&meta->hash_proc, meta->typcollation,
+									  value);
+
+	return (uint32) DatumGetInt32(h);
+}
+
+/*
+ * Hash each element of "arr" into "sig".  The metadata must have a valid
+ * hash_proc (use anyarray_get_meta(..., true)).
+ */
+static void
+hash_array_into(unsigned char *sig, int siglen,
+				ArrayType *arr, AnyArrayTypeInfo *meta)
+{
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+	int			i;
+
+	if (ARR_NDIM(arr) == 0)
+		return;
+
+	deconstruct_array(arr, meta->element_type, meta->typlen,
+					  meta->typbyval, meta->typalign,
+					  &values, &nulls, &nelems);
+
+	for (i = 0; i < nelems; i++)
+	{
+		if (!nulls[i])
+			set_bit(sig, hash_elem(meta, values[i]), siglen);
+	}
+
+	pfree(values);
+	pfree(nulls);
+}
+
+/* Count "1" bits in a buffer of "n" bytes. */
+static int
+popcount_bytes(const unsigned char *p, int n)
+{
+	return pg_popcount((const char *) p, n);
+}
+
+
+/* ------------------------------------------------------------------------
+ *  compress / decompress
+ * ------------------------------------------------------------------------
+ */
+
+Datum
+anyarray_gist_compress(PG_FUNCTION_ARGS)
+{
+	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
+	GISTENTRY  *retval = entry;
+	int			siglen = ANYARRAY_GET_SIGLEN();
+
+	if (entry->leafkey)
+	{
+		ArrayType  *arr = DatumGetArrayTypeP(entry->key);
+		AnyArrayGistKey *key;
+		AnyArrayTypeInfo *meta;
+
+		ANYARRAY_CHECK_ARRAY(arr);
+
+		meta = anyarray_get_meta(fcinfo, ARR_ELEMTYPE(arr), true);
+
+		key = alloc_key(false, siglen, NULL);
+		hash_array_into(ANYARRAY_GKEY_SIGN(key), siglen, arr, meta);
+
+		retval = (GISTENTRY *) palloc(sizeof(GISTENTRY));
+		gistentryinit(*retval, PointerGetDatum(key), entry->rel, entry->page,
+					  entry->offset, false);
+	}
+	else if (!ANYARRAY_GKEY_ISALLTRUE(DatumGetPointer(entry->key)))
+	{
+		AnyArrayGistKey *k = (AnyArrayGistKey *) DatumGetPointer(entry->key);
+
+		/*
+		 * If every bit happens to be set, switch to ALLISTRUE storage so
+		 * subsequent operations don't have to compare a full bit vector.
+		 */
+		if (popcount_bytes(ANYARRAY_GKEY_SIGN(k), siglen) ==
+			siglen * BITS_PER_BYTE)
+		{
+			AnyArrayGistKey *r = alloc_key(true, siglen, NULL);
+
+			retval = (GISTENTRY *) palloc(sizeof(GISTENTRY));
+			gistentryinit(*retval, PointerGetDatum(r), entry->rel, entry->page,
+						  entry->offset, false);
+		}
+	}
+
+	PG_RETURN_POINTER(retval);
+}
+
+Datum
+anyarray_gist_decompress(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_POINTER(PG_GETARG_POINTER(0));
+}
+
+
+/* ------------------------------------------------------------------------
+ *  union / same / penalty / picksplit / options
+ * ------------------------------------------------------------------------
+ */
+
+Datum
+anyarray_gist_union(PG_FUNCTION_ARGS)
+{
+	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
+	int		   *sizep = (int *) PG_GETARG_POINTER(1);
+	int			siglen = ANYARRAY_GET_SIGLEN();
+	AnyArrayGistKey *out;
+	unsigned char *sig;
+	int			n = entryvec->n;
+	int			i;
+	bool		all = false;
+
+	out = alloc_key(false, siglen, NULL);
+	sig = ANYARRAY_GKEY_SIGN(out);
+
+	for (i = 0; i < n; i++)
+	{
+		AnyArrayGistKey *k = (AnyArrayGistKey *)
+			DatumGetPointer(entryvec->vector[i].key);
+
+		if (ANYARRAY_GKEY_ISALLTRUE(k))
+		{
+			all = true;
+			break;
+		}
+		for (int j = 0; j < siglen; j++)
+			sig[j] |= ANYARRAY_GKEY_SIGN(k)[j];
+	}
+
+	if (all || popcount_bytes(sig, siglen) == siglen * BITS_PER_BYTE)
+	{
+		pfree(out);
+		out = alloc_key(true, siglen, NULL);
+	}
+
+	*sizep = VARSIZE(out);
+	PG_RETURN_POINTER(out);
+}
+
+Datum
+anyarray_gist_same(PG_FUNCTION_ARGS)
+{
+	AnyArrayGistKey *a = (AnyArrayGistKey *) PG_GETARG_POINTER(0);
+	AnyArrayGistKey *b = (AnyArrayGistKey *) PG_GETARG_POINTER(1);
+	bool	   *result = (bool *) PG_GETARG_POINTER(2);
+	int			siglen = ANYARRAY_GET_SIGLEN();
+
+	if (ANYARRAY_GKEY_ISALLTRUE(a) || ANYARRAY_GKEY_ISALLTRUE(b))
+		*result = ANYARRAY_GKEY_ISALLTRUE(a) && ANYARRAY_GKEY_ISALLTRUE(b);
+	else
+		*result = (memcmp(ANYARRAY_GKEY_SIGN(a), ANYARRAY_GKEY_SIGN(b),
+						  siglen) == 0);
+
+	PG_RETURN_POINTER(result);
+}
+
+/*
+ * Hamming weight of (orig OR new) - Hamming weight of orig: the number of
+ * new bits a child would introduce.  ALLISTRUE entries have the maximum
+ * possible weight (siglen*8) so the answer is 0.
+ */
+Datum
+anyarray_gist_penalty(PG_FUNCTION_ARGS)
+{
+	GISTENTRY  *origentry = (GISTENTRY *) PG_GETARG_POINTER(0);
+	GISTENTRY  *newentry = (GISTENTRY *) PG_GETARG_POINTER(1);
+	float	   *penalty = (float *) PG_GETARG_POINTER(2);
+	int			siglen = ANYARRAY_GET_SIGLEN();
+	AnyArrayGistKey *orig = (AnyArrayGistKey *) DatumGetPointer(origentry->key);
+	AnyArrayGistKey *new_ = (AnyArrayGistKey *) DatumGetPointer(newentry->key);
+
+	if (ANYARRAY_GKEY_ISALLTRUE(orig))
+	{
+		*penalty = 0.0;
+	}
+	else if (ANYARRAY_GKEY_ISALLTRUE(new_))
+	{
+		int			orig_bits = popcount_bytes(ANYARRAY_GKEY_SIGN(orig), siglen);
+
+		*penalty = (float) (siglen * BITS_PER_BYTE - orig_bits);
+	}
+	else
+	{
+		unsigned char *o = ANYARRAY_GKEY_SIGN(orig);
+		unsigned char *n = ANYARRAY_GKEY_SIGN(new_);
+		int			added = 0;
+
+		for (int i = 0; i < siglen; i++)
+		{
+			unsigned char extra = n[i] & ~o[i];
+
+			added += pg_number_of_ones[extra];
+		}
+		*penalty = (float) added;
+	}
+
+	PG_RETURN_POINTER(penalty);
+}
+
+/*
+ * Standard signature picksplit: sort entries by Hamming weight and split
+ * down the middle.  This isn't optimal but is correct and balanced; the
+ * fancier Guttman-style splits used by intbig can be added later.
+ */
+typedef struct PickSplitEntry
+{
+	OffsetNumber offset;
+	int			weight;
+} PickSplitEntry;
+
+static int
+psplit_cmp(const void *a, const void *b)
+{
+	int			wa = ((const PickSplitEntry *) a)->weight;
+	int			wb = ((const PickSplitEntry *) b)->weight;
+
+	return (wa > wb) - (wa < wb);
+}
+
+static void
+or_into(unsigned char *dst, const unsigned char *src, int siglen)
+{
+	for (int i = 0; i < siglen; i++)
+		dst[i] |= src[i];
+}
+
+Datum
+anyarray_gist_picksplit(PG_FUNCTION_ARGS)
+{
+	GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0);
+	GIST_SPLITVEC *v = (GIST_SPLITVEC *) PG_GETARG_POINTER(1);
+	int			siglen = ANYARRAY_GET_SIGLEN();
+	int			n = entryvec->n - 1;	/* entries start at index 1 */
+	PickSplitEntry *items;
+	AnyArrayGistKey *left,
+			   *right;
+	int			i;
+	int			split;
+
+	items = (PickSplitEntry *) palloc(n * sizeof(PickSplitEntry));
+	for (i = 0; i < n; i++)
+	{
+		AnyArrayGistKey *k = (AnyArrayGistKey *)
+			DatumGetPointer(entryvec->vector[i + 1].key);
+
+		items[i].offset = (OffsetNumber) (i + 1);
+		items[i].weight = ANYARRAY_GKEY_ISALLTRUE(k)
+			? siglen * BITS_PER_BYTE
+			: popcount_bytes(ANYARRAY_GKEY_SIGN(k), siglen);
+	}
+	qsort(items, n, sizeof(PickSplitEntry), psplit_cmp);
+
+	v->spl_left = (OffsetNumber *) palloc(n * sizeof(OffsetNumber));
+	v->spl_right = (OffsetNumber *) palloc(n * sizeof(OffsetNumber));
+	v->spl_nleft = 0;
+	v->spl_nright = 0;
+	left = alloc_key(false, siglen, NULL);
+	right = alloc_key(false, siglen, NULL);
+
+	split = n / 2;
+	if (split == 0)
+		split = 1;
+	if (split == n)
+		split = n - 1;
+
+	for (i = 0; i < split; i++)
+	{
+		AnyArrayGistKey *k = (AnyArrayGistKey *)
+			DatumGetPointer(entryvec->vector[items[i].offset].key);
+
+		v->spl_left[v->spl_nleft++] = items[i].offset;
+		if (ANYARRAY_GKEY_ISALLTRUE(k))
+		{
+			pfree(left);
+			left = alloc_key(true, siglen, NULL);
+		}
+		else if (!ANYARRAY_GKEY_ISALLTRUE(left))
+			or_into(ANYARRAY_GKEY_SIGN(left), ANYARRAY_GKEY_SIGN(k), siglen);
+	}
+	for (; i < n; i++)
+	{
+		AnyArrayGistKey *k = (AnyArrayGistKey *)
+			DatumGetPointer(entryvec->vector[items[i].offset].key);
+
+		v->spl_right[v->spl_nright++] = items[i].offset;
+		if (ANYARRAY_GKEY_ISALLTRUE(k))
+		{
+			pfree(right);
+			right = alloc_key(true, siglen, NULL);
+		}
+		else if (!ANYARRAY_GKEY_ISALLTRUE(right))
+			or_into(ANYARRAY_GKEY_SIGN(right), ANYARRAY_GKEY_SIGN(k), siglen);
+	}
+
+	v->spl_ldatum = PointerGetDatum(left);
+	v->spl_rdatum = PointerGetDatum(right);
+
+	pfree(items);
+	PG_RETURN_POINTER(v);
+}
+
+/* opclass options: just the signature length, in bytes */
+static void
+fill_siglen_default(int *siglen)
+{
+	*siglen = ANYARRAY_SIGLEN_DEFAULT;
+}
+
+Datum
+anyarray_gist_options(PG_FUNCTION_ARGS)
+{
+	local_relopts *relopts = (local_relopts *) PG_GETARG_POINTER(0);
+
+	init_local_reloptions(relopts, sizeof(AnyArrayGistOptions));
+	add_local_int_reloption(relopts, "siglen",
+							"signature length in bytes",
+							ANYARRAY_SIGLEN_DEFAULT, 1, ANYARRAY_SIGLEN_MAX,
+							offsetof(AnyArrayGistOptions, siglen));
+
+	(void) fill_siglen_default;
+	PG_RETURN_VOID();
+}
+
+
+/* ------------------------------------------------------------------------
+ *  consistent
+ * ------------------------------------------------------------------------
+ */
+
+/*
+ * For overlap: any single matching bit means the key MIGHT contain something
+ * the query references.  Returns true if at least one element of "query"
+ * hashes to a set bit in "sig"; ALLISTRUE keys trivially pass.
+ */
+static bool
+sig_overlap_array(const unsigned char *sig, int siglen,
+				  ArrayType *query, AnyArrayTypeInfo *meta)
+{
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+	int			i;
+	bool		found = false;
+
+	if (ARR_NDIM(query) == 0)
+		return false;
+
+	deconstruct_array(query, meta->element_type, meta->typlen,
+					  meta->typbyval, meta->typalign,
+					  &values, &nulls, &nelems);
+
+	for (i = 0; i < nelems; i++)
+	{
+		if (nulls[i])
+			continue;
+		if (get_bit(sig, hash_elem(meta, values[i]), siglen))
+		{
+			found = true;
+			break;
+		}
+	}
+	pfree(values);
+	pfree(nulls);
+	return found;
+}
+
+/*
+ * For contains: every element of the query must hash to a set bit.
+ * If any required bit is missing the key cannot contain the query.
+ */
+static bool
+sig_contains_array(const unsigned char *sig, int siglen,
+				   ArrayType *query, AnyArrayTypeInfo *meta)
+{
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+	int			i;
+	bool		ok = true;
+
+	if (ARR_NDIM(query) == 0)
+		return true;
+
+	deconstruct_array(query, meta->element_type, meta->typlen,
+					  meta->typbyval, meta->typalign,
+					  &values, &nulls, &nelems);
+
+	for (i = 0; i < nelems; i++)
+	{
+		if (nulls[i])
+			continue;
+		if (!get_bit(sig, hash_elem(meta, values[i]), siglen))
+		{
+			ok = false;
+			break;
+		}
+	}
+	pfree(values);
+	pfree(nulls);
+	return ok;
+}
+
+/*
+ * Recursive postfix evaluator for sig_eval_query().
+ *
+ * "vals" holds the precomputed signature lookup for every VAL slot.  NOT
+ * is non-restrictive under a signature (the bit could still be set by some
+ * other inserted array), so we conservatively report true on negation; the
+ * GiST recheck step will then filter on the real values.
+ */
+static bool
+sig_eval_walk(AnyQuery *q, int idx, const bool *vals)
+{
+	AnyQueryItem *it;
+
+	check_stack_depth();
+	Assert(idx >= 0);
+	it = &q->items[idx];
+
+	if (it->type == ANYQ_VAL)
+		return vals[idx];
+	if (it->payload == ANYQ_NOT)
+		return true;
+	if (it->payload == ANYQ_AND)
+		return sig_eval_walk(q, idx + it->left, vals) &&
+			sig_eval_walk(q, idx - 1, vals);
+	/* ANYQ_OR */
+	return sig_eval_walk(q, idx + it->left, vals) ||
+		sig_eval_walk(q, idx - 1, vals);
+}
+
+/*
+ * Evaluate an anyquery against a signature.  Each VAL is parsed using the
+ * element type's text input, hashed, then looked up in the signature.
+ */
+static bool
+sig_eval_query(const unsigned char *sig, int siglen,
+			   AnyQuery *q, AnyArrayTypeInfo *meta,
+			   Oid input_func, Oid input_typioparam)
+{
+	bool	   *vals;
+	bool		result;
+	int			i;
+
+	if (q->size <= 0)
+		return false;
+
+	vals = (bool *) palloc0(sizeof(bool) * q->size);
+
+	for (i = 0; i < q->size; i++)
+	{
+		AnyQueryItem *it = &q->items[i];
+		Datum		v;
+
+		if (it->type != ANYQ_VAL)
+			continue;
+		v = OidInputFunctionCall(input_func,
+								 (char *) ANYQUERY_STRING(q, it),
+								 input_typioparam, -1);
+		vals[i] = get_bit(sig, hash_elem(meta, v), siglen);
+	}
+
+	result = sig_eval_walk(q, q->size - 1, vals);
+	pfree(vals);
+	return result;
+}
+
+Datum
+anyarray_gist_consistent(PG_FUNCTION_ARGS)
+{
+	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
+	StrategyNumber strat = (StrategyNumber) PG_GETARG_UINT16(2);
+	Oid			subtype = PG_GETARG_OID(3);
+	bool	   *recheck = (bool *) PG_GETARG_POINTER(4);
+	AnyArrayGistKey *key = (AnyArrayGistKey *) DatumGetPointer(entry->key);
+	int			siglen = ANYARRAY_GET_SIGLEN();
+	bool		result;
+
+	/* All signature-based answers require a recheck. */
+	*recheck = true;
+
+	if (ANYARRAY_GKEY_ISALLTRUE(key))
+	{
+		/* Cannot prune anything from an all-true signature. */
+		PG_RETURN_BOOL(true);
+	}
+
+	(void) subtype;
+
+	if (strat == ANYARRAY_BOOLEAN_STRATEGY)
+	{
+		AnyQuery   *q = PG_GETARG_ANYQUERY_P(1);
+		AnyArrayTypeInfo *meta;
+		Form_pg_index ind;
+		Oid			coltype;
+		Oid			elemtype;
+		Oid			input_func;
+		Oid			input_typioparam;
+
+		/*
+		 * The query is anyquery, so we recover the element type from the
+		 * indexed column's pg_index entry.  The index's own rd_att gives the
+		 * STORAGE type (anyarray_gist_key), not the original array type, so
+		 * we read the indrelid + indkey instead.
+		 */
+		ind = entry->rel->rd_index;
+		if (ind == NULL || ind->indnatts < 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INTERNAL_ERROR),
+					 errmsg("anyarray GiST index has no key column")));
+		coltype = get_atttype(ind->indrelid, ind->indkey.values[0]);
+		elemtype = get_element_type(coltype);
+		if (!OidIsValid(elemtype))
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("cannot determine element type for anyquery match")));
+
+		meta = anyarray_get_meta(fcinfo, elemtype, true);
+		getTypeInputInfo(elemtype, &input_func, &input_typioparam);
+
+		result = sig_eval_query(ANYARRAY_GKEY_SIGN(key), siglen, q, meta,
+								input_func, input_typioparam);
+		PG_FREE_IF_COPY(q, 1);
+		PG_RETURN_BOOL(result);
+	}
+	else
+	{
+		ArrayType  *query = PG_GETARG_ARRAYTYPE_P(1);
+		AnyArrayTypeInfo *meta;
+
+		ANYARRAY_CHECK_ARRAY(query);
+		meta = anyarray_get_meta(fcinfo, ARR_ELEMTYPE(query), true);
+
+		switch (strat)
+		{
+			case ANYARRAY_OVERLAP_STRATEGY:
+				result = sig_overlap_array(ANYARRAY_GKEY_SIGN(key), siglen,
+										   query, meta);
+				break;
+			case ANYARRAY_CONTAINS_STRATEGY:
+				result = sig_contains_array(ANYARRAY_GKEY_SIGN(key), siglen,
+											query, meta);
+				break;
+			case ANYARRAY_CONTAINED_STRATEGY:
+
+				/*
+				 * For "key <@ query", signatures cannot exclude a non-leaf
+				 * key because its bits may come from many distinct children
+				 * which need not individually be contained in query. Always
+				 * recheck.
+				 */
+				if (GIST_LEAF(entry))
+				{
+					/*
+					 * At the leaf, every set bit must also have been set by
+					 * the query (i.e., element hashes to that bit).
+					 */
+					unsigned char *qsig;
+					int			i;
+
+					qsig = (unsigned char *) palloc0(siglen);
+					hash_array_into(qsig, siglen, query, meta);
+					result = true;
+					for (i = 0; i < siglen; i++)
+					{
+						if (ANYARRAY_GKEY_SIGN(key)[i] & ~qsig[i])
+						{
+							result = false;
+							break;
+						}
+					}
+					pfree(qsig);
+				}
+				else
+					result = true;
+				break;
+			case ANYARRAY_EQUAL_STRATEGY:
+
+				/*
+				 * The leaf signature for an equal row contains every bit of
+				 * the query's hash; internal unions contain at least those
+				 * bits.  So "key contains query bits" is a sound
+				 * over-approximation that recheck will tighten.
+				 */
+				result = sig_contains_array(ANYARRAY_GKEY_SIGN(key), siglen,
+											query, meta);
+				break;
+			default:
+				elog(ERROR, "unrecognized strategy number: %d", strat);
+				result = false;
+				break;
+		}
+
+		PG_FREE_IF_COPY(query, 1);
+		PG_RETURN_BOOL(result);
+	}
+}
diff --git a/contrib/anyarray/anyarray_op.c b/contrib/anyarray/anyarray_op.c
new file mode 100644
index 00000000000..47a050bb0e3
--- /dev/null
+++ b/contrib/anyarray/anyarray_op.c
@@ -0,0 +1,613 @@
+/*-------------------------------------------------------------------------
+ *
+ * anyarray_op.c
+ *		Set-style operations on arrays of any type.
+ *
+ * All functions accept anyarray / anyelement inputs and dispatch to the
+ * element type's btree comparison function through anyarray_get_meta().
+ * The operations mirror the corresponding intarray operations but are
+ * type-polymorphic.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		contrib/anyarray/anyarray_op.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "anyarray.h"
+
+#include "catalog/pg_type.h"
+#include "lib/qunique.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+
+
+/*
+ * deconstruct_arr_meta
+ *		Pull element values out of "arr" and fill in "*meta" for its type.
+ *
+ * Returns Datum array in *out_values (palloc'd), count in *out_nelems.  The
+ * input array must already have passed ANYARRAY_CHECK_ARRAY; we still gate
+ * on it defensively because all entry points feed through this helper.
+ */
+static void
+deconstruct_arr_meta(FunctionCallInfo fcinfo, ArrayType *arr,
+					 AnyArrayTypeInfo **out_meta,
+					 Datum **out_values, int *out_nelems)
+{
+	AnyArrayTypeInfo *meta;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	ANYARRAY_CHECK_ARRAY(arr);
+
+	meta = anyarray_get_meta(fcinfo, ARR_ELEMTYPE(arr), false);
+
+	if (ARR_NDIM(arr) == 0)
+	{
+		*out_meta = meta;
+		*out_values = NULL;
+		*out_nelems = 0;
+		return;
+	}
+
+	deconstruct_array(arr, meta->element_type,
+					  meta->typlen, meta->typbyval, meta->typalign,
+					  &values, &nulls, &nelems);
+	pfree(nulls);				/* we've already rejected nulls above */
+
+	*out_meta = meta;
+	*out_values = values;
+	*out_nelems = nelems;
+}
+
+/*
+ * make_array_from_datums
+ *		Build a 1-D array from the given Datum vector.  Empty -> empty array.
+ */
+static ArrayType *
+make_array_from_datums(Datum *values, int nelems, AnyArrayTypeInfo *meta)
+{
+	if (nelems == 0)
+		return construct_empty_array(meta->element_type);
+
+	return construct_array(values, nelems, meta->element_type,
+						   meta->typlen, meta->typbyval, meta->typalign);
+}
+
+
+/* ------------------------------------------------------------------------
+ *  sort / uniq
+ * ------------------------------------------------------------------------
+ */
+
+PG_FUNCTION_INFO_V1(anyarray_sort);
+PG_FUNCTION_INFO_V1(anyarray_sort_dir);
+PG_FUNCTION_INFO_V1(anyarray_uniq);
+
+/*
+ * Sort an array ascending.
+ */
+Datum
+anyarray_sort(PG_FUNCTION_ARGS)
+{
+	ArrayType  *arr = PG_GETARG_ARRAYTYPE_P(0);
+	AnyArrayTypeInfo *meta;
+	Datum	   *values;
+	int			nelems;
+
+	deconstruct_arr_meta(fcinfo, arr, &meta, &values, &nelems);
+
+	if (nelems > 1)
+		qsort_arg(values, nelems, sizeof(Datum), anyarray_cmp_datum, meta);
+
+	PG_RETURN_ARRAYTYPE_P(make_array_from_datums(values, nelems, meta));
+}
+
+/*
+ * Sort an array.  The direction text is case-insensitive and must be one of
+ * 'asc' / 'ascending' or 'desc' / 'descending'.
+ */
+Datum
+anyarray_sort_dir(PG_FUNCTION_ARGS)
+{
+	ArrayType  *arr = PG_GETARG_ARRAYTYPE_P(0);
+	text	   *dir = PG_GETARG_TEXT_PP(1);
+	const char *dirstr = VARDATA_ANY(dir);
+	int			dirlen = VARSIZE_ANY_EXHDR(dir);
+	bool		ascending;
+	AnyArrayTypeInfo *meta;
+	Datum	   *values;
+	int			nelems;
+
+	if ((dirlen == 3 && pg_strncasecmp(dirstr, "asc", 3) == 0) ||
+		(dirlen == 9 && pg_strncasecmp(dirstr, "ascending", 9) == 0))
+		ascending = true;
+	else if ((dirlen == 4 && pg_strncasecmp(dirstr, "desc", 4) == 0) ||
+			 (dirlen == 10 && pg_strncasecmp(dirstr, "descending", 10) == 0))
+		ascending = false;
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("second parameter must be \"asc\" or \"desc\"")));
+
+	deconstruct_arr_meta(fcinfo, arr, &meta, &values, &nelems);
+
+	if (nelems > 1)
+	{
+		qsort_arg(values, nelems, sizeof(Datum), anyarray_cmp_datum, meta);
+		if (!ascending)
+		{
+			Datum	   *l = values;
+			Datum	   *r = values + nelems - 1;
+
+			while (l < r)
+			{
+				Datum		tmp = *l;
+
+				*l++ = *r;
+				*r-- = tmp;
+			}
+		}
+	}
+
+	PG_RETURN_ARRAYTYPE_P(make_array_from_datums(values, nelems, meta));
+}
+
+/*
+ * Remove duplicate elements; does not require pre-sorted input.
+ */
+Datum
+anyarray_uniq(PG_FUNCTION_ARGS)
+{
+	ArrayType  *arr = PG_GETARG_ARRAYTYPE_P(0);
+	AnyArrayTypeInfo *meta;
+	Datum	   *values;
+	int			nelems;
+	int			nunique;
+
+	deconstruct_arr_meta(fcinfo, arr, &meta, &values, &nelems);
+
+	if (nelems > 1)
+	{
+		qsort_arg(values, nelems, sizeof(Datum), anyarray_cmp_datum, meta);
+		nunique = qunique_arg(values, nelems, sizeof(Datum),
+							  anyarray_cmp_datum, meta);
+	}
+	else
+		nunique = nelems;
+
+	PG_RETURN_ARRAYTYPE_P(make_array_from_datums(values, nunique, meta));
+}
+
+
+/* ------------------------------------------------------------------------
+ *  idx / subarray / cardinality
+ * ------------------------------------------------------------------------
+ */
+
+PG_FUNCTION_INFO_V1(anyarray_idx);
+PG_FUNCTION_INFO_V1(anyarray_subarray);
+PG_FUNCTION_INFO_V1(anyarray_subarray_to_end);
+PG_FUNCTION_INFO_V1(anyarray_icount);
+
+/*
+ * Return the 1-based index of the first occurrence of "elem" in "arr",
+ * or 0 if not found.
+ */
+Datum
+anyarray_idx(PG_FUNCTION_ARGS)
+{
+	ArrayType  *arr = PG_GETARG_ARRAYTYPE_P(0);
+	Datum		elem = PG_GETARG_DATUM(1);
+	AnyArrayTypeInfo *meta;
+	Datum	   *values;
+	int			nelems;
+	int			i;
+
+	deconstruct_arr_meta(fcinfo, arr, &meta, &values, &nelems);
+
+	for (i = 0; i < nelems; i++)
+	{
+		Datum		eq = FunctionCall2Coll(&meta->eq_proc, meta->typcollation,
+										   values[i], elem);
+
+		if (DatumGetBool(eq))
+			PG_RETURN_INT32(i + 1);
+	}
+
+	PG_RETURN_INT32(0);
+}
+
+/*
+ * Common subarray extraction.  "start" is 1-based; if it is non-positive
+ * the function returns an empty array.  "len" gives the maximum number of
+ * elements to copy; if "have_len" is false the whole tail is returned.
+ */
+static ArrayType *
+do_subarray(FunctionCallInfo fcinfo, ArrayType *arr,
+			int32 start, int32 len, bool have_len)
+{
+	AnyArrayTypeInfo *meta;
+	Datum	   *values;
+	int			nelems;
+	int			off;
+	int			n;
+
+	deconstruct_arr_meta(fcinfo, arr, &meta, &values, &nelems);
+
+	if (start < 1 || start > nelems)
+		return construct_empty_array(meta->element_type);
+
+	off = start - 1;
+	if (have_len)
+	{
+		if (len <= 0)
+			return construct_empty_array(meta->element_type);
+		n = Min(len, nelems - off);
+	}
+	else
+		n = nelems - off;
+
+	return make_array_from_datums(values + off, n, meta);
+}
+
+/*
+ * subarray(arr, start, len)
+ */
+Datum
+anyarray_subarray(PG_FUNCTION_ARGS)
+{
+	ArrayType  *arr = PG_GETARG_ARRAYTYPE_P(0);
+	int32		start = PG_GETARG_INT32(1);
+	int32		len = PG_GETARG_INT32(2);
+
+	PG_RETURN_ARRAYTYPE_P(do_subarray(fcinfo, arr, start, len, true));
+}
+
+/*
+ * subarray(arr, start) -- to end
+ */
+Datum
+anyarray_subarray_to_end(PG_FUNCTION_ARGS)
+{
+	ArrayType  *arr = PG_GETARG_ARRAYTYPE_P(0);
+	int32		start = PG_GETARG_INT32(1);
+
+	PG_RETURN_ARRAYTYPE_P(do_subarray(fcinfo, arr, start, 0, false));
+}
+
+/*
+ * icount: total element count, intarray-style.  This duplicates
+ * built-in cardinality() but we expose it under the # operator the way
+ * intarray does.
+ */
+Datum
+anyarray_icount(PG_FUNCTION_ARGS)
+{
+	ArrayType  *arr = PG_GETARG_ARRAYTYPE_P(0);
+
+	ANYARRAY_CHECK_ARRAY(arr);
+	PG_RETURN_INT32(ANYARRAY_NELEMS(arr));
+}
+
+
+/* ------------------------------------------------------------------------
+ *  intersect / union / difference
+ * ------------------------------------------------------------------------
+ */
+
+PG_FUNCTION_INFO_V1(anyarray_intersect);
+PG_FUNCTION_INFO_V1(anyarray_union);
+PG_FUNCTION_INFO_V1(anyarray_union_elem);
+PG_FUNCTION_INFO_V1(anyarray_difference);
+PG_FUNCTION_INFO_V1(anyarray_difference_elem);
+
+/*
+ * deconstruct_two
+ *		Like deconstruct_arr_meta() but for two array inputs that must share
+ *		an element type.  The second array is allowed to be a different
+ *		ARRAY OID (it might come from another column) as long as the element
+ *		types match.
+ */
+static AnyArrayTypeInfo *
+deconstruct_two(FunctionCallInfo fcinfo,
+				ArrayType *a, ArrayType *b,
+				Datum **avals, int *anelems,
+				Datum **bvals, int *bnelems)
+{
+	AnyArrayTypeInfo *meta;
+	bool	   *nulls;
+
+	ANYARRAY_CHECK_ARRAY(a);
+	ANYARRAY_CHECK_ARRAY(b);
+
+	if (ARR_ELEMTYPE(a) != ARR_ELEMTYPE(b))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("cannot operate on arrays of different element types"),
+				 errdetail("Left operand has element type %s, right operand has %s.",
+						   format_type_be(ARR_ELEMTYPE(a)),
+						   format_type_be(ARR_ELEMTYPE(b)))));
+
+	meta = anyarray_get_meta(fcinfo, ARR_ELEMTYPE(a), false);
+
+	if (ARR_NDIM(a) == 0)
+	{
+		*avals = NULL;
+		*anelems = 0;
+	}
+	else
+	{
+		deconstruct_array(a, meta->element_type, meta->typlen,
+						  meta->typbyval, meta->typalign,
+						  avals, &nulls, anelems);
+		pfree(nulls);
+	}
+
+	if (ARR_NDIM(b) == 0)
+	{
+		*bvals = NULL;
+		*bnelems = 0;
+	}
+	else
+	{
+		deconstruct_array(b, meta->element_type, meta->typlen,
+						  meta->typbyval, meta->typalign,
+						  bvals, &nulls, bnelems);
+		pfree(nulls);
+	}
+
+	return meta;
+}
+
+/*
+ * Sort + de-dup in place.  Returns the new length.
+ */
+static int
+sort_uniq(Datum *values, int nelems, AnyArrayTypeInfo *meta)
+{
+	if (nelems <= 1)
+		return nelems;
+
+	qsort_arg(values, nelems, sizeof(Datum), anyarray_cmp_datum, meta);
+	return qunique_arg(values, nelems, sizeof(Datum),
+					   anyarray_cmp_datum, meta);
+}
+
+/*
+ * Intersection: returns the sorted, deduplicated values present in both
+ * inputs.
+ */
+Datum
+anyarray_intersect(PG_FUNCTION_ARGS)
+{
+	ArrayType  *a = PG_GETARG_ARRAYTYPE_P(0);
+	ArrayType  *b = PG_GETARG_ARRAYTYPE_P(1);
+	AnyArrayTypeInfo *meta;
+	Datum	   *av,
+			   *bv,
+			   *out;
+	int			an,
+				bn,
+				i = 0,
+				j = 0,
+				k = 0;
+
+	meta = deconstruct_two(fcinfo, a, b, &av, &an, &bv, &bn);
+
+	an = sort_uniq(av, an, meta);
+	bn = sort_uniq(bv, bn, meta);
+
+	out = (Datum *) palloc(sizeof(Datum) * Min(an, bn));
+	while (i < an && j < bn)
+	{
+		int			cmp = anyarray_cmp_datum(&av[i], &bv[j], meta);
+
+		if (cmp == 0)
+		{
+			out[k++] = av[i];
+			i++;
+			j++;
+		}
+		else if (cmp < 0)
+			i++;
+		else
+			j++;
+	}
+
+	PG_RETURN_ARRAYTYPE_P(make_array_from_datums(out, k, meta));
+}
+
+/*
+ * Union: returns the sorted, deduplicated values present in either input.
+ */
+Datum
+anyarray_union(PG_FUNCTION_ARGS)
+{
+	ArrayType  *a = PG_GETARG_ARRAYTYPE_P(0);
+	ArrayType  *b = PG_GETARG_ARRAYTYPE_P(1);
+	AnyArrayTypeInfo *meta;
+	Datum	   *av,
+			   *bv,
+			   *out;
+	int			an,
+				bn,
+				i = 0,
+				j = 0,
+				k = 0;
+
+	meta = deconstruct_two(fcinfo, a, b, &av, &an, &bv, &bn);
+
+	an = sort_uniq(av, an, meta);
+	bn = sort_uniq(bv, bn, meta);
+
+	out = (Datum *) palloc(sizeof(Datum) * (an + bn));
+	while (i < an && j < bn)
+	{
+		int			cmp = anyarray_cmp_datum(&av[i], &bv[j], meta);
+
+		if (cmp == 0)
+		{
+			out[k++] = av[i];
+			i++;
+			j++;
+		}
+		else if (cmp < 0)
+			out[k++] = av[i++];
+		else
+			out[k++] = bv[j++];
+	}
+	while (i < an)
+		out[k++] = av[i++];
+	while (j < bn)
+		out[k++] = bv[j++];
+
+	PG_RETURN_ARRAYTYPE_P(make_array_from_datums(out, k, meta));
+}
+
+/*
+ * array | element : add element if not already present, returning the
+ * sorted, deduplicated result.
+ */
+Datum
+anyarray_union_elem(PG_FUNCTION_ARGS)
+{
+	ArrayType  *a = PG_GETARG_ARRAYTYPE_P(0);
+	Datum		elem = PG_GETARG_DATUM(1);
+	Oid			elem_type = get_fn_expr_argtype(fcinfo->flinfo, 1);
+	AnyArrayTypeInfo *meta;
+	Datum	   *values;
+	int			nelems;
+	int			out_n;
+
+	ANYARRAY_CHECK_ARRAY(a);
+
+	if (ARR_ELEMTYPE(a) != elem_type)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("element type does not match array element type")));
+
+	meta = anyarray_get_meta(fcinfo, ARR_ELEMTYPE(a), false);
+
+	if (ARR_NDIM(a) == 0)
+	{
+		nelems = 0;
+		values = (Datum *) palloc(sizeof(Datum));
+	}
+	else
+	{
+		bool	   *nulls;
+
+		deconstruct_array(a, meta->element_type, meta->typlen,
+						  meta->typbyval, meta->typalign,
+						  &values, &nulls, &nelems);
+		pfree(nulls);
+		values = repalloc(values, sizeof(Datum) * (nelems + 1));
+	}
+
+	values[nelems++] = elem;
+
+	out_n = sort_uniq(values, nelems, meta);
+	PG_RETURN_ARRAYTYPE_P(make_array_from_datums(values, out_n, meta));
+}
+
+/*
+ * Difference: returns sorted, deduplicated values from "a" that are not in
+ * "b".
+ */
+Datum
+anyarray_difference(PG_FUNCTION_ARGS)
+{
+	ArrayType  *a = PG_GETARG_ARRAYTYPE_P(0);
+	ArrayType  *b = PG_GETARG_ARRAYTYPE_P(1);
+	AnyArrayTypeInfo *meta;
+	Datum	   *av,
+			   *bv,
+			   *out;
+	int			an,
+				bn,
+				i = 0,
+				j = 0,
+				k = 0;
+
+	meta = deconstruct_two(fcinfo, a, b, &av, &an, &bv, &bn);
+
+	an = sort_uniq(av, an, meta);
+	bn = sort_uniq(bv, bn, meta);
+
+	out = (Datum *) palloc(sizeof(Datum) * an);
+	while (i < an && j < bn)
+	{
+		int			cmp = anyarray_cmp_datum(&av[i], &bv[j], meta);
+
+		if (cmp == 0)
+		{
+			i++;
+			j++;
+		}
+		else if (cmp < 0)
+			out[k++] = av[i++];
+		else
+			j++;
+	}
+	while (i < an)
+		out[k++] = av[i++];
+
+	PG_RETURN_ARRAYTYPE_P(make_array_from_datums(out, k, meta));
+}
+
+/*
+ * array - element : remove all occurrences of "elem", preserving order.
+ * (Note: intarray returns the input order-preserved; we do the same.)
+ */
+Datum
+anyarray_difference_elem(PG_FUNCTION_ARGS)
+{
+	ArrayType  *a = PG_GETARG_ARRAYTYPE_P(0);
+	Datum		elem = PG_GETARG_DATUM(1);
+	Oid			elem_type = get_fn_expr_argtype(fcinfo->flinfo, 1);
+	AnyArrayTypeInfo *meta;
+	Datum	   *values;
+	int			nelems;
+	int			i,
+				k = 0;
+
+	ANYARRAY_CHECK_ARRAY(a);
+
+	if (ARR_ELEMTYPE(a) != elem_type)
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("element type does not match array element type")));
+
+	meta = anyarray_get_meta(fcinfo, ARR_ELEMTYPE(a), false);
+
+	if (ARR_NDIM(a) == 0)
+		PG_RETURN_ARRAYTYPE_P(construct_empty_array(meta->element_type));
+
+	{
+		bool	   *nulls;
+
+		deconstruct_array(a, meta->element_type, meta->typlen,
+						  meta->typbyval, meta->typalign,
+						  &values, &nulls, &nelems);
+		pfree(nulls);
+	}
+
+	for (i = 0; i < nelems; i++)
+	{
+		Datum		eq = FunctionCall2Coll(&meta->eq_proc, meta->typcollation,
+										   values[i], elem);
+
+		if (!DatumGetBool(eq))
+			values[k++] = values[i];
+	}
+
+	PG_RETURN_ARRAYTYPE_P(make_array_from_datums(values, k, meta));
+}
diff --git a/contrib/anyarray/expected/anyarray.out b/contrib/anyarray/expected/anyarray.out
new file mode 100644
index 00000000000..b17c6ef033e
--- /dev/null
+++ b/contrib/anyarray/expected/anyarray.out
@@ -0,0 +1,727 @@
+CREATE EXTENSION anyarray;
+--
+-- Phase 1: set operations, helpers and operators for int8, uuid, text
+--
+-- ===== int8 =====
+SELECT anyarray_sort(ARRAY[3, 1, 2, 1]::int8[]);
+ anyarray_sort 
+---------------
+ {1,1,2,3}
+(1 row)
+
+SELECT anyarray_sort(ARRAY[3, 1, 2, 1]::int8[], 'desc');
+ anyarray_sort 
+---------------
+ {3,2,1,1}
+(1 row)
+
+SELECT anyarray_sort(ARRAY[]::int8[]);
+ anyarray_sort 
+---------------
+ {}
+(1 row)
+
+SELECT anyarray_uniq(ARRAY[1, 1, 2, 3, 3, 2]::int8[]);
+ anyarray_uniq 
+---------------
+ {1,2,3}
+(1 row)
+
+SELECT anyarray_idx(ARRAY[10, 20, 30]::int8[], 20::int8);
+ anyarray_idx 
+--------------
+            2
+(1 row)
+
+SELECT anyarray_idx(ARRAY[10, 20, 30]::int8[], 99::int8);
+ anyarray_idx 
+--------------
+            0
+(1 row)
+
+SELECT anyarray_subarray(ARRAY[1,2,3,4,5]::int8[], 2, 2);
+ anyarray_subarray 
+-------------------
+ {2,3}
+(1 row)
+
+SELECT anyarray_subarray(ARRAY[1,2,3,4,5]::int8[], 3);
+ anyarray_subarray 
+-------------------
+ {3,4,5}
+(1 row)
+
+SELECT anyarray_subarray(ARRAY[1,2,3]::int8[], 5, 1);
+ anyarray_subarray 
+-------------------
+ {}
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] & ARRAY[2,3,4]::int8[];
+ ?column? 
+----------
+ {2,3}
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] | ARRAY[3,4,5]::int8[];
+  ?column?   
+-------------
+ {1,2,3,4,5}
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] | 4::int8;
+ ?column?  
+-----------
+ {1,2,3,4}
+(1 row)
+
+SELECT ARRAY[1,2,3,4]::int8[] - ARRAY[2,4]::int8[];
+ ?column? 
+----------
+ {1,3}
+(1 row)
+
+SELECT ARRAY[1,2,2,3,2]::int8[] - 2::int8;
+ ?column? 
+----------
+ {1,3}
+(1 row)
+
+SELECT # ARRAY[10,20,30]::int8[];
+ ?column? 
+----------
+        3
+(1 row)
+
+SELECT ARRAY[10,20,30]::int8[] # 20::int8;
+ ?column? 
+----------
+        2
+(1 row)
+
+-- ===== uuid =====
+SELECT anyarray_sort(ARRAY[
+  '00000000-0000-0000-0000-000000000003'::uuid,
+  '00000000-0000-0000-0000-000000000001',
+  '00000000-0000-0000-0000-000000000002']);
+                                                  anyarray_sort                                                   
+------------------------------------------------------------------------------------------------------------------
+ {00000000-0000-0000-0000-000000000001,00000000-0000-0000-0000-000000000002,00000000-0000-0000-0000-000000000003}
+(1 row)
+
+SELECT anyarray_uniq(ARRAY[
+  '00000000-0000-0000-0000-000000000001'::uuid,
+  '00000000-0000-0000-0000-000000000001',
+  '00000000-0000-0000-0000-000000000002']);
+                                anyarray_uniq                                
+-----------------------------------------------------------------------------
+ {00000000-0000-0000-0000-000000000001,00000000-0000-0000-0000-000000000002}
+(1 row)
+
+SELECT anyarray_idx(ARRAY[
+  '00000000-0000-0000-0000-000000000001'::uuid,
+  '00000000-0000-0000-0000-000000000002'],
+  '00000000-0000-0000-0000-000000000002'::uuid);
+ anyarray_idx 
+--------------
+            2
+(1 row)
+
+SELECT ARRAY['00000000-0000-0000-0000-000000000001'::uuid,
+             '00000000-0000-0000-0000-000000000002']
+     & ARRAY['00000000-0000-0000-0000-000000000002'::uuid,
+             '00000000-0000-0000-0000-000000000003'];
+                ?column?                
+----------------------------------------
+ {00000000-0000-0000-0000-000000000002}
+(1 row)
+
+SELECT ARRAY['00000000-0000-0000-0000-000000000001'::uuid]
+     | '00000000-0000-0000-0000-000000000002'::uuid;
+                                  ?column?                                   
+-----------------------------------------------------------------------------
+ {00000000-0000-0000-0000-000000000001,00000000-0000-0000-0000-000000000002}
+(1 row)
+
+-- ===== text =====
+SELECT anyarray_sort(ARRAY['banana','apple','cherry']);
+     anyarray_sort     
+-----------------------
+ {apple,banana,cherry}
+(1 row)
+
+SELECT anyarray_sort(ARRAY['banana','apple','cherry'], 'desc');
+     anyarray_sort     
+-----------------------
+ {cherry,banana,apple}
+(1 row)
+
+SELECT anyarray_uniq(ARRAY['a','b','a','c','b']);
+ anyarray_uniq 
+---------------
+ {a,b,c}
+(1 row)
+
+SELECT anyarray_idx(ARRAY['a','b','c'], 'b'::text);
+ anyarray_idx 
+--------------
+            2
+(1 row)
+
+SELECT ARRAY['a','b','c']::text[] & ARRAY['b','c','d']::text[];
+ ?column? 
+----------
+ {b,c}
+(1 row)
+
+SELECT ARRAY['a','b','c']::text[] | 'd'::text;
+ ?column?  
+-----------
+ {a,b,c,d}
+(1 row)
+
+SELECT ARRAY['a','b','c','b']::text[] - 'b'::text;
+ ?column? 
+----------
+ {a,c}
+(1 row)
+
+-- ===== error cases =====
+-- multi-dim
+SELECT anyarray_sort(ARRAY[[1,2],[3,4]]::int8[]);
+ERROR:  multidimensional arrays are not supported
+-- nulls
+SELECT anyarray_sort(ARRAY[1, NULL, 2]::int8[]);
+ERROR:  array must not contain nulls
+-- bad direction
+SELECT anyarray_sort(ARRAY[1,2,3]::int8[], 'bogus');
+ERROR:  second parameter must be "asc" or "desc"
+--
+-- Phase 2: anyquery and @@ operator
+--
+-- parsing + output round-trip
+SELECT '1 & 2'::anyquery::text;
+ text  
+-------
+ 1 & 2
+(1 row)
+
+SELECT '1 & 2 | 3'::anyquery::text;
+   text    
+-----------
+ 1 & 2 | 3
+(1 row)
+
+SELECT '(1 | 2) & 3'::anyquery::text;
+     text      
+---------------
+ ( 1 | 2 ) & 3
+(1 row)
+
+SELECT '!1 & 2'::anyquery::text;
+  text  
+--------
+ !1 & 2
+(1 row)
+
+SELECT '!(1 | 2) & 3'::anyquery::text;
+        text        
+--------------------
+ !( ( 1 | 2 ) ) & 3
+(1 row)
+
+SELECT '"hello world" | foo'::anyquery::text;
+        text         
+---------------------
+ "hello world" | foo
+(1 row)
+
+SELECT anyquery_querytree('1 & 2 | 3'::anyquery);
+ anyquery_querytree 
+--------------------
+ 1 2 & 3 |
+(1 row)
+
+-- int8 matching
+SELECT ARRAY[1,2,3]::int8[] @@ '1'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] @@ '1 & 2'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] @@ '1 & 4'::anyquery;
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] @@ '1 | 4'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] @@ '!4'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] @@ '!1'::anyquery;
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] @@ '!4 & 1'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT ARRAY[1,2,3]::int8[] @@ '(1 | 4) & (2 | 5)'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- commutator
+SELECT '1 & 2'::anyquery ~~ ARRAY[1,2,3]::int8[];
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '1 & 4'::anyquery ~~ ARRAY[1,2,3]::int8[];
+ ?column? 
+----------
+ f
+(1 row)
+
+-- uuid
+SELECT ARRAY['00000000-0000-0000-0000-000000000001'::uuid,
+             '00000000-0000-0000-0000-000000000002']
+       @@ '00000000-0000-0000-0000-000000000001 & 00000000-0000-0000-0000-000000000002'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT ARRAY['00000000-0000-0000-0000-000000000001'::uuid]
+       @@ '00000000-0000-0000-0000-000000000002'::anyquery;
+ ?column? 
+----------
+ f
+(1 row)
+
+-- text
+SELECT ARRAY['apple','banana','cherry']::text[] @@ 'apple & banana'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT ARRAY['apple','banana','cherry']::text[] @@ '"banana" | grape'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT ARRAY['apple','banana','cherry']::text[] @@ 'durian | grape'::anyquery;
+ ?column? 
+----------
+ f
+(1 row)
+
+-- empty array
+SELECT ARRAY[]::int8[] @@ '1'::anyquery;
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT ARRAY[]::int8[] @@ '!1'::anyquery;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- parse errors
+SELECT ''::anyquery;
+ERROR:  unexpected end of input in anyquery
+LINE 1: SELECT ''::anyquery;
+               ^
+SELECT '1 &'::anyquery;
+ERROR:  unexpected end of input in anyquery
+LINE 1: SELECT '1 &'::anyquery;
+               ^
+SELECT '1 & (2'::anyquery;
+ERROR:  missing closing parenthesis in anyquery
+LINE 1: SELECT '1 & (2'::anyquery;
+               ^
+SELECT '1 ) 2'::anyquery;
+ERROR:  unexpected trailing input in anyquery
+LINE 1: SELECT '1 ) 2'::anyquery;
+               ^
+-- runtime errors: token cannot be parsed as element type
+SELECT ARRAY[1,2]::int8[] @@ 'abc'::anyquery;
+ERROR:  invalid input syntax for type bigint: "abc"
+--
+-- Phase 3: GiST signature index
+--
+-- amvalidate: well-formed opclass
+SELECT amname, opcname FROM pg_opclass opc
+LEFT JOIN pg_am am ON am.oid = opcmethod
+WHERE opc.opcname = 'anyarray_gist_ops' AND NOT amvalidate(opc.oid);
+ amname | opcname 
+--------+---------
+(0 rows)
+
+-- int8 GiST: build deterministic table
+CREATE TABLE anyarray_gist_int8 (id int, a int8[]);
+INSERT INTO anyarray_gist_int8 VALUES
+  (1, ARRAY[1,2,3]::int8[]),
+  (2, ARRAY[10,20,30]::int8[]),
+  (3, ARRAY[10,20,30]::int8[]),
+  (4, ARRAY[1,2,3,4,5]::int8[]),
+  (5, ARRAY[100,200]::int8[]);
+CREATE INDEX anyarray_gist_int8_idx
+  ON anyarray_gist_int8 USING gist(a anyarray_gist_ops);
+SET enable_seqscan = off;
+SELECT id FROM anyarray_gist_int8 WHERE a @> ARRAY[1,2]::int8[] ORDER BY id;
+ id 
+----
+  1
+  4
+(2 rows)
+
+SELECT id FROM anyarray_gist_int8 WHERE a && ARRAY[10,200]::int8[] ORDER BY id;
+ id 
+----
+  2
+  3
+  5
+(3 rows)
+
+SELECT id FROM anyarray_gist_int8 WHERE a = ARRAY[10,20,30]::int8[] ORDER BY id;
+ id 
+----
+  2
+  3
+(2 rows)
+
+SELECT id FROM anyarray_gist_int8 WHERE a <@ ARRAY[1,2,3,4,5]::int8[] ORDER BY id;
+ id 
+----
+  1
+  4
+(2 rows)
+
+SELECT id FROM anyarray_gist_int8 WHERE a @@ '10 & 20'::anyquery ORDER BY id;
+ id 
+----
+  2
+  3
+(2 rows)
+
+SELECT id FROM anyarray_gist_int8 WHERE a @@ '1 | 100'::anyquery ORDER BY id;
+ id 
+----
+  1
+  4
+  5
+(3 rows)
+
+SELECT id FROM anyarray_gist_int8 WHERE a @@ '!1 & 10'::anyquery ORDER BY id;
+ id 
+----
+  2
+  3
+(2 rows)
+
+-- result consistency: GiST must agree with seqscan
+SELECT (
+  SELECT count(*) FROM anyarray_gist_int8 WHERE a @> ARRAY[10]::int8[]
+) = (
+  SELECT count(*) FROM (SELECT 1 FROM anyarray_gist_int8 WHERE a @> ARRAY[10]::int8[]) s
+) AS gist_matches_seqscan;
+ gist_matches_seqscan 
+----------------------
+ t
+(1 row)
+
+RESET enable_seqscan;
+-- text GiST
+CREATE TABLE anyarray_gist_text (id int, a text[]);
+INSERT INTO anyarray_gist_text VALUES
+  (1, ARRAY['apple','banana']),
+  (2, ARRAY['banana','cherry']),
+  (3, ARRAY['apple','cherry','durian']);
+CREATE INDEX anyarray_gist_text_idx
+  ON anyarray_gist_text USING gist(a anyarray_gist_ops(siglen=64));
+SET enable_seqscan = off;
+SELECT id FROM anyarray_gist_text WHERE a @> ARRAY['apple']::text[] ORDER BY id;
+ id 
+----
+  1
+  3
+(2 rows)
+
+SELECT id FROM anyarray_gist_text WHERE a @@ 'apple & cherry'::anyquery ORDER BY id;
+ id 
+----
+  3
+(1 row)
+
+RESET enable_seqscan;
+-- siglen bounds
+CREATE INDEX ON anyarray_gist_text USING gist(a anyarray_gist_ops(siglen=0));
+ERROR:  value 0 out of bounds for option "siglen"
+DETAIL:  Valid values are between "1" and "2024".
+CREATE INDEX ON anyarray_gist_text USING gist(a anyarray_gist_ops(siglen=8193));
+ERROR:  value 8193 out of bounds for option "siglen"
+DETAIL:  Valid values are between "1" and "2024".
+--
+-- Phase 4: GIN index (per-type, supports standard ops + @@)
+--
+-- amvalidate: all three opclasses
+SELECT amname, opcname FROM pg_opclass opc
+LEFT JOIN pg_am am ON am.oid = opcmethod
+WHERE opc.opcname LIKE '%anyquery_gin_ops' AND NOT amvalidate(opc.oid);
+ amname | opcname 
+--------+---------
+(0 rows)
+
+-- int8 GIN
+CREATE TABLE anyarray_gin_int8 (id int, a int8[]);
+INSERT INTO anyarray_gin_int8 VALUES
+  (1, ARRAY[1,2,3]::int8[]),
+  (2, ARRAY[10,20,30]::int8[]),
+  (3, ARRAY[10,20,30]::int8[]),
+  (4, ARRAY[1,2,3,4,5]::int8[]),
+  (5, ARRAY[100,200]::int8[]);
+CREATE INDEX anyarray_gin_int8_idx
+  ON anyarray_gin_int8 USING gin(a int8_anyquery_gin_ops);
+SET enable_seqscan = off;
+SELECT id FROM anyarray_gin_int8 WHERE a @> ARRAY[1,2]::int8[] ORDER BY id;
+ id 
+----
+  1
+  4
+(2 rows)
+
+SELECT id FROM anyarray_gin_int8 WHERE a && ARRAY[10,200]::int8[] ORDER BY id;
+ id 
+----
+  2
+  3
+  5
+(3 rows)
+
+SELECT id FROM anyarray_gin_int8 WHERE a @@ '10 & 20'::anyquery ORDER BY id;
+ id 
+----
+  2
+  3
+(2 rows)
+
+SELECT id FROM anyarray_gin_int8 WHERE a @@ '(1 & 2) | 100'::anyquery ORDER BY id;
+ id 
+----
+  1
+  4
+  5
+(3 rows)
+
+SELECT id FROM anyarray_gin_int8 WHERE a @@ '!1'::anyquery ORDER BY id;
+ id 
+----
+  2
+  3
+  5
+(3 rows)
+
+-- uuid GIN
+CREATE TABLE anyarray_gin_uuid (id int, a uuid[]);
+INSERT INTO anyarray_gin_uuid VALUES
+  (1, ARRAY['11111111-1111-1111-1111-111111111111'::uuid,
+            '22222222-2222-2222-2222-222222222222']),
+  (2, ARRAY['33333333-3333-3333-3333-333333333333'::uuid]),
+  (3, ARRAY['11111111-1111-1111-1111-111111111111'::uuid,
+            '33333333-3333-3333-3333-333333333333']);
+CREATE INDEX anyarray_gin_uuid_idx
+  ON anyarray_gin_uuid USING gin(a uuid_anyquery_gin_ops);
+SET enable_seqscan = off;
+SELECT id FROM anyarray_gin_uuid
+  WHERE a @@ '11111111-1111-1111-1111-111111111111 & 22222222-2222-2222-2222-222222222222'::anyquery
+  ORDER BY id;
+ id 
+----
+  1
+(1 row)
+
+SELECT id FROM anyarray_gin_uuid
+  WHERE a @> ARRAY['11111111-1111-1111-1111-111111111111'::uuid]
+  ORDER BY id;
+ id 
+----
+  1
+  3
+(2 rows)
+
+RESET enable_seqscan;
+-- text GIN
+CREATE TABLE anyarray_gin_text (id int, a text[]);
+INSERT INTO anyarray_gin_text VALUES
+  (1, ARRAY['apple','banana']),
+  (2, ARRAY['banana','cherry']),
+  (3, ARRAY['apple','cherry','durian']);
+CREATE INDEX anyarray_gin_text_idx
+  ON anyarray_gin_text USING gin(a text_anyquery_gin_ops);
+SET enable_seqscan = off;
+SELECT id FROM anyarray_gin_text WHERE a @@ 'apple & cherry'::anyquery ORDER BY id;
+ id 
+----
+  3
+(1 row)
+
+SELECT id FROM anyarray_gin_text WHERE a @@ '"durian"'::anyquery ORDER BY id;
+ id 
+----
+  3
+(1 row)
+
+SELECT id FROM anyarray_gin_text WHERE a @@ 'apple | grape'::anyquery ORDER BY id;
+ id 
+----
+  1
+  3
+(2 rows)
+
+SELECT id FROM anyarray_gin_text WHERE a @@ '!banana'::anyquery ORDER BY id;
+ id 
+----
+  3
+(1 row)
+
+RESET enable_seqscan;
+--
+-- Phase 5: cross-AM consistency and edge cases
+--
+-- Build a larger deterministic int8 table and index it with both GiST and
+-- GIN.  Then run each candidate query under seqscan, GiST and GIN and check
+-- that all three plans return the same row set.
+--
+CREATE TABLE anyarray_xcheck (id int, a int8[]);
+INSERT INTO anyarray_xcheck
+SELECT g,
+       ARRAY[(g % 7)::int8, ((g * 3) % 11)::int8, ((g + 1) % 5)::int8]
+FROM generate_series(1, 200) g;
+-- a few hand-picked rows to exercise common values
+INSERT INTO anyarray_xcheck VALUES
+  (1001, ARRAY[1,2,3,4,5]::int8[]),
+  (1002, ARRAY[1,2,3,4,5]::int8[]),
+  (1003, ARRAY[100,200,300]::int8[]),
+  (1004, ARRAY[]::int8[]),
+  (1005, NULL);
+CREATE INDEX anyarray_xcheck_gist ON anyarray_xcheck
+  USING gist(a anyarray_gist_ops);
+CREATE INDEX anyarray_xcheck_gin ON anyarray_xcheck
+  USING gin(a int8_anyquery_gin_ops);
+-- Run each predicate three times: pure seqscan, GiST-only, GIN-only.
+-- All three result sets must match.
+CREATE FUNCTION anyarray_xcheck_match(pred text)
+RETURNS TABLE(gist_ok bool, gin_ok bool)
+LANGUAGE plpgsql AS $$
+DECLARE
+  seq int[]; gst int[]; gin int[];
+BEGIN
+  SET LOCAL enable_seqscan = on;
+  SET LOCAL enable_indexscan = off;
+  SET LOCAL enable_bitmapscan = off;
+  EXECUTE 'SELECT array_agg(id ORDER BY id) FROM anyarray_xcheck WHERE '
+          || pred INTO seq;
+
+  -- Force GiST by hiding the GIN index.
+  ALTER INDEX anyarray_xcheck_gin SET (fastupdate = off);
+  SET LOCAL enable_seqscan = off;
+  SET LOCAL enable_indexscan = on;
+  SET LOCAL enable_bitmapscan = on;
+  DROP INDEX anyarray_xcheck_gin;
+  EXECUTE 'SELECT array_agg(id ORDER BY id) FROM anyarray_xcheck WHERE '
+          || pred INTO gst;
+  CREATE INDEX anyarray_xcheck_gin ON anyarray_xcheck
+    USING gin(a int8_anyquery_gin_ops);
+
+  -- Force GIN by hiding GiST.
+  DROP INDEX anyarray_xcheck_gist;
+  EXECUTE 'SELECT array_agg(id ORDER BY id) FROM anyarray_xcheck WHERE '
+          || pred INTO gin;
+  CREATE INDEX anyarray_xcheck_gist ON anyarray_xcheck
+    USING gist(a anyarray_gist_ops);
+
+  gist_ok := seq IS NOT DISTINCT FROM gst;
+  gin_ok  := seq IS NOT DISTINCT FROM gin;
+  RETURN NEXT;
+END $$;
+SELECT pred, gist_ok, gin_ok FROM (VALUES
+  ('a @> ARRAY[1,2]::int8[]'),
+  ('a @> ARRAY[100,200]::int8[]'),
+  ('a && ARRAY[5,99]::int8[]'),
+  ('a = ARRAY[1,2,3,4,5]::int8[]'),
+  ('a @@ ''1 & 2''::anyquery'),
+  ('a @@ ''100 | 999''::anyquery'),
+  ('a @@ ''!1 & 2''::anyquery')
+) v(pred), LATERAL anyarray_xcheck_match(pred);
+             pred             | gist_ok | gin_ok 
+------------------------------+---------+--------
+ a @> ARRAY[1,2]::int8[]      | t       | t
+ a @> ARRAY[100,200]::int8[]  | t       | t
+ a && ARRAY[5,99]::int8[]     | t       | t
+ a = ARRAY[1,2,3,4,5]::int8[] | t       | t
+ a @@ '1 & 2'::anyquery       | t       | t
+ a @@ '100 | 999'::anyquery   | t       | t
+ a @@ '!1 & 2'::anyquery      | t       | t
+(7 rows)
+
+-- Edge cases: empty / NULL arrays via index
+SET enable_seqscan = off;
+SELECT id FROM anyarray_xcheck WHERE a @> ARRAY[]::int8[] AND id = 1004;
+  id  
+------
+ 1004
+(1 row)
+
+SELECT id FROM anyarray_xcheck WHERE a <@ ARRAY[1,2,3]::int8[] AND id < 100 ORDER BY id LIMIT 3;
+ id 
+----
+  1
+ 15
+ 30
+(3 rows)
+
+SELECT count(*) FROM anyarray_xcheck WHERE a IS NULL;
+ count 
+-------
+     1
+(1 row)
+
+RESET enable_seqscan;
+-- DELETE / VACUUM / re-query through index
+DELETE FROM anyarray_xcheck WHERE id BETWEEN 1 AND 50;
+VACUUM anyarray_xcheck;
+SET enable_seqscan = off;
+SELECT count(*) FROM anyarray_xcheck WHERE a @@ '1 | 2'::anyquery;
+ count 
+-------
+    98
+(1 row)
+
+RESET enable_seqscan;
+DROP FUNCTION anyarray_xcheck_match(text);
diff --git a/contrib/anyarray/meson.build b/contrib/anyarray/meson.build
new file mode 100644
index 00000000000..a20646b82b2
--- /dev/null
+++ b/contrib/anyarray/meson.build
@@ -0,0 +1,38 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+anyarray_sources = files(
+  'anyarray.c',
+  'anyarray_bool.c',
+  'anyarray_gin.c',
+  'anyarray_gist.c',
+  'anyarray_op.c',
+)
+
+if host_system == 'windows'
+  anyarray_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'anyarray',
+    '--FILEDESC', 'anyarray - operations and indexes for arrays of any type',])
+endif
+
+anyarray = shared_module('anyarray',
+  anyarray_sources,
+  kwargs: contrib_mod_args,
+)
+contrib_targets += anyarray
+
+install_data(
+  'anyarray--1.0.sql',
+  'anyarray.control',
+  kwargs: contrib_data_args,
+)
+
+tests += {
+  'name': 'anyarray',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'anyarray',
+    ],
+  },
+}
diff --git a/contrib/anyarray/sql/anyarray.sql b/contrib/anyarray/sql/anyarray.sql
new file mode 100644
index 00000000000..90cc061fbde
--- /dev/null
+++ b/contrib/anyarray/sql/anyarray.sql
@@ -0,0 +1,330 @@
+CREATE EXTENSION anyarray;
+
+--
+-- Phase 1: set operations, helpers and operators for int8, uuid, text
+--
+
+-- ===== int8 =====
+SELECT anyarray_sort(ARRAY[3, 1, 2, 1]::int8[]);
+SELECT anyarray_sort(ARRAY[3, 1, 2, 1]::int8[], 'desc');
+SELECT anyarray_sort(ARRAY[]::int8[]);
+SELECT anyarray_uniq(ARRAY[1, 1, 2, 3, 3, 2]::int8[]);
+SELECT anyarray_idx(ARRAY[10, 20, 30]::int8[], 20::int8);
+SELECT anyarray_idx(ARRAY[10, 20, 30]::int8[], 99::int8);
+SELECT anyarray_subarray(ARRAY[1,2,3,4,5]::int8[], 2, 2);
+SELECT anyarray_subarray(ARRAY[1,2,3,4,5]::int8[], 3);
+SELECT anyarray_subarray(ARRAY[1,2,3]::int8[], 5, 1);
+
+SELECT ARRAY[1,2,3]::int8[] & ARRAY[2,3,4]::int8[];
+SELECT ARRAY[1,2,3]::int8[] | ARRAY[3,4,5]::int8[];
+SELECT ARRAY[1,2,3]::int8[] | 4::int8;
+SELECT ARRAY[1,2,3,4]::int8[] - ARRAY[2,4]::int8[];
+SELECT ARRAY[1,2,2,3,2]::int8[] - 2::int8;
+SELECT # ARRAY[10,20,30]::int8[];
+SELECT ARRAY[10,20,30]::int8[] # 20::int8;
+
+-- ===== uuid =====
+SELECT anyarray_sort(ARRAY[
+  '00000000-0000-0000-0000-000000000003'::uuid,
+  '00000000-0000-0000-0000-000000000001',
+  '00000000-0000-0000-0000-000000000002']);
+SELECT anyarray_uniq(ARRAY[
+  '00000000-0000-0000-0000-000000000001'::uuid,
+  '00000000-0000-0000-0000-000000000001',
+  '00000000-0000-0000-0000-000000000002']);
+SELECT anyarray_idx(ARRAY[
+  '00000000-0000-0000-0000-000000000001'::uuid,
+  '00000000-0000-0000-0000-000000000002'],
+  '00000000-0000-0000-0000-000000000002'::uuid);
+
+SELECT ARRAY['00000000-0000-0000-0000-000000000001'::uuid,
+             '00000000-0000-0000-0000-000000000002']
+     & ARRAY['00000000-0000-0000-0000-000000000002'::uuid,
+             '00000000-0000-0000-0000-000000000003'];
+
+SELECT ARRAY['00000000-0000-0000-0000-000000000001'::uuid]
+     | '00000000-0000-0000-0000-000000000002'::uuid;
+
+-- ===== text =====
+SELECT anyarray_sort(ARRAY['banana','apple','cherry']);
+SELECT anyarray_sort(ARRAY['banana','apple','cherry'], 'desc');
+SELECT anyarray_uniq(ARRAY['a','b','a','c','b']);
+SELECT anyarray_idx(ARRAY['a','b','c'], 'b'::text);
+SELECT ARRAY['a','b','c']::text[] & ARRAY['b','c','d']::text[];
+SELECT ARRAY['a','b','c']::text[] | 'd'::text;
+SELECT ARRAY['a','b','c','b']::text[] - 'b'::text;
+
+-- ===== error cases =====
+-- multi-dim
+SELECT anyarray_sort(ARRAY[[1,2],[3,4]]::int8[]);
+-- nulls
+SELECT anyarray_sort(ARRAY[1, NULL, 2]::int8[]);
+-- bad direction
+SELECT anyarray_sort(ARRAY[1,2,3]::int8[], 'bogus');
+
+--
+-- Phase 2: anyquery and @@ operator
+--
+
+-- parsing + output round-trip
+SELECT '1 & 2'::anyquery::text;
+SELECT '1 & 2 | 3'::anyquery::text;
+SELECT '(1 | 2) & 3'::anyquery::text;
+SELECT '!1 & 2'::anyquery::text;
+SELECT '!(1 | 2) & 3'::anyquery::text;
+SELECT '"hello world" | foo'::anyquery::text;
+SELECT anyquery_querytree('1 & 2 | 3'::anyquery);
+
+-- int8 matching
+SELECT ARRAY[1,2,3]::int8[] @@ '1'::anyquery;
+SELECT ARRAY[1,2,3]::int8[] @@ '1 & 2'::anyquery;
+SELECT ARRAY[1,2,3]::int8[] @@ '1 & 4'::anyquery;
+SELECT ARRAY[1,2,3]::int8[] @@ '1 | 4'::anyquery;
+SELECT ARRAY[1,2,3]::int8[] @@ '!4'::anyquery;
+SELECT ARRAY[1,2,3]::int8[] @@ '!1'::anyquery;
+SELECT ARRAY[1,2,3]::int8[] @@ '!4 & 1'::anyquery;
+SELECT ARRAY[1,2,3]::int8[] @@ '(1 | 4) & (2 | 5)'::anyquery;
+
+-- commutator
+SELECT '1 & 2'::anyquery ~~ ARRAY[1,2,3]::int8[];
+SELECT '1 & 4'::anyquery ~~ ARRAY[1,2,3]::int8[];
+
+-- uuid
+SELECT ARRAY['00000000-0000-0000-0000-000000000001'::uuid,
+             '00000000-0000-0000-0000-000000000002']
+       @@ '00000000-0000-0000-0000-000000000001 & 00000000-0000-0000-0000-000000000002'::anyquery;
+SELECT ARRAY['00000000-0000-0000-0000-000000000001'::uuid]
+       @@ '00000000-0000-0000-0000-000000000002'::anyquery;
+
+-- text
+SELECT ARRAY['apple','banana','cherry']::text[] @@ 'apple & banana'::anyquery;
+SELECT ARRAY['apple','banana','cherry']::text[] @@ '"banana" | grape'::anyquery;
+SELECT ARRAY['apple','banana','cherry']::text[] @@ 'durian | grape'::anyquery;
+
+-- empty array
+SELECT ARRAY[]::int8[] @@ '1'::anyquery;
+SELECT ARRAY[]::int8[] @@ '!1'::anyquery;
+
+-- parse errors
+SELECT ''::anyquery;
+SELECT '1 &'::anyquery;
+SELECT '1 & (2'::anyquery;
+SELECT '1 ) 2'::anyquery;
+
+-- runtime errors: token cannot be parsed as element type
+SELECT ARRAY[1,2]::int8[] @@ 'abc'::anyquery;
+
+--
+-- Phase 3: GiST signature index
+--
+
+-- amvalidate: well-formed opclass
+SELECT amname, opcname FROM pg_opclass opc
+LEFT JOIN pg_am am ON am.oid = opcmethod
+WHERE opc.opcname = 'anyarray_gist_ops' AND NOT amvalidate(opc.oid);
+
+-- int8 GiST: build deterministic table
+CREATE TABLE anyarray_gist_int8 (id int, a int8[]);
+INSERT INTO anyarray_gist_int8 VALUES
+  (1, ARRAY[1,2,3]::int8[]),
+  (2, ARRAY[10,20,30]::int8[]),
+  (3, ARRAY[10,20,30]::int8[]),
+  (4, ARRAY[1,2,3,4,5]::int8[]),
+  (5, ARRAY[100,200]::int8[]);
+
+CREATE INDEX anyarray_gist_int8_idx
+  ON anyarray_gist_int8 USING gist(a anyarray_gist_ops);
+
+SET enable_seqscan = off;
+
+SELECT id FROM anyarray_gist_int8 WHERE a @> ARRAY[1,2]::int8[] ORDER BY id;
+SELECT id FROM anyarray_gist_int8 WHERE a && ARRAY[10,200]::int8[] ORDER BY id;
+SELECT id FROM anyarray_gist_int8 WHERE a = ARRAY[10,20,30]::int8[] ORDER BY id;
+SELECT id FROM anyarray_gist_int8 WHERE a <@ ARRAY[1,2,3,4,5]::int8[] ORDER BY id;
+SELECT id FROM anyarray_gist_int8 WHERE a @@ '10 & 20'::anyquery ORDER BY id;
+SELECT id FROM anyarray_gist_int8 WHERE a @@ '1 | 100'::anyquery ORDER BY id;
+SELECT id FROM anyarray_gist_int8 WHERE a @@ '!1 & 10'::anyquery ORDER BY id;
+
+-- result consistency: GiST must agree with seqscan
+SELECT (
+  SELECT count(*) FROM anyarray_gist_int8 WHERE a @> ARRAY[10]::int8[]
+) = (
+  SELECT count(*) FROM (SELECT 1 FROM anyarray_gist_int8 WHERE a @> ARRAY[10]::int8[]) s
+) AS gist_matches_seqscan;
+
+RESET enable_seqscan;
+
+-- text GiST
+CREATE TABLE anyarray_gist_text (id int, a text[]);
+INSERT INTO anyarray_gist_text VALUES
+  (1, ARRAY['apple','banana']),
+  (2, ARRAY['banana','cherry']),
+  (3, ARRAY['apple','cherry','durian']);
+
+CREATE INDEX anyarray_gist_text_idx
+  ON anyarray_gist_text USING gist(a anyarray_gist_ops(siglen=64));
+
+SET enable_seqscan = off;
+SELECT id FROM anyarray_gist_text WHERE a @> ARRAY['apple']::text[] ORDER BY id;
+SELECT id FROM anyarray_gist_text WHERE a @@ 'apple & cherry'::anyquery ORDER BY id;
+RESET enable_seqscan;
+
+-- siglen bounds
+CREATE INDEX ON anyarray_gist_text USING gist(a anyarray_gist_ops(siglen=0));
+CREATE INDEX ON anyarray_gist_text USING gist(a anyarray_gist_ops(siglen=8193));
+
+--
+-- Phase 4: GIN index (per-type, supports standard ops + @@)
+--
+
+-- amvalidate: all three opclasses
+SELECT amname, opcname FROM pg_opclass opc
+LEFT JOIN pg_am am ON am.oid = opcmethod
+WHERE opc.opcname LIKE '%anyquery_gin_ops' AND NOT amvalidate(opc.oid);
+
+-- int8 GIN
+CREATE TABLE anyarray_gin_int8 (id int, a int8[]);
+INSERT INTO anyarray_gin_int8 VALUES
+  (1, ARRAY[1,2,3]::int8[]),
+  (2, ARRAY[10,20,30]::int8[]),
+  (3, ARRAY[10,20,30]::int8[]),
+  (4, ARRAY[1,2,3,4,5]::int8[]),
+  (5, ARRAY[100,200]::int8[]);
+
+CREATE INDEX anyarray_gin_int8_idx
+  ON anyarray_gin_int8 USING gin(a int8_anyquery_gin_ops);
+
+SET enable_seqscan = off;
+SELECT id FROM anyarray_gin_int8 WHERE a @> ARRAY[1,2]::int8[] ORDER BY id;
+SELECT id FROM anyarray_gin_int8 WHERE a && ARRAY[10,200]::int8[] ORDER BY id;
+SELECT id FROM anyarray_gin_int8 WHERE a @@ '10 & 20'::anyquery ORDER BY id;
+SELECT id FROM anyarray_gin_int8 WHERE a @@ '(1 & 2) | 100'::anyquery ORDER BY id;
+SELECT id FROM anyarray_gin_int8 WHERE a @@ '!1'::anyquery ORDER BY id;
+
+-- uuid GIN
+CREATE TABLE anyarray_gin_uuid (id int, a uuid[]);
+INSERT INTO anyarray_gin_uuid VALUES
+  (1, ARRAY['11111111-1111-1111-1111-111111111111'::uuid,
+            '22222222-2222-2222-2222-222222222222']),
+  (2, ARRAY['33333333-3333-3333-3333-333333333333'::uuid]),
+  (3, ARRAY['11111111-1111-1111-1111-111111111111'::uuid,
+            '33333333-3333-3333-3333-333333333333']);
+
+CREATE INDEX anyarray_gin_uuid_idx
+  ON anyarray_gin_uuid USING gin(a uuid_anyquery_gin_ops);
+
+SET enable_seqscan = off;
+SELECT id FROM anyarray_gin_uuid
+  WHERE a @@ '11111111-1111-1111-1111-111111111111 & 22222222-2222-2222-2222-222222222222'::anyquery
+  ORDER BY id;
+SELECT id FROM anyarray_gin_uuid
+  WHERE a @> ARRAY['11111111-1111-1111-1111-111111111111'::uuid]
+  ORDER BY id;
+RESET enable_seqscan;
+
+-- text GIN
+CREATE TABLE anyarray_gin_text (id int, a text[]);
+INSERT INTO anyarray_gin_text VALUES
+  (1, ARRAY['apple','banana']),
+  (2, ARRAY['banana','cherry']),
+  (3, ARRAY['apple','cherry','durian']);
+
+CREATE INDEX anyarray_gin_text_idx
+  ON anyarray_gin_text USING gin(a text_anyquery_gin_ops);
+
+SET enable_seqscan = off;
+SELECT id FROM anyarray_gin_text WHERE a @@ 'apple & cherry'::anyquery ORDER BY id;
+SELECT id FROM anyarray_gin_text WHERE a @@ '"durian"'::anyquery ORDER BY id;
+SELECT id FROM anyarray_gin_text WHERE a @@ 'apple | grape'::anyquery ORDER BY id;
+SELECT id FROM anyarray_gin_text WHERE a @@ '!banana'::anyquery ORDER BY id;
+RESET enable_seqscan;
+
+--
+-- Phase 5: cross-AM consistency and edge cases
+--
+-- Build a larger deterministic int8 table and index it with both GiST and
+-- GIN.  Then run each candidate query under seqscan, GiST and GIN and check
+-- that all three plans return the same row set.
+--
+
+CREATE TABLE anyarray_xcheck (id int, a int8[]);
+
+INSERT INTO anyarray_xcheck
+SELECT g,
+       ARRAY[(g % 7)::int8, ((g * 3) % 11)::int8, ((g + 1) % 5)::int8]
+FROM generate_series(1, 200) g;
+-- a few hand-picked rows to exercise common values
+INSERT INTO anyarray_xcheck VALUES
+  (1001, ARRAY[1,2,3,4,5]::int8[]),
+  (1002, ARRAY[1,2,3,4,5]::int8[]),
+  (1003, ARRAY[100,200,300]::int8[]),
+  (1004, ARRAY[]::int8[]),
+  (1005, NULL);
+
+CREATE INDEX anyarray_xcheck_gist ON anyarray_xcheck
+  USING gist(a anyarray_gist_ops);
+CREATE INDEX anyarray_xcheck_gin ON anyarray_xcheck
+  USING gin(a int8_anyquery_gin_ops);
+
+-- Run each predicate three times: pure seqscan, GiST-only, GIN-only.
+-- All three result sets must match.
+CREATE FUNCTION anyarray_xcheck_match(pred text)
+RETURNS TABLE(gist_ok bool, gin_ok bool)
+LANGUAGE plpgsql AS $$
+DECLARE
+  seq int[]; gst int[]; gin int[];
+BEGIN
+  SET LOCAL enable_seqscan = on;
+  SET LOCAL enable_indexscan = off;
+  SET LOCAL enable_bitmapscan = off;
+  EXECUTE 'SELECT array_agg(id ORDER BY id) FROM anyarray_xcheck WHERE '
+          || pred INTO seq;
+
+  -- Force GiST by hiding the GIN index.
+  ALTER INDEX anyarray_xcheck_gin SET (fastupdate = off);
+  SET LOCAL enable_seqscan = off;
+  SET LOCAL enable_indexscan = on;
+  SET LOCAL enable_bitmapscan = on;
+  DROP INDEX anyarray_xcheck_gin;
+  EXECUTE 'SELECT array_agg(id ORDER BY id) FROM anyarray_xcheck WHERE '
+          || pred INTO gst;
+  CREATE INDEX anyarray_xcheck_gin ON anyarray_xcheck
+    USING gin(a int8_anyquery_gin_ops);
+
+  -- Force GIN by hiding GiST.
+  DROP INDEX anyarray_xcheck_gist;
+  EXECUTE 'SELECT array_agg(id ORDER BY id) FROM anyarray_xcheck WHERE '
+          || pred INTO gin;
+  CREATE INDEX anyarray_xcheck_gist ON anyarray_xcheck
+    USING gist(a anyarray_gist_ops);
+
+  gist_ok := seq IS NOT DISTINCT FROM gst;
+  gin_ok  := seq IS NOT DISTINCT FROM gin;
+  RETURN NEXT;
+END $$;
+
+SELECT pred, gist_ok, gin_ok FROM (VALUES
+  ('a @> ARRAY[1,2]::int8[]'),
+  ('a @> ARRAY[100,200]::int8[]'),
+  ('a && ARRAY[5,99]::int8[]'),
+  ('a = ARRAY[1,2,3,4,5]::int8[]'),
+  ('a @@ ''1 & 2''::anyquery'),
+  ('a @@ ''100 | 999''::anyquery'),
+  ('a @@ ''!1 & 2''::anyquery')
+) v(pred), LATERAL anyarray_xcheck_match(pred);
+
+-- Edge cases: empty / NULL arrays via index
+SET enable_seqscan = off;
+SELECT id FROM anyarray_xcheck WHERE a @> ARRAY[]::int8[] AND id = 1004;
+SELECT id FROM anyarray_xcheck WHERE a <@ ARRAY[1,2,3]::int8[] AND id < 100 ORDER BY id LIMIT 3;
+SELECT count(*) FROM anyarray_xcheck WHERE a IS NULL;
+RESET enable_seqscan;
+
+-- DELETE / VACUUM / re-query through index
+DELETE FROM anyarray_xcheck WHERE id BETWEEN 1 AND 50;
+VACUUM anyarray_xcheck;
+SET enable_seqscan = off;
+SELECT count(*) FROM anyarray_xcheck WHERE a @@ '1 | 2'::anyquery;
+RESET enable_seqscan;
+
+DROP FUNCTION anyarray_xcheck_match(text);
diff --git a/contrib/meson.build b/contrib/meson.build
index ebb7f83d8c5..e158f18fa7e 100644
--- a/contrib/meson.build
+++ b/contrib/meson.build
@@ -13,6 +13,7 @@ contrib_doc_args = {
 }
 
 subdir('amcheck')
+subdir('anyarray')
 subdir('auth_delay')
 subdir('auto_explain')
 subdir('basic_archive')
diff --git a/doc/src/sgml/anyarray.sgml b/doc/src/sgml/anyarray.sgml
new file mode 100644
index 00000000000..4f376e0868c
--- /dev/null
+++ b/doc/src/sgml/anyarray.sgml
@@ -0,0 +1,482 @@
+<!-- doc/src/sgml/anyarray.sgml -->
+
+<sect1 id="anyarray" xreflabel="anyarray">
+ <title>anyarray &mdash; operations and indexes for arrays of any type</title>
+
+ <indexterm zone="anyarray">
+  <primary>anyarray</primary>
+ </indexterm>
+
+ <para>
+  The <filename>anyarray</filename> module generalizes
+  <xref linkend="intarray"/>-style operations to arrays of any element type
+  that provides a default B-tree opclass.  It adds set-style helpers, a
+  textual boolean query type (<type>anyquery</type>), and index support that
+  works for arbitrary element types.
+ </para>
+
+ <para>
+  All operations reject arrays that contain <literal>NULL</literal> elements,
+  and they are defined only for one-dimensional arrays.
+ </para>
+
+ <para>
+  This module is not marked as <quote>trusted</quote>: installing it
+  requires database-level privileges, because the index operator classes
+  it defines refer to internal storage types.
+ </para>
+
+ <sect2 id="anyarray-funcs-ops">
+  <title><filename>anyarray</filename> Functions and Operators</title>
+
+  <para>
+   The functions provided by <filename>anyarray</filename> are listed in
+   <xref linkend="anyarray-func-table"/>, the operators in
+   <xref linkend="anyarray-op-table"/>.
+  </para>
+
+  <table id="anyarray-func-table">
+   <title><filename>anyarray</filename> Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para>
+       <para>
+        Example(s)
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_sort</function> ( <type>anyarray</type> [, <type>text</type> ] )
+        <returnvalue>anyarray</returnvalue>
+       </para>
+       <para>
+        Sorts an array.  The optional second argument is
+        <literal>asc</literal> (default) or <literal>desc</literal>.
+       </para>
+       <para>
+        <literal>anyarray_sort('{3,1,2}'::int8[])</literal>
+        <returnvalue>{1,2,3}</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_uniq</function> ( <type>anyarray</type> )
+        <returnvalue>anyarray</returnvalue>
+       </para>
+       <para>
+        Removes duplicates from the array, returning the sorted unique
+        elements.
+       </para>
+       <para>
+        <literal>anyarray_uniq('{1,1,2,3,3}'::int8[])</literal>
+        <returnvalue>{1,2,3}</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_idx</function> ( <type>anyarray</type>, <type>anyelement</type> )
+        <returnvalue>integer</returnvalue>
+       </para>
+       <para>
+        Returns the 1-based index of the first occurrence of the element,
+        or 0 if not found.
+       </para>
+       <para>
+        <literal>anyarray_idx(ARRAY['a','b','c'], 'b')</literal>
+        <returnvalue>2</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_subarray</function> ( <type>anyarray</type>, <parameter>start</parameter> <type>integer</type> [, <parameter>length</parameter> <type>integer</type> ] )
+        <returnvalue>anyarray</returnvalue>
+       </para>
+       <para>
+        Returns a contiguous slice starting at <parameter>start</parameter>
+        (1-based).  If <parameter>length</parameter> is omitted the slice
+        extends to the end of the array.  Out-of-range positions or
+        non-positive lengths produce an empty array.
+       </para>
+       <para>
+        <literal>anyarray_subarray('{1,2,3,4,5}'::int8[], 2, 2)</literal>
+        <returnvalue>{2,3}</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_icount</function> ( <type>anyarray</type> )
+        <returnvalue>integer</returnvalue>
+       </para>
+       <para>
+        Returns the number of elements in the array (exposed under the
+        <literal>#</literal> operator).
+       </para>
+       <para>
+        <literal>anyarray_icount('{10,20,30}'::int8[])</literal>
+        <returnvalue>3</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_intersect</function> ( <type>anyarray</type>, <type>anyarray</type> )
+        <returnvalue>anyarray</returnvalue>
+       </para>
+       <para>
+        Returns the sorted, deduplicated values present in both inputs.
+       </para>
+       <para>
+        <literal>anyarray_intersect('{1,2,3}'::int8[], '{2,3,4}')</literal>
+        <returnvalue>{2,3}</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_union</function> ( <type>anyarray</type>, <type>anyarray</type> )
+        <returnvalue>anyarray</returnvalue>
+       </para>
+       <para>
+        Returns the sorted, deduplicated union of both inputs.
+       </para>
+       <para>
+        <literal>anyarray_union('{1,2}'::int8[], '{2,3}')</literal>
+        <returnvalue>{1,2,3}</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_union_elem</function> ( <type>anyarray</type>, <type>anyelement</type> )
+        <returnvalue>anyarray</returnvalue>
+       </para>
+       <para>
+        Adds an element to the array if not already present; result is
+        sorted and deduplicated.
+       </para>
+       <para>
+        <literal>anyarray_union_elem('{1,2,3}'::int8[], 4::int8)</literal>
+        <returnvalue>{1,2,3,4}</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_difference</function> ( <type>anyarray</type>, <type>anyarray</type> )
+        <returnvalue>anyarray</returnvalue>
+       </para>
+       <para>
+        Returns the sorted, deduplicated values from the first array that
+        are not in the second.
+       </para>
+       <para>
+        <literal>anyarray_difference('{1,2,3,4}'::int8[], '{2,4}')</literal>
+        <returnvalue>{1,3}</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyarray_difference_elem</function> ( <type>anyarray</type>, <type>anyelement</type> )
+        <returnvalue>anyarray</returnvalue>
+       </para>
+       <para>
+        Removes all occurrences of the given element, preserving the
+        relative order of the surviving elements.
+       </para>
+       <para>
+        <literal>anyarray_difference_elem('{1,2,2,3,2}'::int8[], 2::int8)</literal>
+        <returnvalue>{1,3}</returnvalue>
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <function>anyquery_querytree</function> ( <type>anyquery</type> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Returns the query in postfix form; useful for debugging.
+       </para>
+       <para>
+        <literal>anyquery_querytree('1 &amp; 2 | 3'::anyquery)</literal>
+        <returnvalue>1 2 &amp; 3 |</returnvalue>
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <table id="anyarray-op-table">
+    <title><filename>anyarray</filename> Operators</title>
+     <tgroup cols="1">
+      <thead>
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         Operator
+        </para>
+        <para>
+         Description
+        </para></entry>
+       </row>
+      </thead>
+
+      <tbody>
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <type></type> <literal>#</literal> <type>anyarray</type>
+         <returnvalue>integer</returnvalue>
+        </para>
+        <para>
+         Returns the number of elements in the array (prefix operator).
+        </para></entry>
+       </row>
+
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <type>anyarray</type> <literal>#</literal> <type>anyelement</type>
+         <returnvalue>integer</returnvalue>
+        </para>
+        <para>
+         Returns the 1-based index of the right operand in the array, or 0
+         if not found.  Same as <function>anyarray_idx</function>.
+        </para></entry>
+       </row>
+
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <type>anyarray</type> <literal>&amp;</literal> <type>anyarray</type>
+         <returnvalue>anyarray</returnvalue>
+        </para>
+        <para>
+         Set intersection of the operands.
+        </para></entry>
+       </row>
+
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <type>anyarray</type> <literal>|</literal> <type>anyarray</type>
+         <returnvalue>anyarray</returnvalue>
+        </para>
+        <para>
+         Set union of the operands.
+        </para></entry>
+       </row>
+
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <type>anyarray</type> <literal>|</literal> <type>anyelement</type>
+         <returnvalue>anyarray</returnvalue>
+        </para>
+        <para>
+         Adds the right operand to the array if not already present.
+        </para></entry>
+       </row>
+
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <type>anyarray</type> <literal>-</literal> <type>anyarray</type>
+         <returnvalue>anyarray</returnvalue>
+        </para>
+        <para>
+         Set difference: elements of the left array not present in the
+         right.
+        </para></entry>
+       </row>
+
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <type>anyarray</type> <literal>-</literal> <type>anyelement</type>
+         <returnvalue>anyarray</returnvalue>
+        </para>
+        <para>
+         Removes all occurrences of the right operand from the array.
+        </para></entry>
+       </row>
+
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <type>anyarray</type> <literal>@@</literal> <type>anyquery</type>
+         <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         True if the array satisfies the query (see below).
+        </para></entry>
+       </row>
+
+       <row>
+        <entry role="func_table_entry"><para role="func_signature">
+         <type>anyquery</type> <literal>~~</literal> <type>anyarray</type>
+         <returnvalue>boolean</returnvalue>
+        </para>
+        <para>
+         Commutator of <literal>@@</literal>.
+        </para></entry>
+       </row>
+      </tbody>
+     </tgroup>
+   </table>
+
+   <para>
+    The operators <literal>&amp;&amp;</literal>, <literal>@&gt;</literal>,
+    <literal>&lt;@</literal> and <literal>=</literal> on arrays are
+    <emphasis>not</emphasis> redefined by this extension; the built-in
+    PostgreSQL operators are used.
+   </para>
+ </sect2>
+
+ <sect2 id="anyarray-query">
+  <title>The <type>anyquery</type> Type</title>
+
+  <para>
+   <type>anyquery</type> represents a boolean expression over array
+   elements.  Values are textual tokens; the element type used to interpret
+   them is determined by the left-hand side of <literal>@@</literal> at
+   query time.  Tokens are combined with the operators
+   <literal>&amp;</literal> (AND), <literal>|</literal> (OR) and
+   <literal>!</literal> (NOT).  Parentheses control precedence; without
+   them <literal>&amp;</literal> binds tighter than <literal>|</literal>.
+  </para>
+
+  <para>
+   A token consists of any sequence of characters that are not whitespace
+   or one of <literal>&amp; | ! ( ) "</literal>.  To include those
+   characters or embedded whitespace, wrap the token in double quotes:
+   <literal>"hello world"</literal>.  Inside quotes, a backslash escapes
+   the next character.
+  </para>
+
+  <para>
+   At evaluation time, each token is parsed by the input function of the
+   array's element type.  This means
+   <literal>'1 &amp; 2'::anyquery @@ ARRAY[1,2,3]::bigint[]</literal>
+   parses <quote>1</quote> and <quote>2</quote> as <type>bigint</type>; the
+   same query against a <type>text[]</type> column compares the literal
+   strings <quote>1</quote> and <quote>2</quote>.  An unparseable token
+   raises an error at <literal>@@</literal>-time, not at parse time.
+  </para>
+ </sect2>
+
+ <sect2 id="anyarray-index">
+  <title>Index Support</title>
+
+  <para>
+   <filename>anyarray</filename> provides a polymorphic GiST opclass and
+   per-element-type GIN opclasses.  All of them support the standard array
+   operators (<literal>&amp;&amp;</literal>, <literal>@&gt;</literal>,
+   <literal>&lt;@</literal>, <literal>=</literal>) as well as
+   <literal>@@</literal>.
+  </para>
+
+  <sect3 id="anyarray-index-gist">
+   <title>GiST: <literal>anyarray_gist_ops</literal></title>
+
+   <para>
+    A single opclass on <type>anyarray</type> works for any element type
+    that has a default hash function.  Each indexed array is summarised as
+    a fixed-size bit vector by hashing every element into one bit; internal
+    nodes store the union of their children.  All matches are rechecked
+    because signatures are lossy.
+   </para>
+
+   <para>
+    The optional integer parameter <literal>siglen</literal> sets the
+    signature length in bytes.  The default is 252 bytes (2016 bits);
+    valid values are between 1 and the index-key size limit.  Longer
+    signatures produce more precise indexes at the cost of more storage.
+   </para>
+
+   <programlisting>
+CREATE INDEX my_idx ON my_table USING gist (col anyarray_gist_ops);
+CREATE INDEX my_idx ON my_table USING gist (col anyarray_gist_ops(siglen=64));
+   </programlisting>
+  </sect3>
+
+  <sect3 id="anyarray-index-gin">
+   <title>GIN: per-type opclasses</title>
+
+   <para>
+    Because the GIN extractQuery API does not expose the indexed column's
+    element type when the query is an <type>anyquery</type>, GIN support
+    is provided as one opclass per concrete element type.  Currently
+    supplied:
+   </para>
+
+   <itemizedlist>
+    <listitem><para>
+     <literal>int8_anyquery_gin_ops</literal> for <type>bigint[]</type>
+    </para></listitem>
+    <listitem><para>
+     <literal>uuid_anyquery_gin_ops</literal> for <type>uuid[]</type>
+    </para></listitem>
+    <listitem><para>
+     <literal>text_anyquery_gin_ops</literal> for <type>text[]</type>
+    </para></listitem>
+   </itemizedlist>
+
+   <para>
+    These opclasses are drop-in replacements for the built-in
+    <literal>array_ops</literal>; they additionally support
+    <literal>@@</literal>.
+   </para>
+
+   <programlisting>
+CREATE INDEX my_idx ON my_table USING gin (col int8_anyquery_gin_ops);
+   </programlisting>
+
+   <para>
+    For element types not in the list above, use the built-in GIN
+    <literal>array_ops</literal> for set queries and rely on
+    <literal>anyarray_gist_ops</literal> for <literal>@@</literal>.
+   </para>
+  </sect3>
+ </sect2>
+
+ <sect2 id="anyarray-example">
+  <title>Example</title>
+
+<programlisting>
+CREATE EXTENSION anyarray;
+
+CREATE TABLE message (mid INT PRIMARY KEY, tags TEXT[]);
+
+CREATE INDEX message_tags_gist ON message USING gist (tags anyarray_gist_ops);
+CREATE INDEX message_tags_gin  ON message USING gin  (tags text_anyquery_gin_ops);
+
+-- Messages tagged with both 'urgent' and 'bug':
+SELECT mid FROM message WHERE tags @&gt; ARRAY['urgent','bug'];
+
+-- Same, with the query syntax:
+SELECT mid FROM message WHERE tags @@ 'urgent &amp; bug'::anyquery;
+
+-- Messages tagged 'urgent' but not 'wontfix':
+SELECT mid FROM message WHERE tags @@ 'urgent &amp; !wontfix'::anyquery;
+
+-- Sort and deduplicate a tag list:
+SELECT anyarray_uniq(tags) FROM message WHERE mid = 42;
+</programlisting>
+ </sect2>
+
+ <sect2 id="anyarray-authors">
+  <title>Authors</title>
+
+  <para>
+   The <filename>anyarray</filename> module is a generalization of
+   <xref linkend="intarray"/> by Teodor Sigaev and Oleg Bartunov.
+  </para>
+ </sect2>
+
+</sect1>
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index b9b03654aad..c9aaf79cb69 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -128,6 +128,7 @@ CREATE EXTENSION <replaceable>extension_name</replaceable>;
  </para>
 
  &amcheck;
+ &anyarray;
  &auth-delay;
  &auto-explain;
  &basebackup-to-shell;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 25a85082759..0530342e74b 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -119,6 +119,7 @@
 <!-- contrib information -->
 <!ENTITY contrib         SYSTEM "contrib.sgml">
 <!ENTITY amcheck         SYSTEM "amcheck.sgml">
+<!ENTITY anyarray        SYSTEM "anyarray.sgml">
 <!ENTITY auth-delay      SYSTEM "auth-delay.sgml">
 <!ENTITY auto-explain    SYSTEM "auto-explain.sgml">
 <!ENTITY basic-archive   SYSTEM "basic-archive.sgml">
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index cbd9e10fc1d..049485f5c24 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -126,7 +126,13 @@ AnalyzeAttrFetchFunc
 AnalyzeForeignTable_function
 AnlExprData
 AnlIndexData
+AnyArrayGistKey
+AnyArrayGistOptions
 AnyArrayType
+AnyArrayTypeInfo
+AnyQuery
+AnyQueryCheck
+AnyQueryItem
 Append
 AppendPath
 AppendPathInput
@@ -1744,6 +1750,7 @@ MVNDistinct
 MVNDistinctItem
 ManyTestResource
 ManyTestResourceKind
+MatchContext
 Material
 MaterialPath
 MaterialState
@@ -2372,6 +2379,7 @@ PgXmlStrictness
 Pg_abi_values
 Pg_finfo_record
 Pg_magic_struct
+PickSplitEntry
 PipeProtoChunk
 PipeProtoHeader
 PlaceHolderInfo
-- 
2.39.5 (Apple Git-154)

