diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..91f216e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.deps/
+results/
+**/*.o
+**/*.so
+regression.diffs
+regression.out
+.vscode
+test/c_tests/test_send_recv
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..908853d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,16 @@
+Copyright (c) 2019, Tomas Vondra (tomas.vondra@postgresql.org).
+
+Permission to use, copy, modify, and distribute this software and its documentation
+for any purpose, without fee, and without a written agreement is hereby granted,
+provided that the above copyright notice and this paragraph and the following two
+paragraphs appear in all copies.
+
+IN NO EVENT SHALL $ORGANISATION BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
+INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE
+OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF TOMAS VONDRA HAS BEEN ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+
+TOMAS VONDRA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND $ORGANISATION HAS NO
+OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..85e7691
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,33 @@
+MODULE_big = hashset
+OBJS = hashset.o hashset-api.o
+
+EXTENSION = hashset
+DATA = hashset--0.0.1.sql
+MODULES = hashset
+
+# Keep the CFLAGS separate
+SERVER_INCLUDES=-I$(shell pg_config --includedir-server)
+CLIENT_INCLUDES=-I$(shell pg_config --includedir)
+LIBRARY_PATH = -L$(shell pg_config --libdir)
+
+REGRESS = prelude basic io_varying_lengths random table invalid parsing reported_bugs
+REGRESS_OPTS = --inputdir=test
+
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+
+C_TESTS_DIR = test/c_tests
+
+EXTRA_CLEAN = $(C_TESTS_DIR)/test_send_recv
+
+c_tests: $(C_TESTS_DIR)/test_send_recv
+
+$(C_TESTS_DIR)/test_send_recv: $(C_TESTS_DIR)/test_send_recv.c
+	$(CC) $(SERVER_INCLUDES) $(CLIENT_INCLUDES) -o $@ $< $(LIBRARY_PATH) -lpq
+
+run_c_tests: c_tests
+	cd $(C_TESTS_DIR) && ./test_send_recv.sh
+
+check: all $(REGRESS_PREP) run_c_tests
+
+include $(PGXS)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9c26cf6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,156 @@
+# hashset
+
+This PostgreSQL extension implements hashset, a data structure (type)
+providing a collection of unique, not null integer items with fast lookup.
+
+
+## Version
+
+0.0.1
+
+🚧 **NOTICE** 🚧 This repository is currently under active development and the hashset
+PostgreSQL extension is **not production-ready**. As the codebase is evolving
+with possible breaking changes, we are not providing any migration scripts
+until we reach our first release.
+
+
+## Usage
+
+After installing the extension, you can use the `int4hashset` data type and
+associated functions within your PostgreSQL queries.
+
+To demonstrate the usage, let's consider a hypothetical table `users` which has
+a `user_id` and a `user_likes` of type `int4hashset`.
+
+Firstly, let's create the table:
+
+```sql
+CREATE TABLE users(
+    user_id int PRIMARY KEY,
+    user_likes int4hashset DEFAULT int4hashset()
+);
+```
+In the above statement, the `int4hashset()` initializes an empty hashset
+with zero capacity. The hashset will automatically resize itself when more
+elements are added.
+
+Now, we can perform operations on this table. Here are some examples:
+
+```sql
+-- Insert a new user with id 1. The user_likes will automatically be initialized
+-- as an empty hashset
+INSERT INTO users (user_id) VALUES (1);
+
+-- Add elements (likes) for a user
+UPDATE users SET user_likes = hashset_add(user_likes, 101) WHERE user_id = 1;
+UPDATE users SET user_likes = hashset_add(user_likes, 202) WHERE user_id = 1;
+
+-- Check if a user likes a particular item
+SELECT hashset_contains(user_likes, 101) FROM users WHERE user_id = 1; -- true
+
+-- Count the number of likes a user has
+SELECT hashset_count(user_likes) FROM users WHERE user_id = 1; -- 2
+```
+
+You can also use the aggregate functions to perform operations on multiple rows.
+
+
+## Data types
+
+- **int4hashset**: This data type represents a set of integers. Internally, it uses
+a combination of a bitmap and a value array to store the elements in a set. It's
+a variable-length type.
+
+
+## Functions
+
+- `int4hashset([capacity int, load_factor float4, growth_factor float4, hashfn_id int4]) -> int4hashset`:
+  Initialize an empty int4hashset with optional parameters.
+    - `capacity` specifies the initial capacity, which is zero by default.
+    - `load_factor` represents the threshold for resizing the hashset and defaults to 0.75.
+    - `growth_factor` is the multiplier for resizing and defaults to 2.0.
+    - `hashfn_id` represents the hash function used.
+        - 1=Jenkins/lookup3 (default)
+        - 2=MurmurHash32
+        - 3=Naive hash function
+- `hashset_add(int4hashset, int) -> int4hashset`: Adds an integer to an int4hashset.
+- `hashset_contains(int4hashset, int) -> boolean`: Checks if an int4hashset contains a given integer.
+- `hashset_merge(int4hashset, int4hashset) -> int4hashset`: Merges two int4hashsets into a new int4hashset.
+- `hashset_to_array(int4hashset) -> int[]`: Converts an int4hashset to an array of integers.
+- `hashset_count(int4hashset) -> bigint`: Returns the number of elements in an int4hashset.
+- `hashset_capacity(int4hashset) -> bigint`: Returns the current capacity of an int4hashset.
+- `hashset_max_collisions(int4hashset) -> bigint`: Returns the maximum number of collisions that have occurred for a single element
+- `hashset_intersection(int4hashset, int4hashset) -> int4hashset`: Returns a new int4hashset that is the intersection of the two input sets.
+- `hashset_difference(int4hashset, int4hashset) -> int4hashset`: Returns a new int4hashset that contains the elements present in the first set but not in the second set.
+- `hashset_symmetric_difference(int4hashset, int4hashset) -> int4hashset`: Returns a new int4hashset containing elements that are in either of the input sets, but not in their intersection.
+
+## Aggregation Functions
+
+- `hashset_agg(int) -> int4hashset`: Aggregate integers into a hashset.
+- `hashset_agg(int4hashset) -> int4hashset`: Aggregate hashsets into a hashset.
+
+
+## Operators
+
+- Equality (`=`): Checks if two hashsets are equal.
+- Inequality (`<>`): Checks if two hashsets are not equal.
+
+
+## Hashset Hash Operators
+
+- `hashset_hash(int4hashset) -> integer`: Returns the hash value of an int4hashset.
+
+
+## Hashset Btree Operators
+
+- `<`, `<=`, `>`, `>=`: Comparison operators for hashsets.
+
+
+## Limitations
+
+- The `int4hashset` data type currently supports integers within the range of int4
+(-2147483648 to 2147483647).
+
+
+## Installation
+
+To install the extension on any platform, follow these general steps:
+
+1. Ensure you have PostgreSQL installed on your system, including the development files.
+2. Clone the repository.
+3. Navigate to the cloned repository directory.
+4. Compile the extension using `make`.
+5. Install the extension using `sudo make install`.
+6. Run the tests using `make installcheck` (optional).
+
+To use a different PostgreSQL installation, point configure to a different `pg_config`, using following command:
+```sh
+make PG_CONFIG=/else/where/pg_config
+sudo make install PG_CONFIG=/else/where/pg_config
+```
+
+In your PostgreSQL connection, enable the hashset extension using the following SQL command:
+```sql
+CREATE EXTENSION hashset;
+```
+
+This extension requires PostgreSQL version ?.? or later.
+
+For Ubuntu 22.04.1 LTS, you would run the following commands:
+
+```sh
+sudo apt install postgresql-15 postgresql-server-dev-15 postgresql-client-15
+git clone https://github.com/tvondra/hashset.git
+cd hashset
+make
+sudo make install
+make installcheck
+```
+
+Please note that this project is currently under active development and is not yet considered production-ready.
+
+## License
+
+This software is distributed under the terms of PostgreSQL license.
+See LICENSE or http://www.opensource.org/licenses/bsd-license.php for
+more details.
diff --git a/hashset--0.0.1.sql b/hashset--0.0.1.sql
new file mode 100644
index 0000000..a155190
--- /dev/null
+++ b/hashset--0.0.1.sql
@@ -0,0 +1,298 @@
+/*
+ * Hashset Type Definition
+ */
+
+CREATE TYPE int4hashset;
+
+CREATE OR REPLACE FUNCTION int4hashset_in(cstring)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_in'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION int4hashset_out(int4hashset)
+RETURNS cstring
+AS 'hashset', 'int4hashset_out'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION int4hashset_send(int4hashset)
+RETURNS bytea
+AS 'hashset', 'int4hashset_send'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION int4hashset_recv(internal)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_recv'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE TYPE int4hashset (
+    INPUT = int4hashset_in,
+    OUTPUT = int4hashset_out,
+    RECEIVE = int4hashset_recv,
+    SEND = int4hashset_send,
+    INTERNALLENGTH = variable,
+    STORAGE = external
+);
+
+/*
+ * Hashset Functions
+ */
+
+CREATE OR REPLACE FUNCTION int4hashset(
+    capacity int DEFAULT 0,
+    load_factor float4 DEFAULT 0.75,
+    growth_factor float4 DEFAULT 2.0,
+    hashfn_id int DEFAULT 1
+)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_init'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_add(int4hashset, int)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_add'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_contains(int4hashset, int)
+RETURNS bool
+AS 'hashset', 'int4hashset_contains'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_merge(int4hashset, int4hashset)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_merge'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_to_array(int4hashset)
+RETURNS int[]
+AS 'hashset', 'int4hashset_to_array'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_count(int4hashset)
+RETURNS bigint
+AS 'hashset', 'int4hashset_count'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_capacity(int4hashset)
+RETURNS bigint
+AS 'hashset', 'int4hashset_capacity'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_collisions(int4hashset)
+RETURNS bigint
+AS 'hashset', 'int4hashset_collisions'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_max_collisions(int4hashset)
+RETURNS bigint
+AS 'hashset', 'int4hashset_max_collisions'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION int4_add_int4hashset(int4, int4hashset)
+RETURNS int4hashset
+AS $$SELECT $2 || $1$$
+LANGUAGE SQL
+IMMUTABLE PARALLEL SAFE STRICT COST 1;
+
+CREATE OR REPLACE FUNCTION hashset_intersection(int4hashset, int4hashset)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_intersection'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_difference(int4hashset, int4hashset)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_difference'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_symmetric_difference(int4hashset, int4hashset)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_symmetric_difference'
+LANGUAGE C IMMUTABLE;
+
+/*
+ * Aggregation Functions
+ */
+
+CREATE OR REPLACE FUNCTION int4hashset_agg_add(p_pointer internal, p_value int)
+RETURNS internal
+AS 'hashset', 'int4hashset_agg_add'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION int4hashset_agg_final(p_pointer internal)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_agg_final'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION int4hashset_agg_combine(p_pointer internal, p_pointer2 internal)
+RETURNS internal
+AS 'hashset', 'int4hashset_agg_combine'
+LANGUAGE C IMMUTABLE;
+
+CREATE AGGREGATE hashset_agg(int) (
+    SFUNC = int4hashset_agg_add,
+    STYPE = internal,
+    FINALFUNC = int4hashset_agg_final,
+    COMBINEFUNC = int4hashset_agg_combine,
+    PARALLEL = SAFE
+);
+
+CREATE OR REPLACE FUNCTION int4hashset_agg_add_set(p_pointer internal, p_value int4hashset)
+RETURNS internal
+AS 'hashset', 'int4hashset_agg_add_set'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION int4hashset_agg_final(p_pointer internal)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_agg_final'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION int4hashset_agg_combine(p_pointer internal, p_pointer2 internal)
+RETURNS internal
+AS 'hashset', 'int4hashset_agg_combine'
+LANGUAGE C IMMUTABLE;
+
+CREATE AGGREGATE hashset_agg(int4hashset) (
+    SFUNC = int4hashset_agg_add_set,
+    STYPE = internal,
+    FINALFUNC = int4hashset_agg_final,
+    COMBINEFUNC = int4hashset_agg_combine,
+    PARALLEL = SAFE
+);
+
+/*
+ * Operator Definitions
+ */
+
+CREATE OR REPLACE FUNCTION hashset_equals(int4hashset, int4hashset)
+RETURNS bool
+AS 'hashset', 'int4hashset_equals'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR = (
+    LEFTARG = int4hashset,
+    RIGHTARG = int4hashset,
+    PROCEDURE = hashset_equals,
+    COMMUTATOR = =,
+    HASHES
+);
+
+CREATE OR REPLACE FUNCTION hashset_neq(int4hashset, int4hashset)
+RETURNS bool
+AS 'hashset', 'int4hashset_neq'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR <> (
+    LEFTARG = int4hashset,
+    RIGHTARG = int4hashset,
+    PROCEDURE = hashset_neq,
+    COMMUTATOR = '<>',
+    NEGATOR = '=',
+    RESTRICT = neqsel,
+    JOIN = neqjoinsel,
+    HASHES
+);
+
+CREATE OPERATOR || (
+    leftarg = int4hashset,
+    rightarg = int4,
+    function = hashset_add,
+    commutator = ||
+);
+
+CREATE OPERATOR || (
+    leftarg = int4,
+    rightarg = int4hashset,
+    function = int4_add_int4hashset,
+    commutator = ||
+);
+
+/*
+ * Hashset Hash Operators
+ */
+
+CREATE OR REPLACE FUNCTION hashset_hash(int4hashset)
+RETURNS integer
+AS 'hashset', 'int4hashset_hash'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR CLASS int4hashset_hash_ops
+DEFAULT FOR TYPE int4hashset USING hash AS
+OPERATOR 1 = (int4hashset, int4hashset),
+FUNCTION 1 hashset_hash(int4hashset);
+
+/*
+ * Hashset Btree Operators
+ */
+
+CREATE OR REPLACE FUNCTION hashset_lt(int4hashset, int4hashset)
+RETURNS bool
+AS 'hashset', 'int4hashset_lt'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_le(int4hashset, int4hashset)
+RETURNS boolean
+AS 'hashset', 'int4hashset_le'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_gt(int4hashset, int4hashset)
+RETURNS boolean
+AS 'hashset', 'int4hashset_gt'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_ge(int4hashset, int4hashset)
+RETURNS boolean
+AS 'hashset', 'int4hashset_ge'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_cmp(int4hashset, int4hashset)
+RETURNS integer
+AS 'hashset', 'int4hashset_cmp'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR < (
+    PROCEDURE = hashset_lt,
+    LEFTARG = int4hashset,
+    RIGHTARG = int4hashset,
+    COMMUTATOR = >,
+    NEGATOR = >=,
+    RESTRICT = scalarltsel,
+    JOIN = scalarltjoinsel
+);
+
+CREATE OPERATOR <= (
+    PROCEDURE = hashset_le,
+    LEFTARG = int4hashset,
+    RIGHTARG = int4hashset,
+    COMMUTATOR = '>=',
+    NEGATOR = '>',
+    RESTRICT = scalarltsel,
+    JOIN = scalarltjoinsel
+);
+
+CREATE OPERATOR > (
+    PROCEDURE = hashset_gt,
+    LEFTARG = int4hashset,
+    RIGHTARG = int4hashset,
+    COMMUTATOR = '<',
+    NEGATOR = '<=',
+    RESTRICT = scalargtsel,
+    JOIN = scalargtjoinsel
+);
+
+CREATE OPERATOR >= (
+    PROCEDURE = hashset_ge,
+    LEFTARG = int4hashset,
+    RIGHTARG = int4hashset,
+    COMMUTATOR = '<=',
+    NEGATOR = '<',
+    RESTRICT = scalargtsel,
+    JOIN = scalargtjoinsel
+);
+
+CREATE OPERATOR CLASS int4hashset_btree_ops
+DEFAULT FOR TYPE int4hashset USING btree AS
+OPERATOR 1 < (int4hashset, int4hashset),
+OPERATOR 2 <= (int4hashset, int4hashset),
+OPERATOR 3 = (int4hashset, int4hashset),
+OPERATOR 4 >= (int4hashset, int4hashset),
+OPERATOR 5 > (int4hashset, int4hashset),
+FUNCTION 1 hashset_cmp(int4hashset, int4hashset);
diff --git a/hashset-api.c b/hashset-api.c
new file mode 100644
index 0000000..3feb06d
--- /dev/null
+++ b/hashset-api.c
@@ -0,0 +1,1057 @@
+#include "hashset.h"
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <limits.h>
+
+#define PG_GETARG_INT4HASHSET(x)        (int4hashset_t *) PG_DETOAST_DATUM(PG_GETARG_DATUM(x))
+#define PG_GETARG_INT4HASHSET_COPY(x)   (int4hashset_t *) PG_DETOAST_DATUM_COPY(PG_GETARG_DATUM(x))
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(int4hashset_in);
+PG_FUNCTION_INFO_V1(int4hashset_out);
+PG_FUNCTION_INFO_V1(int4hashset_send);
+PG_FUNCTION_INFO_V1(int4hashset_recv);
+PG_FUNCTION_INFO_V1(int4hashset_add);
+PG_FUNCTION_INFO_V1(int4hashset_contains);
+PG_FUNCTION_INFO_V1(int4hashset_count);
+PG_FUNCTION_INFO_V1(int4hashset_merge);
+PG_FUNCTION_INFO_V1(int4hashset_init);
+PG_FUNCTION_INFO_V1(int4hashset_capacity);
+PG_FUNCTION_INFO_V1(int4hashset_collisions);
+PG_FUNCTION_INFO_V1(int4hashset_max_collisions);
+PG_FUNCTION_INFO_V1(int4hashset_agg_add);
+PG_FUNCTION_INFO_V1(int4hashset_agg_add_set);
+PG_FUNCTION_INFO_V1(int4hashset_agg_final);
+PG_FUNCTION_INFO_V1(int4hashset_agg_combine);
+PG_FUNCTION_INFO_V1(int4hashset_to_array);
+PG_FUNCTION_INFO_V1(int4hashset_equals);
+PG_FUNCTION_INFO_V1(int4hashset_neq);
+PG_FUNCTION_INFO_V1(int4hashset_hash);
+PG_FUNCTION_INFO_V1(int4hashset_lt);
+PG_FUNCTION_INFO_V1(int4hashset_le);
+PG_FUNCTION_INFO_V1(int4hashset_gt);
+PG_FUNCTION_INFO_V1(int4hashset_ge);
+PG_FUNCTION_INFO_V1(int4hashset_cmp);
+PG_FUNCTION_INFO_V1(int4hashset_intersection);
+PG_FUNCTION_INFO_V1(int4hashset_difference);
+PG_FUNCTION_INFO_V1(int4hashset_symmetric_difference);
+
+Datum int4hashset_in(PG_FUNCTION_ARGS);
+Datum int4hashset_out(PG_FUNCTION_ARGS);
+Datum int4hashset_send(PG_FUNCTION_ARGS);
+Datum int4hashset_recv(PG_FUNCTION_ARGS);
+Datum int4hashset_add(PG_FUNCTION_ARGS);
+Datum int4hashset_contains(PG_FUNCTION_ARGS);
+Datum int4hashset_count(PG_FUNCTION_ARGS);
+Datum int4hashset_merge(PG_FUNCTION_ARGS);
+Datum int4hashset_init(PG_FUNCTION_ARGS);
+Datum int4hashset_capacity(PG_FUNCTION_ARGS);
+Datum int4hashset_collisions(PG_FUNCTION_ARGS);
+Datum int4hashset_max_collisions(PG_FUNCTION_ARGS);
+Datum int4hashset_agg_add(PG_FUNCTION_ARGS);
+Datum int4hashset_agg_add_set(PG_FUNCTION_ARGS);
+Datum int4hashset_agg_final(PG_FUNCTION_ARGS);
+Datum int4hashset_agg_combine(PG_FUNCTION_ARGS);
+Datum int4hashset_to_array(PG_FUNCTION_ARGS);
+Datum int4hashset_equals(PG_FUNCTION_ARGS);
+Datum int4hashset_neq(PG_FUNCTION_ARGS);
+Datum int4hashset_hash(PG_FUNCTION_ARGS);
+Datum int4hashset_lt(PG_FUNCTION_ARGS);
+Datum int4hashset_le(PG_FUNCTION_ARGS);
+Datum int4hashset_gt(PG_FUNCTION_ARGS);
+Datum int4hashset_ge(PG_FUNCTION_ARGS);
+Datum int4hashset_cmp(PG_FUNCTION_ARGS);
+Datum int4hashset_intersection(PG_FUNCTION_ARGS);
+Datum int4hashset_difference(PG_FUNCTION_ARGS);
+Datum int4hashset_symmetric_difference(PG_FUNCTION_ARGS);
+
+Datum
+int4hashset_in(PG_FUNCTION_ARGS)
+{
+	char *str = PG_GETARG_CSTRING(0);
+	char *endptr;
+	int32 len = strlen(str);
+	int4hashset_t *set;
+	int64 value;
+
+	/* Skip initial spaces */
+	while (hashset_isspace(*str)) str++;
+
+	/* Check the opening brace */
+	if (*str != '{')
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input syntax for hashset: \"%s\"", str),
+				errdetail("Hashset representation must start with \"{\".")));
+	}
+
+	/* Start parsing from the first number (after the opening brace) */
+	str++;
+
+	/* Initial size based on input length (arbitrary, could be optimized) */
+	set = int4hashset_allocate(
+		len/2,
+		DEFAULT_LOAD_FACTOR,
+		DEFAULT_GROWTH_FACTOR,
+		DEFAULT_HASHFN_ID
+	);
+
+	while (true)
+	{
+		/* Skip spaces before number */
+		while (hashset_isspace(*str)) str++;
+
+		/* Check for closing brace, handling the case for an empty set */
+		if (*str == '}')
+		{
+			str++; /* Move past the closing brace */
+			break;
+		}
+
+		/* Parse the number */
+		value = strtol(str, &endptr, 10);
+
+		if (errno == ERANGE || value < PG_INT32_MIN || value > PG_INT32_MAX)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					errmsg("value \"%s\" is out of range for type %s", str,
+							"integer")));
+		}
+
+		/* Add the value to the hashset, resize if needed */
+		if (set->nelements >= set->capacity)
+		{
+			set = int4hashset_resize(set);
+		}
+		set = int4hashset_add_element(set, (int32)value);
+
+		/* Error handling for strtol */
+		if (endptr == str)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("invalid input syntax for integer: \"%s\"", str)));
+		}
+
+		str = endptr; /* Move to next potential number or closing brace */
+
+        /* Skip spaces before the next number or closing brace */
+		while (hashset_isspace(*str)) str++;
+
+		if (*str == ',')
+		{
+			str++; /* Skip comma before next loop iteration */
+		}
+		else if (*str != '}')
+		{
+			/* Unexpected character */
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("unexpected character \"%c\" in hashset input", *str)));
+		}
+	}
+
+	/* Only whitespace is allowed after the closing brace */
+	while (*str)
+	{
+		if (!hashset_isspace(*str))
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					errmsg("malformed hashset literal: \"%s\"", str),
+					errdetail("Junk after closing right brace.")));
+		}
+		str++;
+	}
+
+	PG_RETURN_POINTER(set);
+}
+
+Datum
+int4hashset_out(PG_FUNCTION_ARGS)
+{
+	int4hashset_t *set = PG_GETARG_INT4HASHSET(0);
+	char *bitmap;
+	int32 *values;
+	int i;
+	StringInfoData str;
+
+	/* Calculate the pointer to the bitmap and values array */
+	bitmap = set->data;
+	values = (int32 *) (set->data + CEIL_DIV(set->capacity, 8));
+
+	/* Initialize the StringInfo buffer */
+	initStringInfo(&str);
+
+	/* Append the opening brace for the output hashset string */
+	appendStringInfoChar(&str, '{');
+
+	/* Loop through the elements and append them to the string */
+	for (i = 0; i < set->capacity; i++)
+	{
+		int byte = i / 8;
+		int bit = i % 8;
+
+		/* Check if the bit in the bitmap is set */
+		if (bitmap[byte] & (0x01 << bit))
+		{
+			/* Append the value */
+			if (str.len > 1)
+				appendStringInfoChar(&str, ',');
+			appendStringInfo(&str, "%d", values[i]);
+		}
+	}
+
+	/* Append the closing brace for the output hashset string */
+	appendStringInfoChar(&str, '}');
+
+	/* Return the resulting string */
+	PG_RETURN_CSTRING(str.data);
+}
+
+Datum
+int4hashset_send(PG_FUNCTION_ARGS)
+{
+	int4hashset_t  *set = PG_GETARG_INT4HASHSET(0);
+	StringInfoData	buf;
+	int32			data_size;
+	int				version = 1;
+
+	/* Begin constructing the message */
+	pq_begintypsend(&buf);
+
+	/* Send the version number */
+	pq_sendint8(&buf, version);
+
+	/* Send the non-data fields */
+	pq_sendint32(&buf, set->flags);
+	pq_sendint32(&buf, set->capacity);
+	pq_sendint32(&buf, set->nelements);
+	pq_sendint32(&buf, set->hashfn_id);
+	pq_sendfloat4(&buf, set->load_factor);
+	pq_sendfloat4(&buf, set->growth_factor);
+	pq_sendint32(&buf, set->ncollisions);
+	pq_sendint32(&buf, set->max_collisions);
+	pq_sendint32(&buf, set->hash);
+
+	/* Compute and send the size of the data field */
+	data_size = VARSIZE(set) - offsetof(int4hashset_t, data);
+	pq_sendbytes(&buf, set->data, data_size);
+
+	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
+}
+
+Datum
+int4hashset_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo		buf = (StringInfo) PG_GETARG_POINTER(0);
+	int4hashset_t  *set;
+	int32			data_size;
+	Size			total_size;
+	const char	   *binary_data;
+	int				version;
+	int32			flags;
+	int32			capacity;
+	int32			nelements;
+	int32			hashfn_id;
+	float4			load_factor;
+	float4			growth_factor;
+	int32			ncollisions;
+	int32			max_collisions;
+	int32			hash;
+
+	version = pq_getmsgint(buf, 1);
+	if (version != 1)
+		elog(ERROR, "unsupported hashset version number %d", version);
+
+	/* Read fields from buffer */
+	flags = pq_getmsgint(buf, 4);
+	capacity = pq_getmsgint(buf, 4);
+	nelements = pq_getmsgint(buf, 4);
+	hashfn_id = pq_getmsgint(buf, 4);
+	load_factor = pq_getmsgfloat4(buf);
+	growth_factor = pq_getmsgfloat4(buf);
+	ncollisions = pq_getmsgint(buf, 4);
+	max_collisions = pq_getmsgint(buf, 4);
+	hash = pq_getmsgint(buf, 4);
+
+	/* Compute the size of the data field */
+	data_size = buf->len - buf->cursor;
+
+	/* Read the binary data */
+	binary_data = pq_getmsgbytes(buf, data_size);
+
+	/* Make sure that there is no extra data left in the message */
+	pq_getmsgend(buf);
+
+	/* Compute total size of hashset_t */
+	total_size = offsetof(int4hashset_t, data) + data_size;
+
+	/* Allocate memory for hashset including the data field */
+	set = (int4hashset_t *) palloc0(total_size);
+
+	/* Set the size of the variable-length data structure */
+	SET_VARSIZE(set, total_size);
+
+	/* Populate the structure */
+	set->flags = flags;
+	set->capacity = capacity;
+	set->nelements = nelements;
+	set->hashfn_id = hashfn_id;
+	set->load_factor = load_factor;
+	set->growth_factor = growth_factor;
+	set->ncollisions = ncollisions;
+	set->max_collisions = max_collisions;
+	set->hash = hash;
+	memcpy(set->data, binary_data, data_size);
+
+	PG_RETURN_POINTER(set);
+}
+
+Datum
+int4hashset_add(PG_FUNCTION_ARGS)
+{
+	int4hashset_t *set;
+
+	if (PG_ARGISNULL(1))
+	{
+		if (PG_ARGISNULL(0))
+			PG_RETURN_NULL();
+
+		PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+	}
+
+	/* if there's no hashset allocated, create it now */
+	if (PG_ARGISNULL(0))
+	{
+		set = int4hashset_allocate(
+			DEFAULT_INITIAL_CAPACITY,
+			DEFAULT_LOAD_FACTOR,
+			DEFAULT_GROWTH_FACTOR,
+			DEFAULT_HASHFN_ID
+		);
+	}
+	else
+	{
+		/* make sure we are working with a non-toasted and non-shared copy of the input */
+		set = PG_GETARG_INT4HASHSET_COPY(0);
+	}
+
+	set = int4hashset_add_element(set, PG_GETARG_INT32(1));
+
+	PG_RETURN_POINTER(set);
+}
+
+Datum
+int4hashset_contains(PG_FUNCTION_ARGS)
+{
+	int4hashset_t  *set;
+	int32			value;
+
+	if (PG_ARGISNULL(1) || PG_ARGISNULL(0))
+		PG_RETURN_BOOL(false);
+
+	set = PG_GETARG_INT4HASHSET(0);
+	value = PG_GETARG_INT32(1);
+
+	PG_RETURN_BOOL(int4hashset_contains_element(set, value));
+}
+
+Datum
+int4hashset_count(PG_FUNCTION_ARGS)
+{
+	int4hashset_t	*set;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	set = PG_GETARG_INT4HASHSET(0);
+
+	PG_RETURN_INT64(set->nelements);
+}
+
+Datum
+int4hashset_merge(PG_FUNCTION_ARGS)
+{
+	int				i;
+
+	int4hashset_t  *seta;
+	int4hashset_t  *setb;
+
+	char		   *bitmap;
+	int32_t		   *values;
+
+	if (PG_ARGISNULL(0) && PG_ARGISNULL(1))
+		PG_RETURN_NULL();
+	else if (PG_ARGISNULL(1))
+		PG_RETURN_POINTER(PG_GETARG_INT4HASHSET(0));
+	else if (PG_ARGISNULL(0))
+		PG_RETURN_POINTER(PG_GETARG_INT4HASHSET(1));
+
+	seta = PG_GETARG_INT4HASHSET_COPY(0);
+	setb = PG_GETARG_INT4HASHSET(1);
+
+	bitmap = setb->data;
+	values = (int32 *) (setb->data + CEIL_DIV(setb->capacity, 8));
+
+	for (i = 0; i < setb->capacity; i++)
+	{
+		int	byte = (i / 8);
+		int	bit = (i % 8);
+
+		if (bitmap[byte] & (0x01 << bit))
+			seta = int4hashset_add_element(seta, values[i]);
+	}
+
+	PG_RETURN_POINTER(seta);
+}
+
+Datum
+int4hashset_init(PG_FUNCTION_ARGS)
+{
+	int4hashset_t *set;
+	int32 initial_capacity = PG_GETARG_INT32(0);
+	float4 load_factor = PG_GETARG_FLOAT4(1);
+	float4 growth_factor = PG_GETARG_FLOAT4(2);
+	int32 hashfn_id = PG_GETARG_INT32(3);
+
+	/* Validate input arguments */
+	if (!(initial_capacity >= 0))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("initial capacity cannot be negative")));
+	}
+
+	if (!(load_factor > 0.0 && load_factor < 1.0))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("load factor must be between 0.0 and 1.0")));
+	}
+
+	if (!(growth_factor > 1.0))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("growth factor must be greater than 1.0")));
+	}
+
+	if (!(hashfn_id == JENKINS_LOOKUP3_HASHFN_ID ||
+	      hashfn_id == MURMURHASH32_HASHFN_ID ||
+		  hashfn_id == NAIVE_HASHFN_ID))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Invalid hash function ID")));
+	}
+
+	set = int4hashset_allocate(
+		initial_capacity,
+		load_factor,
+		growth_factor,
+		hashfn_id
+	);
+
+	PG_RETURN_POINTER(set);
+}
+
+Datum
+int4hashset_capacity(PG_FUNCTION_ARGS)
+{
+	int4hashset_t	*set;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	set = PG_GETARG_INT4HASHSET(0);
+
+	PG_RETURN_INT64(set->capacity);
+}
+
+Datum
+int4hashset_collisions(PG_FUNCTION_ARGS)
+{
+	int4hashset_t	*set;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	set = PG_GETARG_INT4HASHSET(0);
+
+	PG_RETURN_INT64(set->ncollisions);
+}
+
+Datum
+int4hashset_max_collisions(PG_FUNCTION_ARGS)
+{
+	int4hashset_t	*set;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	set = PG_GETARG_INT4HASHSET(0);
+
+	PG_RETURN_INT64(set->max_collisions);
+}
+
+Datum
+int4hashset_agg_add(PG_FUNCTION_ARGS)
+{
+	MemoryContext	oldcontext;
+	int4hashset_t  *state;
+
+	MemoryContext	aggcontext;
+
+	/* cannot be called directly because of internal-type argument */
+	if (!AggCheckCallContext(fcinfo, &aggcontext))
+		elog(ERROR, "hashset_add_add called in non-aggregate context");
+
+	/*
+	 * We want to skip NULL values altogether - we return either the existing
+	 * hashset (if it already exists) or NULL.
+	 */
+	if (PG_ARGISNULL(1))
+	{
+		if (PG_ARGISNULL(0))
+			PG_RETURN_NULL();
+
+		/* if there already is a state accumulated, don't forget it */
+		PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+	}
+
+	/* if there's no hashset allocated, create it now */
+	if (PG_ARGISNULL(0))
+	{
+		oldcontext = MemoryContextSwitchTo(aggcontext);
+		state = int4hashset_allocate(
+			DEFAULT_INITIAL_CAPACITY,
+			DEFAULT_LOAD_FACTOR,
+			DEFAULT_GROWTH_FACTOR,
+			DEFAULT_HASHFN_ID
+		);
+		MemoryContextSwitchTo(oldcontext);
+	}
+	else
+		state = (int4hashset_t *) PG_GETARG_POINTER(0);
+
+	oldcontext = MemoryContextSwitchTo(aggcontext);
+	state = int4hashset_add_element(state, PG_GETARG_INT32(1));
+	MemoryContextSwitchTo(oldcontext);
+
+	PG_RETURN_POINTER(state);
+}
+
+Datum
+int4hashset_agg_add_set(PG_FUNCTION_ARGS)
+{
+	MemoryContext	oldcontext;
+	int4hashset_t  *state;
+
+	MemoryContext   aggcontext;
+
+	/* cannot be called directly because of internal-type argument */
+	if (!AggCheckCallContext(fcinfo, &aggcontext))
+		elog(ERROR, "hashset_add_add called in non-aggregate context");
+
+	/*
+	 * We want to skip NULL values altogether - we return either the existing
+	 * hashset (if it already exists) or NULL.
+	 */
+	if (PG_ARGISNULL(1))
+	{
+		if (PG_ARGISNULL(0))
+			PG_RETURN_NULL();
+
+		/* if there already is a state accumulated, don't forget it */
+		PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+	}
+
+	/* if there's no hashset allocated, create it now */
+	if (PG_ARGISNULL(0))
+	{
+		oldcontext = MemoryContextSwitchTo(aggcontext);
+		state = int4hashset_allocate(
+			DEFAULT_INITIAL_CAPACITY,
+			DEFAULT_LOAD_FACTOR,
+			DEFAULT_GROWTH_FACTOR,
+			DEFAULT_HASHFN_ID
+		);
+		MemoryContextSwitchTo(oldcontext);
+	}
+	else
+		state = (int4hashset_t *) PG_GETARG_POINTER(0);
+
+	oldcontext = MemoryContextSwitchTo(aggcontext);
+
+	{
+		int				i;
+		char		   *bitmap;
+		int32		   *values;
+		int4hashset_t  *value;
+
+		value = PG_GETARG_INT4HASHSET(1);
+
+		bitmap = value->data;
+		values = (int32 *) (value->data + CEIL_DIV(value->capacity, 8));
+
+		for (i = 0; i < value->capacity; i++)
+		{
+			int	byte = (i / 8);
+			int	bit = (i % 8);
+
+			if (bitmap[byte] & (0x01 << bit))
+				state = int4hashset_add_element(state, values[i]);
+		}
+	}
+
+	MemoryContextSwitchTo(oldcontext);
+
+	PG_RETURN_POINTER(state);
+}
+
+Datum
+int4hashset_agg_final(PG_FUNCTION_ARGS)
+{
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	PG_RETURN_POINTER(PG_GETARG_POINTER(0));
+}
+
+Datum
+int4hashset_agg_combine(PG_FUNCTION_ARGS)
+{
+	int				i;
+	int4hashset_t  *src;
+	int4hashset_t  *dst;
+	MemoryContext	aggcontext;
+	MemoryContext	oldcontext;
+
+	char		   *bitmap;
+	int32		   *values;
+
+	if (!AggCheckCallContext(fcinfo, &aggcontext))
+		elog(ERROR, "hashset_agg_combine called in non-aggregate context");
+
+	/* if no "merged" state yet, try creating it */
+	if (PG_ARGISNULL(0))
+	{
+		/* nope, the second argument is NULL to, so return NULL */
+		if (PG_ARGISNULL(1))
+			PG_RETURN_NULL();
+
+		/* the second argument is not NULL, so copy it */
+		src = (int4hashset_t *) PG_GETARG_POINTER(1);
+
+		/* copy the hashset into the right long-lived memory context */
+		oldcontext = MemoryContextSwitchTo(aggcontext);
+		src = int4hashset_copy(src);
+		MemoryContextSwitchTo(oldcontext);
+
+		PG_RETURN_POINTER(src);
+	}
+
+	/*
+	 * If the second argument is NULL, just return the first one (we know
+	 * it's not NULL at this point).
+	 */
+	if (PG_ARGISNULL(1))
+		PG_RETURN_DATUM(PG_GETARG_DATUM(0));
+
+	/* Now we know neither argument is NULL, so merge them. */
+	src = (int4hashset_t *) PG_GETARG_POINTER(1);
+	dst = (int4hashset_t *) PG_GETARG_POINTER(0);
+
+	bitmap = src->data;
+	values = (int32 *) (src->data + CEIL_DIV(src->capacity, 8));
+
+	for (i = 0; i < src->capacity; i++)
+	{
+		int	byte = (i / 8);
+		int	bit = (i % 8);
+
+		if (bitmap[byte] & (0x01 << bit))
+			dst = int4hashset_add_element(dst, values[i]);
+	}
+
+
+	PG_RETURN_POINTER(dst);
+}
+
+Datum
+int4hashset_to_array(PG_FUNCTION_ARGS)
+{
+	int					i,
+						idx;
+	int4hashset_t	   *set;
+	int32			   *values;
+	int					nvalues;
+
+	char			   *sbitmap;
+	int32			   *svalues;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	set = PG_GETARG_INT4HASHSET(0);
+
+	sbitmap = set->data;
+	svalues = (int32 *) (set->data + CEIL_DIV(set->capacity, 8));
+
+	/* number of values to store in the array */
+	nvalues = set->nelements;
+	values = (int32 *) palloc(sizeof(int32) * nvalues);
+
+	idx = 0;
+	for (i = 0; i < set->capacity; i++)
+	{
+		int	byte = (i / 8);
+		int	bit = (i % 8);
+
+		if (sbitmap[byte] & (0x01 << bit))
+			values[idx++] = svalues[i];
+	}
+
+	Assert(idx == nvalues);
+
+	return int32_to_array(fcinfo, values, nvalues);
+}
+
+Datum
+int4hashset_equals(PG_FUNCTION_ARGS)
+{
+	int4hashset_t *a = PG_GETARG_INT4HASHSET(0);
+	int4hashset_t *b = PG_GETARG_INT4HASHSET(1);
+
+	char *bitmap_a;
+	int32 *values_a;
+	int i;
+
+	/*
+	 * Check if the number of elements is the same
+	 */
+	if (a->nelements != b->nelements)
+		PG_RETURN_BOOL(false);
+
+	bitmap_a = a->data;
+	values_a = (int32 *)(a->data + CEIL_DIV(a->capacity, 8));
+
+	/*
+	 * Check if every element in a is also in b
+	 */
+	for (i = 0; i < a->capacity; i++)
+	{
+		int byte = (i / 8);
+		int bit = (i % 8);
+
+		if (bitmap_a[byte] & (0x01 << bit))
+		{
+			int32 value = values_a[i];
+
+			if (!int4hashset_contains_element(b, value))
+				PG_RETURN_BOOL(false);
+		}
+	}
+
+	/*
+	 * All elements in a are in b and the number of elements is the same,
+	 * so the sets must be equal.
+	 */
+	PG_RETURN_BOOL(true);
+}
+
+
+Datum
+int4hashset_neq(PG_FUNCTION_ARGS)
+{
+    int4hashset_t *a = PG_GETARG_INT4HASHSET(0);
+    int4hashset_t *b = PG_GETARG_INT4HASHSET(1);
+
+    /* If a is not equal to b, then they are not equal */
+    if (!DatumGetBool(DirectFunctionCall2(int4hashset_equals, PointerGetDatum(a), PointerGetDatum(b))))
+        PG_RETURN_BOOL(true);
+
+    PG_RETURN_BOOL(false);
+}
+
+
+Datum int4hashset_hash(PG_FUNCTION_ARGS)
+{
+    int4hashset_t *set = PG_GETARG_INT4HASHSET(0);
+
+    PG_RETURN_INT32(set->hash);
+}
+
+
+Datum
+int4hashset_lt(PG_FUNCTION_ARGS)
+{
+    int4hashset_t *a = PG_GETARG_INT4HASHSET(0);
+    int4hashset_t *b = PG_GETARG_INT4HASHSET(1);
+    int32 cmp;
+
+    cmp = DatumGetInt32(DirectFunctionCall2(int4hashset_cmp,
+                                            PointerGetDatum(a),
+                                            PointerGetDatum(b)));
+
+    PG_RETURN_BOOL(cmp < 0);
+}
+
+
+Datum
+int4hashset_le(PG_FUNCTION_ARGS)
+{
+	int4hashset_t *a = PG_GETARG_INT4HASHSET(0);
+	int4hashset_t *b = PG_GETARG_INT4HASHSET(1);
+	int32 cmp;
+
+	cmp = DatumGetInt32(DirectFunctionCall2(int4hashset_cmp,
+											PointerGetDatum(a),
+											PointerGetDatum(b)));
+
+	PG_RETURN_BOOL(cmp <= 0);
+}
+
+
+Datum
+int4hashset_gt(PG_FUNCTION_ARGS)
+{
+	int4hashset_t *a = PG_GETARG_INT4HASHSET(0);
+	int4hashset_t *b = PG_GETARG_INT4HASHSET(1);
+	int32 cmp;
+
+	cmp = DatumGetInt32(DirectFunctionCall2(int4hashset_cmp,
+											PointerGetDatum(a),
+											PointerGetDatum(b)));
+
+	PG_RETURN_BOOL(cmp > 0);
+}
+
+
+Datum
+int4hashset_ge(PG_FUNCTION_ARGS)
+{
+	int4hashset_t *a = PG_GETARG_INT4HASHSET(0);
+	int4hashset_t *b = PG_GETARG_INT4HASHSET(1);
+	int32 cmp;
+
+	cmp = DatumGetInt32(DirectFunctionCall2(int4hashset_cmp,
+											PointerGetDatum(a),
+											PointerGetDatum(b)));
+
+	PG_RETURN_BOOL(cmp >= 0);
+}
+
+Datum
+int4hashset_cmp(PG_FUNCTION_ARGS)
+{
+	int4hashset_t *a = PG_GETARG_INT4HASHSET(0);
+	int4hashset_t *b = PG_GETARG_INT4HASHSET(1);
+	int32		  *elements_a;
+	int32		  *elements_b;
+
+	/*
+	 * Compare the hashes first, if they are different,
+	 * we can immediately tell which set is 'greater'
+	 */
+	if (a->hash < b->hash)
+		PG_RETURN_INT32(-1);
+	else if (a->hash > b->hash)
+		PG_RETURN_INT32(1);
+
+	/*
+	 * If hashes are equal, perform a more rigorous comparison
+	 */
+
+	/*
+	 * If number of elements are different,
+	 * we can use that to deterministically return -1 or 1
+	 */
+	if (a->nelements < b->nelements)
+		PG_RETURN_INT32(-1);
+	else if (a->nelements > b->nelements)
+		PG_RETURN_INT32(1);
+
+	/* Assert that the number of elements in both hashsets are equal */
+	Assert(a->nelements == b->nelements);
+
+	/* Extract and sort elements from each set */
+	elements_a = int4hashset_extract_sorted_elements(a);
+	elements_b = int4hashset_extract_sorted_elements(b);
+
+	/* Now we can perform a lexicographical comparison */
+	for (int32 i = 0; i < a->nelements; i++)
+	{
+		if (elements_a[i] < elements_b[i])
+		{
+			pfree(elements_a);
+			pfree(elements_b);
+			PG_RETURN_INT32(-1);
+		}
+		else if (elements_a[i] > elements_b[i])
+		{
+			pfree(elements_a);
+			pfree(elements_b);
+			PG_RETURN_INT32(1);
+		}
+	}
+
+	/* All elements are equal, so the sets are equal */
+	pfree(elements_a);
+	pfree(elements_b);
+	PG_RETURN_INT32(0);
+}
+
+Datum
+int4hashset_intersection(PG_FUNCTION_ARGS)
+{
+	int				i;
+	int4hashset_t  *seta;
+	int4hashset_t  *setb;
+	int4hashset_t  *intersection;
+	char		   *bitmap;
+	int32_t		   *values;
+
+	if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		PG_RETURN_NULL();
+
+	seta = PG_GETARG_INT4HASHSET(0);
+	setb = PG_GETARG_INT4HASHSET(1);
+
+	intersection = int4hashset_allocate(
+		seta->capacity,
+		DEFAULT_LOAD_FACTOR,
+		DEFAULT_GROWTH_FACTOR,
+		DEFAULT_HASHFN_ID
+	);
+
+	bitmap = setb->data;
+	values = (int32_t *)(setb->data + CEIL_DIV(setb->capacity, 8));
+
+	for (i = 0; i < setb->capacity; i++)
+	{
+		int byte = (i / 8);
+		int bit = (i % 8);
+
+		if ((bitmap[byte] & (0x01 << bit)) &&
+			int4hashset_contains_element(seta, values[i]))
+		{
+			intersection = int4hashset_add_element(intersection, values[i]);
+		}
+	}
+
+	PG_RETURN_POINTER(intersection);
+}
+
+Datum
+int4hashset_difference(PG_FUNCTION_ARGS)
+{
+	int				i;
+	int4hashset_t	*seta;
+	int4hashset_t	*setb;
+	int4hashset_t	*difference;
+	char			*bitmap;
+	int32_t			*values;
+
+	if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		PG_RETURN_NULL();
+
+	seta = PG_GETARG_INT4HASHSET(0);
+	setb = PG_GETARG_INT4HASHSET(1);
+
+	difference = int4hashset_allocate(
+		seta->capacity,
+		DEFAULT_LOAD_FACTOR,
+		DEFAULT_GROWTH_FACTOR,
+		DEFAULT_HASHFN_ID
+	);
+
+	bitmap = seta->data;
+	values = (int32_t *)(seta->data + CEIL_DIV(seta->capacity, 8));
+
+	for (i = 0; i < seta->capacity; i++)
+	{
+		int byte = (i / 8);
+		int bit = (i % 8);
+
+		if ((bitmap[byte] & (0x01 << bit)) &&
+			!int4hashset_contains_element(setb, values[i]))
+		{
+			difference = int4hashset_add_element(difference, values[i]);
+		}
+	}
+
+	PG_RETURN_POINTER(difference);
+}
+
+Datum
+int4hashset_symmetric_difference(PG_FUNCTION_ARGS)
+{
+	int				i;
+	int4hashset_t  *seta;
+	int4hashset_t  *setb;
+	int4hashset_t  *result;
+	char		   *bitmapa;
+	char		   *bitmapb;
+	int32_t		   *valuesa;
+	int32_t		   *valuesb;
+
+	if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+				 errmsg("hashset arguments cannot be null")));
+
+	seta = PG_GETARG_INT4HASHSET(0);
+	setb = PG_GETARG_INT4HASHSET(1);
+
+	bitmapa = seta->data;
+	valuesa = (int32 *) (seta->data + CEIL_DIV(seta->capacity, 8));
+
+	bitmapb = setb->data;
+	valuesb = (int32 *) (setb->data + CEIL_DIV(setb->capacity, 8));
+
+	result = int4hashset_allocate(
+		seta->nelements + setb->nelements,
+		DEFAULT_LOAD_FACTOR,
+		DEFAULT_GROWTH_FACTOR,
+		DEFAULT_HASHFN_ID
+	);
+
+	/* Add elements that are in seta but not in setb */
+	for (i = 0; i < seta->capacity; i++)
+	{
+		int byte = i / 8;
+		int bit = i % 8;
+
+		if (bitmapa[byte] & (0x01 << bit))
+		{
+			int32 value = valuesa[i];
+			if (!int4hashset_contains_element(setb, value))
+				result = int4hashset_add_element(result, value);
+		}
+	}
+
+	/* Add elements that are in setb but not in seta */
+	for (i = 0; i < setb->capacity; i++)
+	{
+		int byte = i / 8;
+		int bit = i % 8;
+
+		if (bitmapb[byte] & (0x01 << bit))
+		{
+			int32 value = valuesb[i];
+			if (!int4hashset_contains_element(seta, value))
+				result = int4hashset_add_element(result, value);
+		}
+	}
+
+	PG_RETURN_POINTER(result);
+}
diff --git a/hashset.c b/hashset.c
new file mode 100644
index 0000000..67cbdf3
--- /dev/null
+++ b/hashset.c
@@ -0,0 +1,329 @@
+/*
+ * hashset.c
+ *
+ * Copyright (C) Tomas Vondra, 2019
+ */
+
+#include "hashset.h"
+
+static int int32_cmp(const void *a, const void *b);
+
+int4hashset_t *
+int4hashset_allocate(
+	int capacity,
+	float4 load_factor,
+	float4 growth_factor,
+	int hashfn_id
+)
+{
+	Size			len;
+	int4hashset_t  *set;
+	char		   *ptr;
+
+	/*
+	 * Ensure that capacity is not divisible by HASHSET_STEP;
+	 * i.e. the step size used in hashset_add_element()
+	 * and hashset_contains_element().
+	 */
+	while (capacity % HASHSET_STEP == 0)
+		capacity++;
+
+	len = offsetof(int4hashset_t, data);
+	len += CEIL_DIV(capacity, 8);
+	len += capacity * sizeof(int32);
+
+	ptr = palloc0(len);
+	SET_VARSIZE(ptr, len);
+
+	set = (int4hashset_t *) ptr;
+
+	set->flags = 0;
+	set->capacity = capacity;
+	set->nelements = 0;
+	set->hashfn_id = hashfn_id;
+	set->load_factor = load_factor;
+	set->growth_factor = growth_factor;
+	set->ncollisions = 0;
+	set->max_collisions = 0;
+	set->hash = 0; /* Initial hash value */
+
+	set->flags |= 0;
+
+	return set;
+}
+
+int4hashset_t *
+int4hashset_resize(int4hashset_t * set)
+{
+	int				i;
+	int4hashset_t  *new;
+	char		   *bitmap;
+	int32		   *values;
+	int				new_capacity;
+
+	new_capacity = (int)(set->capacity * set->growth_factor);
+
+	/*
+	 * If growth factor is too small, new capacity might remain the same as
+	 * the old capacity. This can lead to an infinite loop in resizing.
+	 * To prevent this, we manually increment the capacity by 1 if new capacity
+	 * equals the old capacity.
+	 */
+	if (new_capacity == set->capacity)
+		new_capacity = set->capacity + 1;
+
+	new = int4hashset_allocate(
+		new_capacity,
+		set->load_factor,
+		set->growth_factor,
+		set->hashfn_id
+	);
+
+	/* Calculate the pointer to the bitmap and values array */
+	bitmap = set->data;
+	values = (int32 *) (set->data + CEIL_DIV(set->capacity, 8));
+
+	for (i = 0; i < set->capacity; i++)
+	{
+		int	byte = (i / 8);
+		int	bit = (i % 8);
+
+		if (bitmap[byte] & (0x01 << bit))
+			int4hashset_add_element(new, values[i]);
+	}
+
+	return new;
+}
+
+int4hashset_t *
+int4hashset_add_element(int4hashset_t *set, int32 value)
+{
+	int		byte;
+	int		bit;
+	uint32	hash;
+	uint32	position;
+	char   *bitmap;
+	int32  *values;
+	int32	current_collisions = 0;
+
+	if (set->nelements > set->capacity * set->load_factor)
+		set = int4hashset_resize(set);
+
+	if (set->hashfn_id == JENKINS_LOOKUP3_HASHFN_ID)
+	{
+		hash = hash_bytes_uint32((uint32) value);
+	}
+	else if (set->hashfn_id == MURMURHASH32_HASHFN_ID)
+	{
+		hash = murmurhash32((uint32) value);
+	}
+	else if (set->hashfn_id == NAIVE_HASHFN_ID)
+	{
+		hash = ((uint32) value * 7691 + 4201);
+	}
+	else
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid hash function ID: \"%d\"", set->hashfn_id)));
+	}
+
+    position = hash % set->capacity;
+
+	bitmap = set->data;
+	values = (int32 *) (set->data + CEIL_DIV(set->capacity, 8));
+
+	while (true)
+	{
+		byte = (position / 8);
+		bit = (position % 8);
+
+		/* The item is already used - maybe it's the same value? */
+		if (bitmap[byte] & (0x01 << bit))
+		{
+			/* Same value, we're done */
+			if (values[position] == value)
+				break;
+
+			/* Increment the collision counter */
+			set->ncollisions++;
+			current_collisions++;
+
+			if (current_collisions > set->max_collisions)
+				set->max_collisions = current_collisions;
+
+			position = (position + HASHSET_STEP) % set->capacity;
+			continue;
+		}
+
+		/* Found an empty spot, before hitting the value first */
+		bitmap[byte] |= (0x01 << bit);
+		values[position] = value;
+
+		set->hash ^= hash;
+
+		set->nelements++;
+
+		break;
+	}
+
+	return set;
+}
+
+bool
+int4hashset_contains_element(int4hashset_t *set, int32 value)
+{
+	int     byte;
+	int     bit;
+	uint32  hash;
+	uint32	position;
+	char   *bitmap;
+	int32  *values;
+	int     num_probes = 0; /* Counter for the number of probes */
+
+	if (set->hashfn_id == JENKINS_LOOKUP3_HASHFN_ID)
+	{
+		hash = hash_bytes_uint32((uint32) value);
+	}
+	else if (set->hashfn_id == MURMURHASH32_HASHFN_ID)
+	{
+		hash = murmurhash32((uint32) value);
+	}
+	else if (set->hashfn_id == NAIVE_HASHFN_ID)
+	{
+		hash = ((uint32) value * NAIVE_HASHFN_MULTIPLIER + NAIVE_HASHFN_INCREMENT);
+	}
+	else
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid hash function ID: \"%d\"", set->hashfn_id)));
+	}
+
+	position = hash % set->capacity;
+
+	bitmap = set->data;
+	values = (int32 *) (set->data + CEIL_DIV(set->capacity, 8));
+
+	while (true)
+	{
+		byte = (position / 8);
+		bit = (position % 8);
+
+		/* Found an empty slot, value is not there */
+		if ((bitmap[byte] & (0x01 << bit)) == 0)
+			return false;
+
+		/* Is it the same value? */
+		if (values[position] == value)
+			return true;
+
+		/* Move to the next element */
+		position = (position + HASHSET_STEP) % set->capacity;
+
+		num_probes++; /* Increment the number of probes */
+
+		/* Check if we have probed all slots */
+		if (num_probes >= set->capacity)
+			return false; /* Avoid infinite loop */
+	}
+}
+
+int32 *
+int4hashset_extract_sorted_elements(int4hashset_t *set)
+{
+	/* Allocate memory for the elements array */
+	int32 *elements = palloc(set->nelements * sizeof(int32));
+
+	/* Access the data array */
+	char *bitmap = set->data;
+	int32 *values = (int32 *)(set->data + CEIL_DIV(set->capacity, 8));
+
+	/* Counter for the number of extracted elements */
+	int32 nextracted = 0;
+
+	/* Iterate through all elements */
+	for (int32 i = 0; i < set->capacity; i++)
+	{
+		int byte = i / 8;
+		int bit = i % 8;
+
+		/* Check if the current position is occupied */
+		if (bitmap[byte] & (0x01 << bit))
+		{
+			/* Add the value to the elements array */
+			elements[nextracted++] = values[i];
+		}
+	}
+
+	/* Make sure we extracted the correct number of elements */
+	Assert(nextracted == set->nelements);
+
+	/* Sort the elements array */
+	qsort(elements, nextracted, sizeof(int32), int32_cmp);
+
+	/* Return the sorted elements array */
+	return elements;
+}
+
+int4hashset_t *
+int4hashset_copy(int4hashset_t *src)
+{
+	return src;
+}
+
+/*
+ * hashset_isspace() --- a non-locale-dependent isspace()
+ *
+ * Identical to array_isspace() in src/backend/utils/adt/arrayfuncs.c.
+ * We used to use isspace() for parsing hashset values, but that has
+ * undesirable results: a hashset value might be silently interpreted
+ * differently depending on the locale setting. So here, we hard-wire
+ * the traditional ASCII definition of isspace().
+ */
+bool
+hashset_isspace(char ch)
+{
+	if (ch == ' ' ||
+		ch == '\t' ||
+		ch == '\n' ||
+		ch == '\r' ||
+		ch == '\v' ||
+		ch == '\f')
+		return true;
+	return false;
+}
+
+/*
+ * Construct an SQL array from a simple C double array
+ */
+Datum
+int32_to_array(FunctionCallInfo fcinfo, int32 *d, int len)
+{
+	ArrayBuildState *astate = NULL;
+	int		 i;
+
+	for (i = 0; i < len; i++)
+	{
+		/* Stash away this field */
+		astate = accumArrayResult(astate,
+								  Int32GetDatum(d[i]),
+								  false,
+								  INT4OID,
+								  CurrentMemoryContext);
+	}
+
+	PG_RETURN_DATUM(makeArrayResult(astate,
+					CurrentMemoryContext));
+}
+
+static int
+int32_cmp(const void *a, const void *b)
+{
+	int32 arg1 = *(const int32 *)a;
+	int32 arg2 = *(const int32 *)b;
+
+	if (arg1 < arg2) return -1;
+	if (arg1 > arg2) return 1;
+	return 0;
+}
diff --git a/hashset.control b/hashset.control
new file mode 100644
index 0000000..0743003
--- /dev/null
+++ b/hashset.control
@@ -0,0 +1,3 @@
+comment = 'Provides hashset type.'
+default_version = '0.0.1'
+relocatable = true
diff --git a/hashset.h b/hashset.h
new file mode 100644
index 0000000..3f22133
--- /dev/null
+++ b/hashset.h
@@ -0,0 +1,53 @@
+#ifndef HASHSET_H
+#define HASHSET_H
+
+#include "postgres.h"
+#include "libpq/pqformat.h"
+#include "nodes/memnodes.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "catalog/pg_type.h"
+#include "common/hashfn.h"
+
+#define CEIL_DIV(a, b) (((a) + (b) - 1) / (b))
+#define HASHSET_STEP 13
+#define JENKINS_LOOKUP3_HASHFN_ID 1
+#define MURMURHASH32_HASHFN_ID 2
+#define NAIVE_HASHFN_ID 3
+#define NAIVE_HASHFN_MULTIPLIER 7691
+#define NAIVE_HASHFN_INCREMENT 4201
+
+/*
+ * These defaults should match the the SQL function int4hashset()
+ */
+#define DEFAULT_INITIAL_CAPACITY 0
+#define DEFAULT_LOAD_FACTOR 0.75
+#define DEFAULT_GROWTH_FACTOR 2.0
+#define DEFAULT_HASHFN_ID JENKINS_LOOKUP3_HASHFN_ID
+
+typedef struct int4hashset_t {
+	int32		vl_len_;		/* varlena header (do not touch directly!) */
+	int32		flags;			/* reserved for future use (versioning, ...) */
+	int32		capacity;		/* max number of element we have space for */
+	int32		nelements;		/* number of items added to the hashset */
+	int32		hashfn_id;		/* ID of the hash function used */
+	float4		load_factor;	/* Load factor before triggering resize */
+	float4		growth_factor;	/* Growth factor when resizing the hashset */
+	int32		ncollisions;	/* Number of collisions */
+	int32		max_collisions;	/* Maximum collisions for a single element */
+	int32		hash;			/* Stored hash value of the hashset */
+	char		data[FLEXIBLE_ARRAY_MEMBER];
+} int4hashset_t;
+
+int4hashset_t *int4hashset_allocate(int capacity, float4 load_factor, float4 growth_factor, int hashfn_id);
+int4hashset_t *int4hashset_resize(int4hashset_t * set);
+int4hashset_t *int4hashset_add_element(int4hashset_t *set, int32 value);
+bool int4hashset_contains_element(int4hashset_t *set, int32 value);
+int32 *int4hashset_extract_sorted_elements(int4hashset_t *set);
+int4hashset_t *int4hashset_copy(int4hashset_t *src);
+bool hashset_isspace(char ch);
+Datum int32_to_array(FunctionCallInfo fcinfo, int32 *d, int len);
+
+#endif /* HASHSET_H */
diff --git a/test/c_tests/test_send_recv.c b/test/c_tests/test_send_recv.c
new file mode 100644
index 0000000..cc7c48a
--- /dev/null
+++ b/test/c_tests/test_send_recv.c
@@ -0,0 +1,92 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <libpq-fe.h>
+
+void exit_nicely(PGconn *conn) {
+	PQfinish(conn);
+	exit(1);
+}
+
+int main() {
+	/* Connect to database specified by the PGDATABASE environment variable */
+	const char *hostname = getenv("PGHOST");
+	char		conninfo[1024];
+	PGconn	   *conn;
+
+	if (hostname == NULL)
+		hostname = "localhost";
+
+	/* Connect to database specified by the PGDATABASE environment variable */
+	snprintf(conninfo, sizeof(conninfo), "host=%s port=5432", hostname);
+	conn = PQconnectdb(conninfo);
+	if (PQstatus(conn) != CONNECTION_OK) {
+		fprintf(stderr, "Connection to database failed: %s", PQerrorMessage(conn));
+		exit_nicely(conn);
+	}
+
+	/* Create extension */
+	PQexec(conn, "CREATE EXTENSION IF NOT EXISTS hashset");
+
+	/* Create temporary table */
+	PQexec(conn, "CREATE TABLE IF NOT EXISTS test_hashset_send_recv (hashset_col int4hashset)");
+
+	/* Enable binary output */
+	PQexec(conn, "SET bytea_output = 'escape'");
+
+	/* Insert dummy data */
+	const char *insert_command = "INSERT INTO test_hashset_send_recv (hashset_col) VALUES ('{1,2,3}'::int4hashset)";
+	PGresult *res = PQexec(conn, insert_command);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK) {
+		fprintf(stderr, "INSERT failed: %s", PQerrorMessage(conn));
+		PQclear(res);
+		exit_nicely(conn);
+	}
+	PQclear(res);
+
+	/* Fetch the data in binary format */
+	const char *select_command = "SELECT hashset_col FROM test_hashset_send_recv";
+	int resultFormat = 1;  /* 0 = text, 1 = binary */
+	res = PQexecParams(conn, select_command, 0, NULL, NULL, NULL, NULL, resultFormat);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK) {
+		fprintf(stderr, "SELECT failed: %s", PQerrorMessage(conn));
+		PQclear(res);
+		exit_nicely(conn);
+	}
+
+	/* Store binary data for later use */
+	const char *binary_data = PQgetvalue(res, 0, 0);
+	int binary_data_length = PQgetlength(res, 0, 0);
+	PQclear(res);
+
+	/* Re-insert the binary data */
+	const char *insert_binary_command = "INSERT INTO test_hashset_send_recv (hashset_col) VALUES ($1)";
+	const char *paramValues[1] = {binary_data};
+	int paramLengths[1] = {binary_data_length};
+	int paramFormats[1] = {1}; /* binary format */
+	res = PQexecParams(conn, insert_binary_command, 1, NULL, paramValues, paramLengths, paramFormats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK) {
+		fprintf(stderr, "INSERT failed: %s", PQerrorMessage(conn));
+		PQclear(res);
+		exit_nicely(conn);
+	}
+	PQclear(res);
+
+	/* Check the data */
+	const char *check_command = "SELECT COUNT(DISTINCT hashset_col) AS unique_count, COUNT(*) FROM test_hashset_send_recv";
+	res = PQexec(conn, check_command);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK) {
+		fprintf(stderr, "SELECT failed: %s", PQerrorMessage(conn));
+		PQclear(res);
+		exit_nicely(conn);
+	}
+
+	/* Print the results */
+	printf("unique_count: %s\n", PQgetvalue(res, 0, 0));
+	printf("count: %s\n", PQgetvalue(res, 0, 1));
+	PQclear(res);
+
+	/* Disconnect */
+	PQfinish(conn);
+
+	return 0;
+}
diff --git a/test/c_tests/test_send_recv.sh b/test/c_tests/test_send_recv.sh
new file mode 100755
index 0000000..ab308b3
--- /dev/null
+++ b/test/c_tests/test_send_recv.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+# Get the directory of this script
+SCRIPT_DIR="$(dirname "$(realpath "$0")")"
+
+# Set up database
+export PGDATABASE=test_hashset_send_recv
+dropdb --if-exists "$PGDATABASE"
+createdb
+
+# Define directories
+EXPECTED_DIR="$SCRIPT_DIR/../expected"
+RESULTS_DIR="$SCRIPT_DIR/../results"
+
+# Create the results directory if it doesn't exist
+mkdir -p "$RESULTS_DIR"
+
+# Run the C test and save its output to the results directory
+"$SCRIPT_DIR/test_send_recv" > "$RESULTS_DIR/test_send_recv.out"
+
+printf "test test_send_recv               ... "
+
+# Compare the actual output with the expected output
+if diff -q "$RESULTS_DIR/test_send_recv.out" "$EXPECTED_DIR/test_send_recv.out" > /dev/null 2>&1; then
+    echo "ok"
+    # Clean up by removing the results directory if the test passed
+    rm -r "$RESULTS_DIR"
+else
+    echo "failed"
+    git diff --no-index --color "$EXPECTED_DIR/test_send_recv.out" "$RESULTS_DIR/test_send_recv.out"
+fi
diff --git a/test/expected/basic.out b/test/expected/basic.out
new file mode 100644
index 0000000..b89ab52
--- /dev/null
+++ b/test/expected/basic.out
@@ -0,0 +1,304 @@
+/*
+ * Hashset Type
+ */
+SELECT '{}'::int4hashset; -- empty int4hashset
+ int4hashset 
+-------------
+ {}
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset;
+ int4hashset 
+-------------
+ {3,2,1}
+(1 row)
+
+SELECT '{-2147483648,0,2147483647}'::int4hashset;
+        int4hashset         
+----------------------------
+ {0,2147483647,-2147483648}
+(1 row)
+
+SELECT '{-2147483649}'::int4hashset; -- out of range
+ERROR:  value "-2147483649}" is out of range for type integer
+LINE 1: SELECT '{-2147483649}'::int4hashset;
+               ^
+SELECT '{2147483648}'::int4hashset; -- out of range
+ERROR:  value "2147483648}" is out of range for type integer
+LINE 1: SELECT '{2147483648}'::int4hashset;
+               ^
+/*
+ * Hashset Functions
+ */
+SELECT int4hashset();
+ int4hashset 
+-------------
+ {}
+(1 row)
+
+SELECT int4hashset(
+    capacity := 10,
+    load_factor := 0.9,
+    growth_factor := 1.1,
+    hashfn_id := 1
+);
+ int4hashset 
+-------------
+ {}
+(1 row)
+
+SELECT hashset_add(int4hashset(), 123);
+ hashset_add 
+-------------
+ {123}
+(1 row)
+
+SELECT hashset_add(NULL::int4hashset, 123);
+ hashset_add 
+-------------
+ {123}
+(1 row)
+
+SELECT hashset_add('{123}'::int4hashset, 456);
+ hashset_add 
+-------------
+ {456,123}
+(1 row)
+
+SELECT hashset_contains('{123,456}'::int4hashset, 456); -- true
+ hashset_contains 
+------------------
+ t
+(1 row)
+
+SELECT hashset_contains('{123,456}'::int4hashset, 789); -- false
+ hashset_contains 
+------------------
+ f
+(1 row)
+
+SELECT hashset_merge('{1,2}'::int4hashset, '{2,3}'::int4hashset);
+ hashset_merge 
+---------------
+ {3,1,2}
+(1 row)
+
+SELECT hashset_to_array('{1,2,3}'::int4hashset);
+ hashset_to_array 
+------------------
+ {3,2,1}
+(1 row)
+
+SELECT hashset_count('{1,2,3}'::int4hashset); -- 3
+ hashset_count 
+---------------
+             3
+(1 row)
+
+SELECT hashset_capacity(int4hashset(capacity := 10)); -- 10
+ hashset_capacity 
+------------------
+               10
+(1 row)
+
+SELECT hashset_intersection('{1,2}'::int4hashset,'{2,3}'::int4hashset);
+ hashset_intersection 
+----------------------
+ {2}
+(1 row)
+
+SELECT hashset_difference('{1,2}'::int4hashset,'{2,3}'::int4hashset);
+ hashset_difference 
+--------------------
+ {1}
+(1 row)
+
+SELECT hashset_symmetric_difference('{1,2}'::int4hashset,'{2,3}'::int4hashset);
+ hashset_symmetric_difference 
+------------------------------
+ {1,3}
+(1 row)
+
+/*
+ * Aggregation Functions
+ */
+SELECT hashset_agg(i) FROM generate_series(1,10) AS i;
+      hashset_agg       
+------------------------
+ {6,10,1,8,2,3,4,5,9,7}
+(1 row)
+
+SELECT hashset_agg(h) FROM
+(
+    SELECT hashset_agg(i) AS h FROM generate_series(1,5) AS i
+    UNION ALL
+    SELECT hashset_agg(j) AS h FROM generate_series(6,10) AS j
+) q;
+      hashset_agg       
+------------------------
+ {6,8,1,3,2,10,4,5,9,7}
+(1 row)
+
+/*
+ * Operator Definitions
+ */
+SELECT '{2}'::int4hashset = '{1}'::int4hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::int4hashset = '{2}'::int4hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::int4hashset = '{3}'::int4hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset = '{1,2,3}'::int4hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset = '{2,3,1}'::int4hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset = '{4,5,6}'::int4hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset = '{1,2}'::int4hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset = '{1,2,3,4}'::int4hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::int4hashset <> '{1}'::int4hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::int4hashset <> '{2}'::int4hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::int4hashset <> '{3}'::int4hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset <> '{1,2,3}'::int4hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset <> '{2,3,1}'::int4hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset <> '{4,5,6}'::int4hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset <> '{1,2}'::int4hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset <> '{1,2,3,4}'::int4hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{1,2,3}'::int4hashset || 4;
+ ?column?  
+-----------
+ {1,3,2,4}
+(1 row)
+
+SELECT 4 || '{1,2,3}'::int4hashset;
+ ?column?  
+-----------
+ {1,3,2,4}
+(1 row)
+
+/*
+ * Hashset Hash Operators
+ */
+SELECT hashset_hash('{1,2,3}'::int4hashset);
+ hashset_hash 
+--------------
+    868123687
+(1 row)
+
+SELECT hashset_hash('{3,2,1}'::int4hashset);
+ hashset_hash 
+--------------
+    868123687
+(1 row)
+
+SELECT COUNT(*), COUNT(DISTINCT h)
+FROM
+(
+    SELECT '{1,2,3}'::int4hashset AS h
+    UNION ALL
+    SELECT '{3,2,1}'::int4hashset AS h
+) q;
+ count | count 
+-------+-------
+     2 |     1
+(1 row)
+
+/*
+ * Hashset Btree Operators
+ *
+ * Ordering of hashsets is not based on lexicographic order of elements.
+ * - If two hashsets are not equal, they retain consistent relative order.
+ * - If two hashsets are equal but have elements in different orders, their
+ *   ordering is non-deterministic. This is inherent since the comparison
+ *   function must return 0 for equal hashsets, giving no indication of order.
+ */
+SELECT h FROM
+(
+    SELECT '{1,2,3}'::int4hashset AS h
+    UNION ALL
+    SELECT '{4,5,6}'::int4hashset AS h
+    UNION ALL
+    SELECT '{7,8,9}'::int4hashset AS h
+) q
+ORDER BY h;
+    h    
+---------
+ {9,7,8}
+ {3,2,1}
+ {5,6,4}
+(3 rows)
+
diff --git a/test/expected/invalid.out b/test/expected/invalid.out
new file mode 100644
index 0000000..bd44199
--- /dev/null
+++ b/test/expected/invalid.out
@@ -0,0 +1,4 @@
+SELECT '{1,2s}'::int4hashset;
+ERROR:  unexpected character "s" in hashset input
+LINE 1: SELECT '{1,2s}'::int4hashset;
+               ^
diff --git a/test/expected/io_varying_lengths.out b/test/expected/io_varying_lengths.out
new file mode 100644
index 0000000..45e9fb1
--- /dev/null
+++ b/test/expected/io_varying_lengths.out
@@ -0,0 +1,100 @@
+/*
+ * This test verifies the hashset input/output functions for varying 
+ * initial capacities, ensuring functionality across different sizes.
+ */
+SELECT hashset_sorted('{1}'::int4hashset);
+ hashset_sorted 
+----------------
+ {1}
+(1 row)
+
+SELECT hashset_sorted('{1,2}'::int4hashset);
+ hashset_sorted 
+----------------
+ {1,2}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3}'::int4hashset);
+ hashset_sorted 
+----------------
+ {1,2,3}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4}'::int4hashset);
+ hashset_sorted 
+----------------
+ {1,2,3,4}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5}'::int4hashset);
+ hashset_sorted 
+----------------
+ {1,2,3,4,5}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6}'::int4hashset);
+ hashset_sorted 
+----------------
+ {1,2,3,4,5,6}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7}'::int4hashset);
+ hashset_sorted  
+-----------------
+ {1,2,3,4,5,6,7}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8}'::int4hashset);
+  hashset_sorted   
+-------------------
+ {1,2,3,4,5,6,7,8}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9}'::int4hashset);
+   hashset_sorted    
+---------------------
+ {1,2,3,4,5,6,7,8,9}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10}'::int4hashset);
+     hashset_sorted     
+------------------------
+ {1,2,3,4,5,6,7,8,9,10}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11}'::int4hashset);
+      hashset_sorted       
+---------------------------
+ {1,2,3,4,5,6,7,8,9,10,11}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12}'::int4hashset);
+        hashset_sorted        
+------------------------------
+ {1,2,3,4,5,6,7,8,9,10,11,12}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12,13}'::int4hashset);
+         hashset_sorted          
+---------------------------------
+ {1,2,3,4,5,6,7,8,9,10,11,12,13}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12,13,14}'::int4hashset);
+           hashset_sorted           
+------------------------------------
+ {1,2,3,4,5,6,7,8,9,10,11,12,13,14}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}'::int4hashset);
+            hashset_sorted             
+---------------------------------------
+ {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}
+(1 row)
+
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}'::int4hashset);
+              hashset_sorted              
+------------------------------------------
+ {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}
+(1 row)
+
diff --git a/test/expected/parsing.out b/test/expected/parsing.out
new file mode 100644
index 0000000..263797e
--- /dev/null
+++ b/test/expected/parsing.out
@@ -0,0 +1,71 @@
+/* Valid */
+SELECT '{1,23,-456}'::int4hashset;
+ int4hashset 
+-------------
+ {1,-456,23}
+(1 row)
+
+SELECT ' { 1 , 23 , -456 } '::int4hashset;
+ int4hashset 
+-------------
+ {1,-456,23}
+(1 row)
+
+/* Only whitespace is allowed after the closing brace */
+SELECT ' { 1 , 23 , -456 } 1'::int4hashset; -- error
+ERROR:  malformed hashset literal: "1"
+LINE 2: SELECT ' { 1 , 23 , -456 } 1'::int4hashset;
+               ^
+DETAIL:  Junk after closing right brace.
+SELECT ' { 1 , 23 , -456 } ,'::int4hashset; -- error
+ERROR:  malformed hashset literal: ","
+LINE 1: SELECT ' { 1 , 23 , -456 } ,'::int4hashset;
+               ^
+DETAIL:  Junk after closing right brace.
+SELECT ' { 1 , 23 , -456 } {'::int4hashset; -- error
+ERROR:  malformed hashset literal: "{"
+LINE 1: SELECT ' { 1 , 23 , -456 } {'::int4hashset;
+               ^
+DETAIL:  Junk after closing right brace.
+SELECT ' { 1 , 23 , -456 } }'::int4hashset; -- error
+ERROR:  malformed hashset literal: "}"
+LINE 1: SELECT ' { 1 , 23 , -456 } }'::int4hashset;
+               ^
+DETAIL:  Junk after closing right brace.
+SELECT ' { 1 , 23 , -456 } x'::int4hashset; -- error
+ERROR:  malformed hashset literal: "x"
+LINE 1: SELECT ' { 1 , 23 , -456 } x'::int4hashset;
+               ^
+DETAIL:  Junk after closing right brace.
+/* Unexpected character when expecting closing brace */
+SELECT ' { 1 , 23 , -456 1'::int4hashset; -- error
+ERROR:  unexpected character "1" in hashset input
+LINE 2: SELECT ' { 1 , 23 , -456 1'::int4hashset;
+               ^
+SELECT ' { 1 , 23 , -456 {'::int4hashset; -- error
+ERROR:  unexpected character "{" in hashset input
+LINE 1: SELECT ' { 1 , 23 , -456 {'::int4hashset;
+               ^
+SELECT ' { 1 , 23 , -456 x'::int4hashset; -- error
+ERROR:  unexpected character "x" in hashset input
+LINE 1: SELECT ' { 1 , 23 , -456 x'::int4hashset;
+               ^
+/* Error handling for strtol */
+SELECT ' { , 23 , -456 } '::int4hashset; -- error
+ERROR:  invalid input syntax for integer: ", 23 , -456 } "
+LINE 2: SELECT ' { , 23 , -456 } '::int4hashset;
+               ^
+SELECT ' { 1 , 23 , '::int4hashset; -- error
+ERROR:  invalid input syntax for integer: ""
+LINE 1: SELECT ' { 1 , 23 , '::int4hashset;
+               ^
+SELECT ' { s , 23 , -456 } '::int4hashset; -- error
+ERROR:  invalid input syntax for integer: "s , 23 , -456 } "
+LINE 1: SELECT ' { s , 23 , -456 } '::int4hashset;
+               ^
+/* Missing opening brace */
+SELECT ' 1 , 23 , -456 } '::int4hashset; -- error
+ERROR:  invalid input syntax for hashset: "1 , 23 , -456 } "
+LINE 2: SELECT ' 1 , 23 , -456 } '::int4hashset;
+               ^
+DETAIL:  Hashset representation must start with "{".
diff --git a/test/expected/prelude.out b/test/expected/prelude.out
new file mode 100644
index 0000000..f34e190
--- /dev/null
+++ b/test/expected/prelude.out
@@ -0,0 +1,7 @@
+CREATE EXTENSION hashset;
+CREATE OR REPLACE FUNCTION hashset_sorted(int4hashset)
+RETURNS TEXT AS
+$$
+SELECT array_agg(i ORDER BY i::int)::text
+FROM regexp_split_to_table(regexp_replace($1::text,'^{|}$','','g'),',') i
+$$ LANGUAGE sql;
diff --git a/test/expected/random.out b/test/expected/random.out
new file mode 100644
index 0000000..9d9026b
--- /dev/null
+++ b/test/expected/random.out
@@ -0,0 +1,38 @@
+SELECT setseed(0.12345);
+ setseed 
+---------
+ 
+(1 row)
+
+\set MAX_INT 2147483647
+CREATE TABLE hashset_random_int4_numbers AS
+    SELECT
+        (random()*:MAX_INT)::int AS i
+    FROM generate_series(1,(random()*10000)::int)
+;
+SELECT
+    md5(hashset_sorted)
+FROM
+(
+    SELECT
+        hashset_sorted(int4hashset(format('{%s}',string_agg(i::text,','))))
+    FROM hashset_random_int4_numbers
+) q;
+               md5                
+----------------------------------
+ 4ad6e4233861becbeb4a665376952a16
+(1 row)
+
+SELECT
+    md5(input_sorted)
+FROM
+(
+    SELECT
+        format('{%s}',string_agg(i::text,',' ORDER BY i)) AS input_sorted
+    FROM hashset_random_int4_numbers
+) q;
+               md5                
+----------------------------------
+ 4ad6e4233861becbeb4a665376952a16
+(1 row)
+
diff --git a/test/expected/reported_bugs.out b/test/expected/reported_bugs.out
new file mode 100644
index 0000000..b356b64
--- /dev/null
+++ b/test/expected/reported_bugs.out
@@ -0,0 +1,138 @@
+/*
+ * Bug in hashset_add() and hashset_merge() functions altering original hashset.
+ *
+ * Previously, the hashset_add() and hashset_merge() functions were modifying the
+ * original hashset in-place, leading to unexpected results as the original data
+ * within the hashset was being altered.
+ *
+ * The issue was addressed by implementing a macro function named
+ * PG_GETARG_INT4HASHSET_COPY() within the C code. This function guarantees that
+ * a copy of the hashset is created and subsequently modified, thereby preserving
+ * the integrity of the original hashset.
+ *
+ * As a result of this fix, hashset_add() and hashset_merge() now operate on
+ * a copied hashset, ensuring that the original data remains unaltered, and
+ * the query executes correctly.
+ */
+SELECT
+    q.hashset_agg,
+    hashset_add(hashset_agg,4)
+FROM
+(
+    SELECT
+        hashset_agg(generate_series)
+    FROM generate_series(1,3)
+) q;
+ hashset_agg | hashset_add 
+-------------+-------------
+ {3,1,2}     | {3,4,1,2}
+(1 row)
+
+/*
+ * Bug in hashset_hash() function with respect to element insertion order.
+ *
+ * Prior to the fix, the hashset_hash() function was accumulating the hashes
+ * of individual elements in a non-commutative manner. As a consequence, the
+ * final hash value was sensitive to the order in which elements were inserted
+ * into the hashset. This behavior led to inconsistencies, as logically
+ * equivalent sets (i.e., sets with the same elements but in different orders)
+ * produced different hash values.
+ *
+ * The bug was fixed by modifying the hashset_hash() function to use a
+ * commutative operation when combining the hashes of individual elements.
+ * This change ensures that the final hash value is independent of the
+ * element insertion order, and logically equivalent sets produce the
+ * same hash.
+ */
+SELECT hashset_hash('{1,2}'::int4hashset);
+ hashset_hash 
+--------------
+   -840053840
+(1 row)
+
+SELECT hashset_hash('{2,1}'::int4hashset);
+ hashset_hash 
+--------------
+   -840053840
+(1 row)
+
+SELECT hashset_cmp('{1,2}','{2,1}')
+UNION
+SELECT hashset_cmp('{1,2}','{1,2,1}')
+UNION
+SELECT hashset_cmp('{1,2}','{1,2}');
+ hashset_cmp 
+-------------
+           0
+(1 row)
+
+/*
+ * Bug in int4hashset_resize() not utilizing growth_factor.
+ *
+ * The previous implementation hard-coded a growth factor of 2, neglecting
+ * the struct's growth_factor field. This bug was addressed by properly
+ * using growth_factor for new capacity calculation, with an additional
+ * safety check to prevent possible infinite loops in resizing.
+ */
+SELECT hashset_capacity(hashset_add(hashset_add(int4hashset(
+    capacity := 0,
+    load_factor := 0.75,
+    growth_factor := 1.1
+), 123), 456));
+ hashset_capacity 
+------------------
+                2
+(1 row)
+
+SELECT hashset_capacity(hashset_add(hashset_add(int4hashset(
+    capacity := 0,
+    load_factor := 0.75,
+    growth_factor := 10
+), 123), 456));
+ hashset_capacity 
+------------------
+               10
+(1 row)
+
+/*
+ * Bug in int4hashset_capacity() not detoasting input correctly.
+ */
+SELECT hashset_capacity(int4hashset(capacity:=10)) AS capacity_10;
+ capacity_10 
+-------------
+          10
+(1 row)
+
+SELECT hashset_capacity(int4hashset(capacity:=1000)) AS capacity_1000;
+ capacity_1000 
+---------------
+          1000
+(1 row)
+
+SELECT hashset_capacity(int4hashset(capacity:=100000)) AS capacity_100000;
+ capacity_100000 
+-----------------
+          100000
+(1 row)
+
+CREATE TABLE test_capacity_10 AS SELECT int4hashset(capacity:=10) AS capacity_10;
+CREATE TABLE test_capacity_1000 AS SELECT int4hashset(capacity:=1000) AS capacity_1000;
+CREATE TABLE test_capacity_100000 AS SELECT int4hashset(capacity:=100000) AS capacity_100000;
+SELECT hashset_capacity(capacity_10) AS capacity_10 FROM test_capacity_10;
+ capacity_10 
+-------------
+          10
+(1 row)
+
+SELECT hashset_capacity(capacity_1000) AS capacity_1000 FROM test_capacity_1000;
+ capacity_1000 
+---------------
+          1000
+(1 row)
+
+SELECT hashset_capacity(capacity_100000) AS capacity_100000 FROM test_capacity_100000;
+ capacity_100000 
+-----------------
+          100000
+(1 row)
+
diff --git a/test/expected/table.out b/test/expected/table.out
new file mode 100644
index 0000000..9793a49
--- /dev/null
+++ b/test/expected/table.out
@@ -0,0 +1,25 @@
+CREATE TABLE users (
+    user_id int PRIMARY KEY,
+    user_likes int4hashset DEFAULT int4hashset(capacity := 2)
+);
+INSERT INTO users (user_id) VALUES (1);
+UPDATE users SET user_likes = hashset_add(user_likes, 101) WHERE user_id = 1;
+UPDATE users SET user_likes = hashset_add(user_likes, 202) WHERE user_id = 1;
+SELECT hashset_contains(user_likes, 101) FROM users WHERE user_id = 1;
+ hashset_contains 
+------------------
+ t
+(1 row)
+
+SELECT hashset_count(user_likes) FROM users WHERE user_id = 1;
+ hashset_count 
+---------------
+             2
+(1 row)
+
+SELECT hashset_sorted(user_likes) FROM users WHERE user_id = 1;
+ hashset_sorted 
+----------------
+ {101,202}
+(1 row)
+
diff --git a/test/expected/test_send_recv.out b/test/expected/test_send_recv.out
new file mode 100644
index 0000000..12382d5
--- /dev/null
+++ b/test/expected/test_send_recv.out
@@ -0,0 +1,2 @@
+unique_count: 1
+count: 2
diff --git a/test/sql/basic.sql b/test/sql/basic.sql
new file mode 100644
index 0000000..8688666
--- /dev/null
+++ b/test/sql/basic.sql
@@ -0,0 +1,108 @@
+/*
+ * Hashset Type
+ */
+
+SELECT '{}'::int4hashset; -- empty int4hashset
+SELECT '{1,2,3}'::int4hashset;
+SELECT '{-2147483648,0,2147483647}'::int4hashset;
+SELECT '{-2147483649}'::int4hashset; -- out of range
+SELECT '{2147483648}'::int4hashset; -- out of range
+
+/*
+ * Hashset Functions
+ */
+
+SELECT int4hashset();
+SELECT int4hashset(
+    capacity := 10,
+    load_factor := 0.9,
+    growth_factor := 1.1,
+    hashfn_id := 1
+);
+SELECT hashset_add(int4hashset(), 123);
+SELECT hashset_add(NULL::int4hashset, 123);
+SELECT hashset_add('{123}'::int4hashset, 456);
+SELECT hashset_contains('{123,456}'::int4hashset, 456); -- true
+SELECT hashset_contains('{123,456}'::int4hashset, 789); -- false
+SELECT hashset_merge('{1,2}'::int4hashset, '{2,3}'::int4hashset);
+SELECT hashset_to_array('{1,2,3}'::int4hashset);
+SELECT hashset_count('{1,2,3}'::int4hashset); -- 3
+SELECT hashset_capacity(int4hashset(capacity := 10)); -- 10
+SELECT hashset_intersection('{1,2}'::int4hashset,'{2,3}'::int4hashset);
+SELECT hashset_difference('{1,2}'::int4hashset,'{2,3}'::int4hashset);
+SELECT hashset_symmetric_difference('{1,2}'::int4hashset,'{2,3}'::int4hashset);
+
+/*
+ * Aggregation Functions
+ */
+
+SELECT hashset_agg(i) FROM generate_series(1,10) AS i;
+
+SELECT hashset_agg(h) FROM
+(
+    SELECT hashset_agg(i) AS h FROM generate_series(1,5) AS i
+    UNION ALL
+    SELECT hashset_agg(j) AS h FROM generate_series(6,10) AS j
+) q;
+
+/*
+ * Operator Definitions
+ */
+
+SELECT '{2}'::int4hashset = '{1}'::int4hashset; -- false
+SELECT '{2}'::int4hashset = '{2}'::int4hashset; -- true
+SELECT '{2}'::int4hashset = '{3}'::int4hashset; -- false
+
+SELECT '{1,2,3}'::int4hashset = '{1,2,3}'::int4hashset; -- true
+SELECT '{1,2,3}'::int4hashset = '{2,3,1}'::int4hashset; -- true
+SELECT '{1,2,3}'::int4hashset = '{4,5,6}'::int4hashset; -- false
+SELECT '{1,2,3}'::int4hashset = '{1,2}'::int4hashset; -- false
+SELECT '{1,2,3}'::int4hashset = '{1,2,3,4}'::int4hashset; -- false
+
+SELECT '{2}'::int4hashset <> '{1}'::int4hashset; -- true
+SELECT '{2}'::int4hashset <> '{2}'::int4hashset; -- false
+SELECT '{2}'::int4hashset <> '{3}'::int4hashset; -- true
+
+SELECT '{1,2,3}'::int4hashset <> '{1,2,3}'::int4hashset; -- false
+SELECT '{1,2,3}'::int4hashset <> '{2,3,1}'::int4hashset; -- false
+SELECT '{1,2,3}'::int4hashset <> '{4,5,6}'::int4hashset; -- true
+SELECT '{1,2,3}'::int4hashset <> '{1,2}'::int4hashset; -- true
+SELECT '{1,2,3}'::int4hashset <> '{1,2,3,4}'::int4hashset; -- true
+
+SELECT '{1,2,3}'::int4hashset || 4;
+SELECT 4 || '{1,2,3}'::int4hashset;
+
+/*
+ * Hashset Hash Operators
+ */
+
+SELECT hashset_hash('{1,2,3}'::int4hashset);
+SELECT hashset_hash('{3,2,1}'::int4hashset);
+
+SELECT COUNT(*), COUNT(DISTINCT h)
+FROM
+(
+    SELECT '{1,2,3}'::int4hashset AS h
+    UNION ALL
+    SELECT '{3,2,1}'::int4hashset AS h
+) q;
+
+/*
+ * Hashset Btree Operators
+ *
+ * Ordering of hashsets is not based on lexicographic order of elements.
+ * - If two hashsets are not equal, they retain consistent relative order.
+ * - If two hashsets are equal but have elements in different orders, their
+ *   ordering is non-deterministic. This is inherent since the comparison
+ *   function must return 0 for equal hashsets, giving no indication of order.
+ */
+
+SELECT h FROM
+(
+    SELECT '{1,2,3}'::int4hashset AS h
+    UNION ALL
+    SELECT '{4,5,6}'::int4hashset AS h
+    UNION ALL
+    SELECT '{7,8,9}'::int4hashset AS h
+) q
+ORDER BY h;
diff --git a/test/sql/benchmark.sql b/test/sql/benchmark.sql
new file mode 100644
index 0000000..1535c22
--- /dev/null
+++ b/test/sql/benchmark.sql
@@ -0,0 +1,191 @@
+DROP EXTENSION IF EXISTS hashset CASCADE;
+CREATE EXTENSION hashset;
+
+\timing on
+
+\echo * Benchmark array_agg(DISTINCT ...) vs hashset_agg()
+
+DROP TABLE IF EXISTS benchmark_input_100k;
+DROP TABLE IF EXISTS benchmark_input_10M;
+DROP TABLE IF EXISTS benchmark_array_agg;
+DROP TABLE IF EXISTS benchmark_hashset_agg;
+
+SELECT setseed(0.12345);
+
+CREATE TABLE benchmark_input_100k AS
+SELECT
+    i,
+    i/10 AS j,
+    (floor(4294967296 * random()) - 2147483648)::int AS rnd
+FROM generate_series(1,100000) AS i;
+
+CREATE TABLE benchmark_input_10M AS
+SELECT
+    i,
+    i/10 AS j,
+    (floor(4294967296 * random()) - 2147483648)::int AS rnd
+FROM generate_series(1,10000000) AS i;
+
+\echo *** Benchmark array_agg(DISTINCT ...) vs hashset_agg(...) for 100k unique integers
+CREATE TABLE benchmark_array_agg AS
+SELECT array_agg(DISTINCT i) FROM benchmark_input_100k;
+CREATE TABLE benchmark_hashset_agg AS
+SELECT hashset_agg(i) FROM benchmark_input_100k;
+
+\echo *** Benchmark array_agg(DISTINCT ...) vs hashset_agg(...) for 10M unique integers
+INSERT INTO benchmark_array_agg
+SELECT array_agg(DISTINCT i) FROM benchmark_input_10M;
+INSERT INTO benchmark_hashset_agg
+SELECT hashset_agg(i) FROM benchmark_input_10M;
+
+\echo *** Benchmark array_agg(DISTINCT ...) vs hashset_agg(...) for 100k integers (10% uniqueness)
+INSERT INTO benchmark_array_agg
+SELECT array_agg(DISTINCT j) FROM benchmark_input_100k;
+INSERT INTO benchmark_hashset_agg
+SELECT hashset_agg(j) FROM benchmark_input_100k;
+
+\echo *** Benchmark array_agg(DISTINCT ...) vs hashset_agg(...) for 10M integers (10% uniqueness)
+INSERT INTO benchmark_array_agg
+SELECT array_agg(DISTINCT j) FROM benchmark_input_10M;
+INSERT INTO benchmark_hashset_agg
+SELECT hashset_agg(j) FROM benchmark_input_10M;
+
+\echo *** Benchmark array_agg(DISTINCT ...) vs hashset_agg(...) for 100k random integers
+INSERT INTO benchmark_array_agg
+SELECT array_agg(DISTINCT rnd) FROM benchmark_input_100k;
+INSERT INTO benchmark_hashset_agg
+SELECT hashset_agg(rnd) FROM benchmark_input_100k;
+
+\echo *** Benchmark array_agg(DISTINCT ...) vs hashset_agg(...) for 10M random integers
+INSERT INTO benchmark_array_agg
+SELECT array_agg(DISTINCT rnd) FROM benchmark_input_10M;
+INSERT INTO benchmark_hashset_agg
+SELECT hashset_agg(rnd) FROM benchmark_input_10M;
+
+SELECT cardinality(array_agg) FROM benchmark_array_agg ORDER BY 1;
+
+SELECT
+    hashset_count(hashset_agg),
+    hashset_capacity(hashset_agg),
+    hashset_collisions(hashset_agg),
+    hashset_max_collisions(hashset_agg)
+FROM benchmark_hashset_agg;
+
+SELECT hashset_capacity(hashset_agg(rnd)) FROM benchmark_input_10M;
+
+\echo * Benchmark different hash functions
+
+\echo *** Elements in sequence 1..100000
+
+\echo - Testing default hash function (Jenkins/lookup3)
+
+DO
+$$
+DECLARE
+    h int4hashset;
+BEGIN
+    h := int4hashset(hashfn_id := 1);
+    FOR i IN 1..100000 LOOP
+        h := hashset_add(h, i);
+    END LOOP;
+    RAISE NOTICE 'hashset_count: %', hashset_count(h);
+    RAISE NOTICE 'hashset_capacity: %', hashset_capacity(h);
+    RAISE NOTICE 'hashset_collisions: %', hashset_collisions(h);
+    RAISE NOTICE 'hashset_max_collisions: %', hashset_max_collisions(h);
+END
+$$ LANGUAGE plpgsql;
+
+\echo - Testing Murmurhash32
+
+DO
+$$
+DECLARE
+    h int4hashset;
+BEGIN
+    h := int4hashset(hashfn_id := 2);
+    FOR i IN 1..100000 LOOP
+        h := hashset_add(h, i);
+    END LOOP;
+    RAISE NOTICE 'hashset_count: %', hashset_count(h);
+    RAISE NOTICE 'hashset_capacity: %', hashset_capacity(h);
+    RAISE NOTICE 'hashset_collisions: %', hashset_collisions(h);
+    RAISE NOTICE 'hashset_max_collisions: %', hashset_max_collisions(h);
+END
+$$ LANGUAGE plpgsql;
+
+\echo - Testing naive hash function
+
+DO
+$$
+DECLARE
+    h int4hashset;
+BEGIN
+    h := int4hashset(hashfn_id := 3);
+    FOR i IN 1..100000 LOOP
+        h := hashset_add(h, i);
+    END LOOP;
+    RAISE NOTICE 'hashset_count: %', hashset_count(h);
+    RAISE NOTICE 'hashset_capacity: %', hashset_capacity(h);
+    RAISE NOTICE 'hashset_collisions: %', hashset_collisions(h);
+    RAISE NOTICE 'hashset_max_collisions: %', hashset_max_collisions(h);
+END
+$$ LANGUAGE plpgsql;
+
+\echo *** Testing 100000 random ints
+
+SELECT setseed(0.12345);
+\echo - Testing default hash function (Jenkins/lookup3)
+
+DO
+$$
+DECLARE
+    h int4hashset;
+BEGIN
+    h := int4hashset(hashfn_id := 1);
+    FOR i IN 1..100000 LOOP
+        h := hashset_add(h, (floor(4294967296 * random()) - 2147483648)::int);
+    END LOOP;
+    RAISE NOTICE 'hashset_count: %', hashset_count(h);
+    RAISE NOTICE 'hashset_capacity: %', hashset_capacity(h);
+    RAISE NOTICE 'hashset_collisions: %', hashset_collisions(h);
+    RAISE NOTICE 'hashset_max_collisions: %', hashset_max_collisions(h);
+END
+$$ LANGUAGE plpgsql;
+
+SELECT setseed(0.12345);
+\echo - Testing Murmurhash32
+
+DO
+$$
+DECLARE
+    h int4hashset;
+BEGIN
+    h := int4hashset(hashfn_id := 2);
+    FOR i IN 1..100000 LOOP
+        h := hashset_add(h, (floor(4294967296 * random()) - 2147483648)::int);
+    END LOOP;
+    RAISE NOTICE 'hashset_count: %', hashset_count(h);
+    RAISE NOTICE 'hashset_capacity: %', hashset_capacity(h);
+    RAISE NOTICE 'hashset_collisions: %', hashset_collisions(h);
+    RAISE NOTICE 'hashset_max_collisions: %', hashset_max_collisions(h);
+END
+$$ LANGUAGE plpgsql;
+
+SELECT setseed(0.12345);
+\echo - Testing naive hash function
+
+DO
+$$
+DECLARE
+    h int4hashset;
+BEGIN
+    h := int4hashset(hashfn_id := 3);
+    FOR i IN 1..100000 LOOP
+        h := hashset_add(h, (floor(4294967296 * random()) - 2147483648)::int);
+    END LOOP;
+    RAISE NOTICE 'hashset_count: %', hashset_count(h);
+    RAISE NOTICE 'hashset_capacity: %', hashset_capacity(h);
+    RAISE NOTICE 'hashset_collisions: %', hashset_collisions(h);
+    RAISE NOTICE 'hashset_max_collisions: %', hashset_max_collisions(h);
+END
+$$ LANGUAGE plpgsql;
diff --git a/test/sql/invalid.sql b/test/sql/invalid.sql
new file mode 100644
index 0000000..43689ab
--- /dev/null
+++ b/test/sql/invalid.sql
@@ -0,0 +1 @@
+SELECT '{1,2s}'::int4hashset;
diff --git a/test/sql/io_varying_lengths.sql b/test/sql/io_varying_lengths.sql
new file mode 100644
index 0000000..8acb6b8
--- /dev/null
+++ b/test/sql/io_varying_lengths.sql
@@ -0,0 +1,21 @@
+/*
+ * This test verifies the hashset input/output functions for varying 
+ * initial capacities, ensuring functionality across different sizes.
+ */
+
+SELECT hashset_sorted('{1}'::int4hashset);
+SELECT hashset_sorted('{1,2}'::int4hashset);
+SELECT hashset_sorted('{1,2,3}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12,13}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12,13,14}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}'::int4hashset);
+SELECT hashset_sorted('{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}'::int4hashset);
diff --git a/test/sql/parsing.sql b/test/sql/parsing.sql
new file mode 100644
index 0000000..1e56bbe
--- /dev/null
+++ b/test/sql/parsing.sql
@@ -0,0 +1,23 @@
+/* Valid */
+SELECT '{1,23,-456}'::int4hashset;
+SELECT ' { 1 , 23 , -456 } '::int4hashset;
+
+/* Only whitespace is allowed after the closing brace */
+SELECT ' { 1 , 23 , -456 } 1'::int4hashset; -- error
+SELECT ' { 1 , 23 , -456 } ,'::int4hashset; -- error
+SELECT ' { 1 , 23 , -456 } {'::int4hashset; -- error
+SELECT ' { 1 , 23 , -456 } }'::int4hashset; -- error
+SELECT ' { 1 , 23 , -456 } x'::int4hashset; -- error
+
+/* Unexpected character when expecting closing brace */
+SELECT ' { 1 , 23 , -456 1'::int4hashset; -- error
+SELECT ' { 1 , 23 , -456 {'::int4hashset; -- error
+SELECT ' { 1 , 23 , -456 x'::int4hashset; -- error
+
+/* Error handling for strtol */
+SELECT ' { , 23 , -456 } '::int4hashset; -- error
+SELECT ' { 1 , 23 , '::int4hashset; -- error
+SELECT ' { s , 23 , -456 } '::int4hashset; -- error
+
+/* Missing opening brace */
+SELECT ' 1 , 23 , -456 } '::int4hashset; -- error
diff --git a/test/sql/prelude.sql b/test/sql/prelude.sql
new file mode 100644
index 0000000..2fee0fc
--- /dev/null
+++ b/test/sql/prelude.sql
@@ -0,0 +1,8 @@
+CREATE EXTENSION hashset;
+
+CREATE OR REPLACE FUNCTION hashset_sorted(int4hashset)
+RETURNS TEXT AS
+$$
+SELECT array_agg(i ORDER BY i::int)::text
+FROM regexp_split_to_table(regexp_replace($1::text,'^{|}$','','g'),',') i
+$$ LANGUAGE sql;
diff --git a/test/sql/random.sql b/test/sql/random.sql
new file mode 100644
index 0000000..7cc8f87
--- /dev/null
+++ b/test/sql/random.sql
@@ -0,0 +1,27 @@
+SELECT setseed(0.12345);
+
+\set MAX_INT 2147483647
+
+CREATE TABLE hashset_random_int4_numbers AS
+    SELECT
+        (random()*:MAX_INT)::int AS i
+    FROM generate_series(1,(random()*10000)::int)
+;
+
+SELECT
+    md5(hashset_sorted)
+FROM
+(
+    SELECT
+        hashset_sorted(int4hashset(format('{%s}',string_agg(i::text,','))))
+    FROM hashset_random_int4_numbers
+) q;
+
+SELECT
+    md5(input_sorted)
+FROM
+(
+    SELECT
+        format('{%s}',string_agg(i::text,',' ORDER BY i)) AS input_sorted
+    FROM hashset_random_int4_numbers
+) q;
diff --git a/test/sql/reported_bugs.sql b/test/sql/reported_bugs.sql
new file mode 100644
index 0000000..9166f5d
--- /dev/null
+++ b/test/sql/reported_bugs.sql
@@ -0,0 +1,85 @@
+/*
+ * Bug in hashset_add() and hashset_merge() functions altering original hashset.
+ *
+ * Previously, the hashset_add() and hashset_merge() functions were modifying the
+ * original hashset in-place, leading to unexpected results as the original data
+ * within the hashset was being altered.
+ *
+ * The issue was addressed by implementing a macro function named
+ * PG_GETARG_INT4HASHSET_COPY() within the C code. This function guarantees that
+ * a copy of the hashset is created and subsequently modified, thereby preserving
+ * the integrity of the original hashset.
+ *
+ * As a result of this fix, hashset_add() and hashset_merge() now operate on
+ * a copied hashset, ensuring that the original data remains unaltered, and
+ * the query executes correctly.
+ */
+SELECT
+    q.hashset_agg,
+    hashset_add(hashset_agg,4)
+FROM
+(
+    SELECT
+        hashset_agg(generate_series)
+    FROM generate_series(1,3)
+) q;
+
+/*
+ * Bug in hashset_hash() function with respect to element insertion order.
+ *
+ * Prior to the fix, the hashset_hash() function was accumulating the hashes
+ * of individual elements in a non-commutative manner. As a consequence, the
+ * final hash value was sensitive to the order in which elements were inserted
+ * into the hashset. This behavior led to inconsistencies, as logically
+ * equivalent sets (i.e., sets with the same elements but in different orders)
+ * produced different hash values.
+ *
+ * The bug was fixed by modifying the hashset_hash() function to use a
+ * commutative operation when combining the hashes of individual elements.
+ * This change ensures that the final hash value is independent of the
+ * element insertion order, and logically equivalent sets produce the
+ * same hash.
+ */
+SELECT hashset_hash('{1,2}'::int4hashset);
+SELECT hashset_hash('{2,1}'::int4hashset);
+
+SELECT hashset_cmp('{1,2}','{2,1}')
+UNION
+SELECT hashset_cmp('{1,2}','{1,2,1}')
+UNION
+SELECT hashset_cmp('{1,2}','{1,2}');
+
+/*
+ * Bug in int4hashset_resize() not utilizing growth_factor.
+ *
+ * The previous implementation hard-coded a growth factor of 2, neglecting
+ * the struct's growth_factor field. This bug was addressed by properly
+ * using growth_factor for new capacity calculation, with an additional
+ * safety check to prevent possible infinite loops in resizing.
+ */
+SELECT hashset_capacity(hashset_add(hashset_add(int4hashset(
+    capacity := 0,
+    load_factor := 0.75,
+    growth_factor := 1.1
+), 123), 456));
+
+SELECT hashset_capacity(hashset_add(hashset_add(int4hashset(
+    capacity := 0,
+    load_factor := 0.75,
+    growth_factor := 10
+), 123), 456));
+
+/*
+ * Bug in int4hashset_capacity() not detoasting input correctly.
+ */
+SELECT hashset_capacity(int4hashset(capacity:=10)) AS capacity_10;
+SELECT hashset_capacity(int4hashset(capacity:=1000)) AS capacity_1000;
+SELECT hashset_capacity(int4hashset(capacity:=100000)) AS capacity_100000;
+
+CREATE TABLE test_capacity_10 AS SELECT int4hashset(capacity:=10) AS capacity_10;
+CREATE TABLE test_capacity_1000 AS SELECT int4hashset(capacity:=1000) AS capacity_1000;
+CREATE TABLE test_capacity_100000 AS SELECT int4hashset(capacity:=100000) AS capacity_100000;
+
+SELECT hashset_capacity(capacity_10) AS capacity_10 FROM test_capacity_10;
+SELECT hashset_capacity(capacity_1000) AS capacity_1000 FROM test_capacity_1000;
+SELECT hashset_capacity(capacity_100000) AS capacity_100000 FROM test_capacity_100000;
diff --git a/test/sql/table.sql b/test/sql/table.sql
new file mode 100644
index 0000000..0472352
--- /dev/null
+++ b/test/sql/table.sql
@@ -0,0 +1,10 @@
+CREATE TABLE users (
+    user_id int PRIMARY KEY,
+    user_likes int4hashset DEFAULT int4hashset(capacity := 2)
+);
+INSERT INTO users (user_id) VALUES (1);
+UPDATE users SET user_likes = hashset_add(user_likes, 101) WHERE user_id = 1;
+UPDATE users SET user_likes = hashset_add(user_likes, 202) WHERE user_id = 1;
+SELECT hashset_contains(user_likes, 101) FROM users WHERE user_id = 1;
+SELECT hashset_count(user_likes) FROM users WHERE user_id = 1;
+SELECT hashset_sorted(user_likes) FROM users WHERE user_id = 1;
