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..ee62511
--- /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 array-and-multiset-semantics
+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..aaa84fd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,343 @@
+# hashset
+
+This PostgreSQL extension implements hashset, a data structure (type)
+providing a collection of unique integer items with fast lookup.
+
+It provides several functions for working with these sets, including operations
+like addition, containment check, conversion to array, union, intersection,
+difference, equality check, and cardinality calculation.
+
+`NULL` values are also allowed in the hash set, and are considered as a unique
+element. When multiple `NULL` values are present in the input, they are treated
+as a single `NULL`.
+
+## Table of Contents
+1. [Version](#version)
+2. [Data Types](#data-types)
+   - [int4hashset](#int4hashset)
+3. [Functions](#functions)
+   - [int4hashset](#int4hashset-1)
+   - [hashset_add](#hashset_add)
+   - [hashset_contains](#hashset_contains)
+   - [hashset_to_array](#hashset_to_array)
+   - [hashset_to_sorted_array](#hashset_to_sorted_array)
+   - [hashset_cardinality](#hashset_cardinality)
+   - [hashset_capacity](#hashset_capacity)
+   - [hashset_max_collisions](#hashset_max_collisions)
+   - [hashset_union](#hashset_union)
+   - [hashset_intersection](#hashset_intersection)
+   - [hashset_difference](#hashset_difference)
+   - [hashset_symmetric_difference](#hashset_symmetric_difference)
+4. [Aggregation Functions](#aggregation-functions)
+5. [Operators](#operators)
+6. [Hashset Hash Operators](#hashset-hash-operators)
+7. [Hashset Btree Operators](#hashset-btree-operators)
+8. [Limitations](#limitations)
+9. [Installation](#installation)
+10. [License](#license)
+
+## 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.
+
+
+## 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()
+
+`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()
+
+`hashset_add(int4hashset, int) -> int4hashset`
+
+Adds an integer to an int4hashset.
+
+```sql
+SELECT hashset_add(NULL, 1); -- {1}
+SELECT hashset_add('{NULL}', 1); -- {1,NULL}
+SELECT hashset_add('{1}', NULL); -- {1,NULL}
+SELECT hashset_add('{1}', 1); -- {1}
+SELECT hashset_add('{1}', 2); -- {1,2}
+```
+
+
+### hashset_contains()
+
+`hashset_contains(int4hashset, int) -> boolean`
+
+Checks if an int4hashset contains a given integer.
+
+```sql
+SELECT hashset_contains('{1}', 1); -- TRUE
+SELECT hashset_contains('{1}', 2); -- FALSE
+```
+
+If the *cardinality* of the hashset is zero (0), it is known that it doesn't
+contain any value, not even an Unknown value represented as `NULL`, so even in
+that case it returns `FALSE`.
+
+```sql
+SELECT hashset_contains('{}', 1); -- FALSE
+SELECT hashset_contains('{}', NULL); -- FALSE
+```
+
+If the hashset is `NULL`, then the result is `NULL`.
+
+```sql
+SELECT hashset_contains(NULL, NULL); -- NULL
+SELECT hashset_contains(NULL, 1); -- NULL
+```
+
+
+### hashset_to_array()
+
+`hashset_to_array(int4hashset) -> int[]`
+
+Converts an int4hashset to an array of unsorted integers.
+
+```sql
+SELECT hashset_to_array('{2,1,3}'); -- {3,2,1}
+```
+
+
+### hashset_to_sorted_array()
+
+`hashset_to_sorted_array(int4hashset) -> int[]`
+
+Converts an int4hashset to an array of sorted integers.
+
+```sql
+SELECT hashset_to_sorted_array('{2,1,3}'); -- {1,2,3}
+```
+
+If the hashset contains a `NULL` element, it follows the same behavior as the
+`ORDER BY` clause in SQL: the `NULL` element is positioned at the end of the
+sorted array.
+
+```sql
+SELECT hashset_to_sorted_array('{2,1,NULL,3}'); -- {1,2,3,NULL}
+```
+
+
+### hashset_cardinality()
+
+`hashset_cardinality(int4hashset) -> bigint`
+
+Returns the number of elements in an int4hashset.
+
+```sql
+SELECT hashset_cardinality(NULL); -- NULL
+SELECT hashset_cardinality('{}'); -- 0
+SELECT hashset_cardinality('{1}'); -- 1
+SELECT hashset_cardinality('{1,1}'); -- 1
+SELECT hashset_cardinality('{NULL,NULL}'); -- 1
+SELECT hashset_cardinality('{1,NULL}'); -- 2
+SELECT hashset_cardinality('{1,2,3}'); -- 3
+```
+
+
+### hashset_capacity()
+
+`hashset_capacity(int4hashset) -> bigint`
+
+Returns the current capacity of an int4hashset.
+
+
+### hashset_max_collisions()
+
+`hashset_max_collisions(int4hashset) -> bigint`
+
+Returns the maximum number of collisions that have occurred for a single element
+
+
+### hashset_union()
+
+`hashset_union(int4hashset, int4hashset) -> int4hashset`
+
+Merges two int4hashsets into a new int4hashset.
+
+```sql
+SELECT hashset_union('{1,2}', '{2,3}'); -- '{1,2,3}
+```
+
+If any of the operands are `NULL`, the result is `NULL`.
+
+```sql
+SELECT hashset_union('{1}', NULL); -- NULL
+SELECT hashset_union(NULL, '{1}'); -- NULL
+```
+
+
+### hashset_intersection()
+
+`hashset_intersection(int4hashset, int4hashset) -> int4hashset`
+
+Returns a new int4hashset that is the intersection of the two input sets.
+
+```sql
+SELECT hashset_intersection('{1,2}', '{2,3}'); -- {2}
+SELECT hashset_intersection('{1,2,NULL}', '{2,3,NULL}'); -- {2,NULL}
+```
+
+If any of the operands are `NULL`, the result is `NULL`.
+
+```sql
+SELECT hashset_intersection('{1,2}', NULL); -- NULL
+SELECT hashset_intersection(NULL, '{2,3}'); -- NULL
+```
+
+
+### hashset_difference()
+
+`hashset_difference(int4hashset, int4hashset) -> int4hashset`
+
+Returns a new int4hashset that contains the elements present in the first set
+but not in the second set.
+
+```sql
+SELECT hashset_difference('{1,2}', '{2,3}'); -- {1}
+SELECT hashset_difference('{1,2,NULL}', '{2,3,NULL}'); -- {1}
+SELECT hashset_difference('{1,2,NULL}', '{2,3}'); -- {1,NULL}
+```
+
+If any of the operands are `NULL`, the result is `NULL`.
+
+```sql
+SELECT hashset_difference('{1,2}', NULL); -- NULL
+SELECT hashset_difference(NULL, '{2,3}'); -- NULL
+```
+
+
+### hashset_symmetric_difference()
+
+`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.
+
+```sql
+SELECT hashset_symmetric_difference('{1,2}', '{2,3}'); -- {1,3}
+SELECT hashset_symmetric_difference('{1,2,NULL}', '{2,3,NULL}'); -- {1,3}
+SELECT hashset_symmetric_difference('{1,2,NULL}', '{2,3}'); -- {1,3,NULL}
+```
+
+If any of the operands are `NULL`, the result is `NULL`.
+
+```sql
+SELECT hashset_symmetric_difference('{1,2}', NULL); -- NULL
+SELECT hashset_symmetric_difference(NULL, '{2,3}'); -- NULL
+```
+
+
+## Aggregation Functions
+
+### hashset_agg(int4)
+
+`hashset_agg(int4) -> int4hashset`
+
+Aggregate integers into a hashset.
+
+```sql
+SELECT hashset_agg(some_int4_column) FROM some_table;
+```
+
+
+### hashset_agg(int4hashset)
+
+`hashset_agg(int4hashset) -> int4hashset`
+
+Aggregate hashsets into a hashset.
+
+```sql
+SELECT hashset_agg(some_int4hashset_column) FROM some_table;
+```
+
+
+## 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/benchmark/.gitignore b/benchmark/.gitignore
new file mode 100644
index 0000000..f3c4e1c
--- /dev/null
+++ b/benchmark/.gitignore
@@ -0,0 +1,2 @@
+soc-pokec-relationships.txt.gz
+soc-pokec-relationships.txt
diff --git a/benchmark/friends_of_friends.sh b/benchmark/friends_of_friends.sh
new file mode 100755
index 0000000..d4570c0
--- /dev/null
+++ b/benchmark/friends_of_friends.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+if [ ! -f "soc-pokec-relationships.txt" ]; then
+    wget https://snap.stanford.edu/data/soc-pokec-relationships.txt.gz
+    gunzip soc-pokec-relationships.txt.gz
+fi
+
+psql -X -c "CREATE TABLE edges (from_node INT, to_node INT);"
+psql -X -c "\COPY edges FROM soc-pokec-relationships.txt;"
+psql -X -c "ALTER TABLE edges ADD PRIMARY KEY (from_node, to_node);"
+psql -X -f friends_of_friends.sql
diff --git a/benchmark/friends_of_friends.sql b/benchmark/friends_of_friends.sql
new file mode 100644
index 0000000..ecc8f0e
--- /dev/null
+++ b/benchmark/friends_of_friends.sql
@@ -0,0 +1,72 @@
+CREATE EXTENSION IF NOT EXISTS hashset;
+
+\timing on
+
+CREATE OR REPLACE VIEW vfriends_of_friends_array_agg_distinct AS
+WITH RECURSIVE friends_of_friends AS (
+    SELECT
+        ARRAY[5867::bigint] AS current,
+        0 AS depth
+    UNION ALL
+    SELECT
+        new_current,
+        friends_of_friends.depth + 1
+    FROM
+        friends_of_friends
+    CROSS JOIN LATERAL (
+        SELECT
+            array_agg(DISTINCT edges.to_node) AS new_current
+        FROM
+            edges
+        WHERE
+            from_node = ANY(friends_of_friends.current)
+    ) q
+    WHERE
+        friends_of_friends.depth < 3
+)
+SELECT
+    COALESCE(array_length(current, 1), 0) AS count_friends_at_depth_3
+FROM
+    friends_of_friends
+WHERE
+    depth = 3;
+
+CREATE OR REPLACE VIEW vfriends_of_friends_hashset_agg AS
+WITH RECURSIVE friends_of_friends AS
+(
+    SELECT
+        '{5867}'::int4hashset AS current,
+        0 AS depth
+    UNION ALL
+    SELECT
+        new_current,
+        friends_of_friends.depth + 1
+    FROM
+        friends_of_friends
+    CROSS JOIN LATERAL
+    (
+        SELECT
+            hashset_agg(edges.to_node) AS new_current
+        FROM
+            edges
+        WHERE
+            from_node = ANY(hashset_to_array(friends_of_friends.current))
+    ) q
+    WHERE
+        friends_of_friends.depth < 3
+)
+SELECT
+    depth,
+    hashset_cardinality(current)
+FROM
+    friends_of_friends
+WHERE
+    depth = 3;
+
+SELECT * FROM vfriends_of_friends_array_agg_distinct;
+SELECT * FROM vfriends_of_friends_array_agg_distinct;
+SELECT * FROM vfriends_of_friends_array_agg_distinct;
+
+SELECT * FROM vfriends_of_friends_hashset_agg;
+SELECT * FROM vfriends_of_friends_hashset_agg;
+SELECT * FROM vfriends_of_friends_hashset_agg;
diff --git a/hashset--0.0.1.sql b/hashset--0.0.1.sql
new file mode 100644
index 0000000..d0478ce
--- /dev/null
+++ b/hashset--0.0.1.sql
@@ -0,0 +1,303 @@
+/*
+ * 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 boolean
+AS 'hashset', 'int4hashset_contains'
+LANGUAGE C IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION hashset_union(int4hashset, int4hashset)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_union'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_to_array(int4hashset)
+RETURNS int[]
+AS 'hashset', 'int4hashset_to_array'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_to_sorted_array(int4hashset)
+RETURNS int[]
+AS 'hashset', 'int4hashset_to_sorted_array'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_cardinality(int4hashset)
+RETURNS bigint
+AS 'hashset', 'int4hashset_cardinality'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_capacity(int4hashset)
+RETURNS bigint
+AS 'hashset', 'int4hashset_capacity'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_collisions(int4hashset)
+RETURNS bigint
+AS 'hashset', 'int4hashset_collisions'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_max_collisions(int4hashset)
+RETURNS bigint
+AS 'hashset', 'int4hashset_max_collisions'
+LANGUAGE C IMMUTABLE STRICT;
+
+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 STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_difference(int4hashset, int4hashset)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_difference'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_symmetric_difference(int4hashset, int4hashset)
+RETURNS int4hashset
+AS 'hashset', 'int4hashset_symmetric_difference'
+LANGUAGE C IMMUTABLE STRICT;
+
+/*
+ * 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 STRICT;
+
+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 STRICT;
+
+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_eq(int4hashset, int4hashset)
+RETURNS boolean
+AS 'hashset', 'int4hashset_eq'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR = (
+    LEFTARG = int4hashset,
+    RIGHTARG = int4hashset,
+    PROCEDURE = hashset_eq,
+    COMMUTATOR = =,
+    HASHES
+);
+
+CREATE OR REPLACE FUNCTION hashset_ne(int4hashset, int4hashset)
+RETURNS boolean
+AS 'hashset', 'int4hashset_ne'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR <> (
+    LEFTARG = int4hashset,
+    RIGHTARG = int4hashset,
+    PROCEDURE = hashset_ne,
+    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 boolean
+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..2b92363
--- /dev/null
+++ b/hashset-api.c
@@ -0,0 +1,1042 @@
+#include "hashset.h"
+
+#include <math.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_cardinality);
+PG_FUNCTION_INFO_V1(int4hashset_union);
+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_to_sorted_array);
+PG_FUNCTION_INFO_V1(int4hashset_eq);
+PG_FUNCTION_INFO_V1(int4hashset_ne);
+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_cardinality(PG_FUNCTION_ARGS);
+Datum int4hashset_union(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_to_sorted_array(PG_FUNCTION_ARGS);
+Datum int4hashset_eq(PG_FUNCTION_ARGS);
+Datum int4hashset_ne(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;
+		}
+
+		/* Check if "null" is encountered (case-insensitive) */
+		if (strncasecmp(str, "null", 4) == 0)
+		{
+			set->null_element = true;
+			str = str + 4; /* Move past "null" */
+		}
+		else
+		{
+			/* 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 number, "null" 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)
+{
+	int				i;
+	int4hashset_t  *set = PG_GETARG_INT4HASHSET(0);
+	char		   *bitmap = HASHSET_GET_BITMAP(set);
+	int32		   *values = HASHSET_GET_VALUES(set);
+	StringInfoData	str;
+
+	/* 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]);
+		}
+	}
+
+	/* Check if the null_element field is set */
+	if (set->null_element)
+	{
+		if (str.len > 1)
+			appendStringInfoChar(&str, ',');
+		appendStringInfoString(&str, "NULL");
+	}
+
+	/* 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);
+    pq_sendbyte(&buf, set->null_element ? 1 : 0);
+
+	/* 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;
+	bool			null_element;
+
+	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);
+	null_element = pq_getmsgbyte(buf) == 1;
+
+	/* 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;
+	set->null_element = null_element;
+	memcpy(set->data, binary_data, data_size);
+
+	PG_RETURN_POINTER(set);
+}
+
+Datum
+int4hashset_add(PG_FUNCTION_ARGS)
+{
+	int4hashset_t *set;
+	/* 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
+	{
+		set = PG_GETARG_INT4HASHSET_COPY(0);
+	}
+
+	if (PG_ARGISNULL(1))
+	{
+		set->null_element = true;
+	}
+	else
+	{
+		int32 element = PG_GETARG_INT32(1);
+		set = int4hashset_add_element(set, element);
+	}
+
+	PG_RETURN_POINTER(set);
+}
+
+Datum
+int4hashset_contains(PG_FUNCTION_ARGS)
+{
+	int4hashset_t  *set;
+	int32			value;
+	bool			result;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	set = PG_GETARG_INT4HASHSET(0);
+
+	if (set->nelements == 0 && !set->null_element)
+		PG_RETURN_BOOL(false);
+
+	if (PG_ARGISNULL(1))
+		PG_RETURN_NULL();
+
+	value = PG_GETARG_INT32(1);
+	result = int4hashset_contains_element(set, value);
+
+	if (!result && set->null_element)
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(result);
+}
+
+Datum
+int4hashset_cardinality(PG_FUNCTION_ARGS)
+{
+	int4hashset_t	*set = PG_GETARG_INT4HASHSET(0);
+	int64			 cardinality = set->nelements + (set->null_element ? 1 : 0);
+
+	PG_RETURN_INT64(cardinality);
+}
+
+Datum
+int4hashset_union(PG_FUNCTION_ARGS)
+{
+	int				i;
+	int4hashset_t  *seta = PG_GETARG_INT4HASHSET_COPY(0);
+	int4hashset_t  *setb = PG_GETARG_INT4HASHSET(1);
+	char		   *bitmap = HASHSET_GET_BITMAP(setb);
+	int32		   *values = HASHSET_GET_VALUES(setb);
+
+	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]);
+	}
+
+	if (!seta->null_element && setb->null_element)
+		seta->null_element = true;
+
+	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 = PG_GETARG_INT4HASHSET(0);
+
+	PG_RETURN_INT64(set->capacity);
+}
+
+Datum
+int4hashset_collisions(PG_FUNCTION_ARGS)
+{
+	int4hashset_t  *set = PG_GETARG_INT4HASHSET(0);
+
+	PG_RETURN_INT64(set->ncollisions);
+}
+
+Datum
+int4hashset_max_collisions(PG_FUNCTION_ARGS)
+{
+	int4hashset_t  *set = PG_GETARG_INT4HASHSET(0);
+
+	PG_RETURN_INT64(set->max_collisions);
+}
+
+Datum
+int4hashset_agg_add(PG_FUNCTION_ARGS)
+{
+	MemoryContext	aggcontext;
+	MemoryContext	oldcontext;
+	int4hashset_t  *state;
+
+	/* 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   aggcontext;
+	MemoryContext	oldcontext;
+	int4hashset_t  *state;
+
+	/* 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;
+		int4hashset_t  *value = PG_GETARG_INT4HASHSET(1);
+		char		   *bitmap = HASHSET_GET_BITMAP(value);
+		int32		   *values = HASHSET_GET_VALUES(value);
+
+		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)
+{
+	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 = HASHSET_GET_BITMAP(src);
+	values = HASHSET_GET_VALUES(src);
+
+	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 = PG_GETARG_INT4HASHSET(0);
+	int32			   *values;
+	int					nvalues;
+	char			   *sbitmap;
+	int32			   *svalues;
+
+	/* if hashset is empty and does not contain null, return an empty array */
+	if(set->nelements == 0 && !set->null_element)
+		PG_RETURN_ARRAYTYPE_P(construct_empty_array(INT4OID));
+
+	sbitmap = HASHSET_GET_BITMAP(set);
+	svalues = HASHSET_GET_VALUES(set);
+
+	/* 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, set->null_element);
+}
+
+Datum
+int4hashset_to_sorted_array(PG_FUNCTION_ARGS)
+{
+	int4hashset_t	   *set;
+	int32			   *values;
+	int					nvalues;
+
+	set = PG_GETARG_INT4HASHSET(0);
+
+	/* if hashset is empty and does not contain null, return an empty array */
+	if(set->nelements == 0 && !set->null_element)
+		PG_RETURN_ARRAYTYPE_P(construct_empty_array(INT4OID));
+
+	/* extract the sorted elements from the hashset */
+	values = int4hashset_extract_sorted_elements(set);
+
+	/* number of values to store in the array */
+	nvalues = set->nelements;
+
+	return int32_to_array(fcinfo, values, nvalues, set->null_element);
+}
+
+Datum
+int4hashset_eq(PG_FUNCTION_ARGS)
+{
+	int				i;
+	int4hashset_t  *a = PG_GETARG_INT4HASHSET(0);
+	int4hashset_t  *b = PG_GETARG_INT4HASHSET(1);
+	char		   *bitmap_a;
+	int32		   *values_a;
+
+	/*
+	 * Check if the number of elements is the same
+	 */
+	if (a->nelements != b->nelements)
+		PG_RETURN_BOOL(false);
+
+	bitmap_a = HASHSET_GET_BITMAP(a);
+	values_a = HASHSET_GET_VALUES(a);
+
+	/*
+	 * 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);
+		}
+	}
+
+	if (a->null_element != b->null_element)
+		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_ne(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_eq, 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 = PG_GETARG_INT4HASHSET(0);
+	int4hashset_t  *setb = PG_GETARG_INT4HASHSET(1);
+	char		   *bitmap = HASHSET_GET_BITMAP(setb);
+	int32		   *values = HASHSET_GET_VALUES(setb);
+
+	int4hashset_t  *intersection;
+
+	intersection = int4hashset_allocate(
+		seta->capacity,
+		DEFAULT_LOAD_FACTOR,
+		DEFAULT_GROWTH_FACTOR,
+		DEFAULT_HASHFN_ID
+	);
+
+	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]);
+		}
+	}
+
+	if (seta->null_element && setb->null_element)
+		intersection->null_element = true;
+
+	PG_RETURN_POINTER(intersection);
+}
+
+Datum
+int4hashset_difference(PG_FUNCTION_ARGS)
+{
+	int				i;
+	int4hashset_t	*seta = PG_GETARG_INT4HASHSET(0);
+	int4hashset_t	*setb = PG_GETARG_INT4HASHSET(1);
+	int4hashset_t	*difference;
+	char			*bitmap = HASHSET_GET_BITMAP(seta);
+	int32			*values = HASHSET_GET_VALUES(seta);
+
+	difference = int4hashset_allocate(
+		seta->capacity,
+		DEFAULT_LOAD_FACTOR,
+		DEFAULT_GROWTH_FACTOR,
+		DEFAULT_HASHFN_ID
+	);
+
+	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]);
+		}
+	}
+
+	if (seta->null_element && !setb->null_element)
+		difference->null_element = true;
+
+	PG_RETURN_POINTER(difference);
+}
+
+Datum
+int4hashset_symmetric_difference(PG_FUNCTION_ARGS)
+{
+	int				i;
+	int4hashset_t  *seta = PG_GETARG_INT4HASHSET(0);
+	int4hashset_t  *setb = PG_GETARG_INT4HASHSET(1);
+	int4hashset_t  *result;
+	char		   *bitmapa = HASHSET_GET_BITMAP(seta);
+	char		   *bitmapb = HASHSET_GET_BITMAP(setb);
+	int32		   *valuesa = HASHSET_GET_VALUES(seta);
+	int32		   *valuesb = HASHSET_GET_VALUES(setb);
+
+	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);
+		}
+	}
+
+	if (seta->null_element ^ setb->null_element)
+		result->null_element = true;
+
+	PG_RETURN_POINTER(result);
+}
diff --git a/hashset.c b/hashset.c
new file mode 100644
index 0000000..65ab25f
--- /dev/null
+++ b/hashset.c
@@ -0,0 +1,327 @@
+/*
+ * 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->null_element = false; /* No null element initially */
+
+	set->flags |= 0;
+
+	return set;
+}
+
+int4hashset_t *
+int4hashset_resize(int4hashset_t * set)
+{
+	int				i;
+	int4hashset_t  *new;
+	char		   *bitmap = HASHSET_GET_BITMAP(set);
+	int32		   *values = HASHSET_GET_VALUES(set);
+	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
+	);
+
+	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 = HASHSET_GET_BITMAP(set);
+	values = HASHSET_GET_VALUES(set);
+
+	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 = HASHSET_GET_BITMAP(set);
+	int32  *values = HASHSET_GET_VALUES(set);
+	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;
+
+	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)
+{
+	int32  *elements = palloc(set->nelements * sizeof(int32));
+	char   *bitmap = HASHSET_GET_BITMAP(set);
+	int32  *values = HASHSET_GET_VALUES(set);
+	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, bool null_element)
+{
+	ArrayBuildState *astate = NULL;
+	int		 i;
+
+	for (i = 0; i < len; i++)
+	{
+		/* Stash away this field */
+		astate = accumArrayResult(astate,
+								  Int32GetDatum(d[i]),
+								  false,
+								  INT4OID,
+								  CurrentMemoryContext);
+	}
+
+	if (null_element)
+	{
+		astate = accumArrayResult(astate,
+								  (Datum) 0,
+								  true,
+								  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..3631e22
--- /dev/null
+++ b/hashset.h
@@ -0,0 +1,56 @@
+#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_GET_BITMAP(set) ((set)->data)
+#define HASHSET_GET_VALUES(set) ((int32 *) ((set)->data + CEIL_DIV((set)->capacity, 8)))
+#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 */
+	bool		null_element;	/* Indicates if null is present in 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, bool null_element);
+
+#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/array-and-multiset-semantics.out b/test/expected/array-and-multiset-semantics.out
new file mode 100644
index 0000000..8f989a1
--- /dev/null
+++ b/test/expected/array-and-multiset-semantics.out
@@ -0,0 +1,365 @@
+CREATE OR REPLACE FUNCTION array_union(int4[], int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            $1 IS NULL OR $2 IS NULL
+        THEN
+            NULL
+        ELSE
+        COALESCE((
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM
+            (
+                SELECT unnest($1)
+                UNION
+                SELECT unnest($2)
+            ) q
+        ),'{}'::int4[])
+    END
+$$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION array_intersection(int4[], int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            $1 IS NULL OR $2 IS NULL
+        THEN
+            NULL
+        ELSE
+        COALESCE((
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM
+            (
+                SELECT unnest($1)
+                INTERSECT
+                SELECT unnest($2)
+            ) q
+        ),'{}'::int4[])
+    END
+$$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION array_difference(int4[], int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            $1 IS NULL OR $2 IS NULL
+        THEN
+            NULL
+        ELSE
+        COALESCE((
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM
+            (
+                SELECT unnest($1)
+                EXCEPT
+                SELECT unnest($2)
+            ) q
+        ),'{}'::int4[])
+    END
+$$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION array_symmetric_difference(int4[], int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            $1 IS NULL OR $2 IS NULL
+        THEN
+            NULL
+        ELSE
+        COALESCE((
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM
+            (
+                SELECT
+                    *
+                FROM
+                (
+                    SELECT unnest($1)
+                    UNION
+                    SELECT unnest($2)
+                ) AS q1
+                EXCEPT
+                SELECT
+                    *
+                FROM
+                (
+                    SELECT unnest($1)
+                    INTERSECT
+                    SELECT unnest($2)
+                ) AS q2
+            ) AS q3
+        ),'{}'::int4[])
+    END
+$$ LANGUAGE sql;
+CREATE OR REPLACE FUNCTION array_sort_distinct(int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            cardinality($1) = 0
+        THEN
+            '{}'::int4[]
+        ELSE
+        (
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM unnest($1)
+        )
+    END
+$$ LANGUAGE sql;
+DROP TABLE IF EXISTS hashset_test_results_1;
+NOTICE:  table "hashset_test_results_1" does not exist, skipping
+CREATE TABLE hashset_test_results_1 AS
+SELECT
+    arg1,
+    arg2,
+    hashset_add(arg1::int4hashset, arg2),
+    array_append(arg1::int4[], arg2),
+    hashset_contains(arg1::int4hashset, arg2),
+    arg2 = ANY(arg1::int4[]) AS "= ANY(...)"
+FROM (VALUES (NULL), ('{}'), ('{NULL}'), ('{1}'), ('{2}'), ('{1,2}'), ('{2,3}')) AS a(arg1)
+CROSS JOIN (VALUES (NULL::int4), (1::int4), (4::int4)) AS b(arg2);
+DROP TABLE IF EXISTS hashset_test_results_2;
+NOTICE:  table "hashset_test_results_2" does not exist, skipping
+CREATE TABLE hashset_test_results_2 AS
+SELECT
+    arg1,
+    arg2,
+    hashset_union(arg1::int4hashset, arg2::int4hashset),
+    array_union(arg1::int4[], arg2::int4[]),
+    hashset_intersection(arg1::int4hashset, arg2::int4hashset),
+    array_intersection(arg1::int4[], arg2::int4[]),
+    hashset_difference(arg1::int4hashset, arg2::int4hashset),
+    array_difference(arg1::int4[], arg2::int4[]),
+    hashset_symmetric_difference(arg1::int4hashset, arg2::int4hashset),
+    array_symmetric_difference(arg1::int4[], arg2::int4[]),
+    hashset_eq(arg1::int4hashset, arg2::int4hashset),
+    array_eq(arg1::int4[], arg2::int4[]),
+    hashset_ne(arg1::int4hashset, arg2::int4hashset),
+    array_ne(arg1::int4[], arg2::int4[])
+FROM (VALUES (NULL), ('{}'), ('{NULL}'), ('{1}'), ('{1,NULL}'), ('{2}'), ('{1,2}'), ('{2,3}')) AS a(arg1)
+CROSS JOIN (VALUES (NULL), ('{}'), ('{NULL}'), ('{1}'), ('{1,NULL}'), ('{2}'), ('{1,2}'), ('{2,3}')) AS b(arg2);
+DROP TABLE IF EXISTS hashset_test_results_3;
+NOTICE:  table "hashset_test_results_3" does not exist, skipping
+CREATE TABLE hashset_test_results_3 AS
+SELECT
+    arg1,
+    hashset_cardinality(arg1::int4hashset),
+    cardinality(arg1::int4[])
+FROM (VALUES (NULL), ('{}'), ('{NULL}'), ('{1}'), ('{2}'), ('{1,2}'), ('{2,3}')) AS a(arg1);
+SELECT * FROM hashset_test_results_1;
+  arg1  | arg2 | hashset_add | array_append | hashset_contains | = ANY(...) 
+--------+------+-------------+--------------+------------------+------------
+        |      | {NULL}      | {NULL}       |                  | 
+        |    1 | {1}         | {1}          |                  | 
+        |    4 | {4}         | {4}          |                  | 
+ {}     |      | {NULL}      | {NULL}       | f                | f
+ {}     |    1 | {1}         | {1}          | f                | f
+ {}     |    4 | {4}         | {4}          | f                | f
+ {NULL} |      | {NULL}      | {NULL,NULL}  |                  | 
+ {NULL} |    1 | {1,NULL}    | {NULL,1}     |                  | 
+ {NULL} |    4 | {4,NULL}    | {NULL,4}     |                  | 
+ {1}    |      | {1,NULL}    | {1,NULL}     |                  | 
+ {1}    |    1 | {1}         | {1,1}        | t                | t
+ {1}    |    4 | {1,4}       | {1,4}        | f                | f
+ {2}    |      | {2,NULL}    | {2,NULL}     |                  | 
+ {2}    |    1 | {2,1}       | {2,1}        | f                | f
+ {2}    |    4 | {2,4}       | {2,4}        | f                | f
+ {1,2}  |      | {1,2,NULL}  | {1,2,NULL}   |                  | 
+ {1,2}  |    1 | {1,2}       | {1,2,1}      | t                | t
+ {1,2}  |    4 | {4,1,2}     | {1,2,4}      | f                | f
+ {2,3}  |      | {2,3,NULL}  | {2,3,NULL}   |                  | 
+ {2,3}  |    1 | {1,2,3}     | {2,3,1}      | f                | f
+ {2,3}  |    4 | {4,2,3}     | {2,3,4}      | f                | f
+(21 rows)
+
+SELECT * FROM hashset_test_results_2;
+   arg1   |   arg2   | hashset_union | array_union  | hashset_intersection | array_intersection | hashset_difference | array_difference | hashset_symmetric_difference | array_symmetric_difference | hashset_eq | array_eq | hashset_ne | array_ne 
+----------+----------+---------------+--------------+----------------------+--------------------+--------------------+------------------+------------------------------+----------------------------+------------+----------+------------+----------
+          |          |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+          | {}       |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+          | {NULL}   |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+          | {1}      |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+          | {1,NULL} |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+          | {2}      |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+          | {1,2}    |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+          | {2,3}    |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+ {}       |          |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+ {}       | {}       | {}            | {}           | {}                   | {}                 | {}                 | {}               | {}                           | {}                         | t          | t        | f          | f
+ {}       | {NULL}   | {NULL}        | {NULL}       | {}                   | {}                 | {}                 | {}               | {NULL}                       | {NULL}                     | f          | f        | t          | t
+ {}       | {1}      | {1}           | {1}          | {}                   | {}                 | {}                 | {}               | {1}                          | {1}                        | f          | f        | t          | t
+ {}       | {1,NULL} | {1,NULL}      | {1,NULL}     | {}                   | {}                 | {}                 | {}               | {1,NULL}                     | {1,NULL}                   | f          | f        | t          | t
+ {}       | {2}      | {2}           | {2}          | {}                   | {}                 | {}                 | {}               | {2}                          | {2}                        | f          | f        | t          | t
+ {}       | {1,2}    | {1,2}         | {1,2}        | {}                   | {}                 | {}                 | {}               | {1,2}                        | {1,2}                      | f          | f        | t          | t
+ {}       | {2,3}    | {2,3}         | {2,3}        | {}                   | {}                 | {}                 | {}               | {2,3}                        | {2,3}                      | f          | f        | t          | t
+ {NULL}   |          |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+ {NULL}   | {}       | {NULL}        | {NULL}       | {}                   | {}                 | {NULL}             | {NULL}           | {NULL}                       | {NULL}                     | f          | f        | t          | t
+ {NULL}   | {NULL}   | {NULL}        | {NULL}       | {NULL}               | {NULL}             | {}                 | {}               | {}                           | {}                         | t          | t        | f          | f
+ {NULL}   | {1}      | {1,NULL}      | {1,NULL}     | {}                   | {}                 | {NULL}             | {NULL}           | {1,NULL}                     | {1,NULL}                   | f          | f        | t          | t
+ {NULL}   | {1,NULL} | {1,NULL}      | {1,NULL}     | {NULL}               | {NULL}             | {}                 | {}               | {1}                          | {1}                        | f          | f        | t          | t
+ {NULL}   | {2}      | {2,NULL}      | {2,NULL}     | {}                   | {}                 | {NULL}             | {NULL}           | {2,NULL}                     | {2,NULL}                   | f          | f        | t          | t
+ {NULL}   | {1,2}    | {2,1,NULL}    | {1,2,NULL}   | {}                   | {}                 | {NULL}             | {NULL}           | {1,2,NULL}                   | {1,2,NULL}                 | f          | f        | t          | t
+ {NULL}   | {2,3}    | {3,2,NULL}    | {2,3,NULL}   | {}                   | {}                 | {NULL}             | {NULL}           | {2,3,NULL}                   | {2,3,NULL}                 | f          | f        | t          | t
+ {1}      |          |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+ {1}      | {}       | {1}           | {1}          | {}                   | {}                 | {1}                | {1}              | {1}                          | {1}                        | f          | f        | t          | t
+ {1}      | {NULL}   | {1,NULL}      | {1,NULL}     | {}                   | {}                 | {1}                | {1}              | {1,NULL}                     | {1,NULL}                   | f          | f        | t          | t
+ {1}      | {1}      | {1}           | {1}          | {1}                  | {1}                | {}                 | {}               | {}                           | {}                         | t          | t        | f          | f
+ {1}      | {1,NULL} | {1,NULL}      | {1,NULL}     | {1}                  | {1}                | {}                 | {}               | {NULL}                       | {NULL}                     | f          | f        | t          | t
+ {1}      | {2}      | {1,2}         | {1,2}        | {}                   | {}                 | {1}                | {1}              | {1,2}                        | {1,2}                      | f          | f        | t          | t
+ {1}      | {1,2}    | {1,2}         | {1,2}        | {1}                  | {1}                | {}                 | {}               | {2}                          | {2}                        | f          | f        | t          | t
+ {1}      | {2,3}    | {3,1,2}       | {1,2,3}      | {}                   | {}                 | {1}                | {1}              | {3,2,1}                      | {1,2,3}                    | f          | f        | t          | t
+ {1,NULL} |          |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+ {1,NULL} | {}       | {1,NULL}      | {1,NULL}     | {}                   | {}                 | {1,NULL}           | {1,NULL}         | {1,NULL}                     | {1,NULL}                   | f          | f        | t          | t
+ {1,NULL} | {NULL}   | {1,NULL}      | {1,NULL}     | {NULL}               | {NULL}             | {1}                | {1}              | {1}                          | {1}                        | f          | f        | t          | t
+ {1,NULL} | {1}      | {1,NULL}      | {1,NULL}     | {1}                  | {1}                | {NULL}             | {NULL}           | {NULL}                       | {NULL}                     | f          | f        | t          | t
+ {1,NULL} | {1,NULL} | {1,NULL}      | {1,NULL}     | {1,NULL}             | {1,NULL}           | {}                 | {}               | {}                           | {}                         | t          | t        | f          | f
+ {1,NULL} | {2}      | {1,2,NULL}    | {1,2,NULL}   | {}                   | {}                 | {1,NULL}           | {1,NULL}         | {1,2,NULL}                   | {1,2,NULL}                 | f          | f        | t          | t
+ {1,NULL} | {1,2}    | {1,2,NULL}    | {1,2,NULL}   | {1}                  | {1}                | {NULL}             | {NULL}           | {2,NULL}                     | {2,NULL}                   | f          | f        | t          | t
+ {1,NULL} | {2,3}    | {3,1,2,NULL}  | {1,2,3,NULL} | {}                   | {}                 | {1,NULL}           | {1,NULL}         | {3,2,1,NULL}                 | {1,2,3,NULL}               | f          | f        | t          | t
+ {2}      |          |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+ {2}      | {}       | {2}           | {2}          | {}                   | {}                 | {2}                | {2}              | {2}                          | {2}                        | f          | f        | t          | t
+ {2}      | {NULL}   | {2,NULL}      | {2,NULL}     | {}                   | {}                 | {2}                | {2}              | {2,NULL}                     | {2,NULL}                   | f          | f        | t          | t
+ {2}      | {1}      | {2,1}         | {1,2}        | {}                   | {}                 | {2}                | {2}              | {2,1}                        | {1,2}                      | f          | f        | t          | t
+ {2}      | {1,NULL} | {2,1,NULL}    | {1,2,NULL}   | {}                   | {}                 | {2}                | {2}              | {2,1,NULL}                   | {1,2,NULL}                 | f          | f        | t          | t
+ {2}      | {2}      | {2}           | {2}          | {2}                  | {2}                | {}                 | {}               | {}                           | {}                         | t          | t        | f          | f
+ {2}      | {1,2}    | {2,1}         | {1,2}        | {2}                  | {2}                | {}                 | {}               | {1}                          | {1}                        | f          | f        | t          | t
+ {2}      | {2,3}    | {2,3}         | {2,3}        | {2}                  | {2}                | {}                 | {}               | {3}                          | {3}                        | f          | f        | t          | t
+ {1,2}    |          |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+ {1,2}    | {}       | {1,2}         | {1,2}        | {}                   | {}                 | {1,2}              | {1,2}            | {1,2}                        | {1,2}                      | f          | f        | t          | t
+ {1,2}    | {NULL}   | {1,2,NULL}    | {1,2,NULL}   | {}                   | {}                 | {1,2}              | {1,2}            | {1,2,NULL}                   | {1,2,NULL}                 | f          | f        | t          | t
+ {1,2}    | {1}      | {1,2}         | {1,2}        | {1}                  | {1}                | {2}                | {2}              | {2}                          | {2}                        | f          | f        | t          | t
+ {1,2}    | {1,NULL} | {1,2,NULL}    | {1,2,NULL}   | {1}                  | {1}                | {2}                | {2}              | {2,NULL}                     | {2,NULL}                   | f          | f        | t          | t
+ {1,2}    | {2}      | {1,2}         | {1,2}        | {2}                  | {2}                | {1}                | {1}              | {1}                          | {1}                        | f          | f        | t          | t
+ {1,2}    | {1,2}    | {1,2}         | {1,2}        | {1,2}                | {1,2}              | {}                 | {}               | {}                           | {}                         | t          | t        | f          | f
+ {1,2}    | {2,3}    | {3,1,2}       | {1,2,3}      | {2}                  | {2}                | {1}                | {1}              | {1,3}                        | {1,3}                      | f          | f        | t          | t
+ {2,3}    |          |               |              |                      |                    |                    |                  |                              |                            |            |          |            | 
+ {2,3}    | {}       | {2,3}         | {2,3}        | {}                   | {}                 | {2,3}              | {2,3}            | {2,3}                        | {2,3}                      | f          | f        | t          | t
+ {2,3}    | {NULL}   | {2,3,NULL}    | {2,3,NULL}   | {}                   | {}                 | {2,3}              | {2,3}            | {2,3,NULL}                   | {2,3,NULL}                 | f          | f        | t          | t
+ {2,3}    | {1}      | {1,2,3}       | {1,2,3}      | {}                   | {}                 | {2,3}              | {2,3}            | {3,2,1}                      | {1,2,3}                    | f          | f        | t          | t
+ {2,3}    | {1,NULL} | {1,2,3,NULL}  | {1,2,3,NULL} | {}                   | {}                 | {2,3}              | {2,3}            | {3,2,1,NULL}                 | {1,2,3,NULL}               | f          | f        | t          | t
+ {2,3}    | {2}      | {2,3}         | {2,3}        | {2}                  | {2}                | {3}                | {3}              | {3}                          | {3}                        | f          | f        | t          | t
+ {2,3}    | {1,2}    | {1,2,3}       | {1,2,3}      | {2}                  | {2}                | {3}                | {3}              | {1,3}                        | {1,3}                      | f          | f        | t          | t
+ {2,3}    | {2,3}    | {2,3}         | {2,3}        | {2,3}                | {2,3}              | {}                 | {}               | {}                           | {}                         | t          | t        | f          | f
+(64 rows)
+
+SELECT * FROM hashset_test_results_3;
+  arg1  | hashset_cardinality | cardinality 
+--------+---------------------+-------------
+        |                     |            
+ {}     |                   0 |           0
+ {NULL} |                   1 |           1
+ {1}    |                   1 |           1
+ {2}    |                   1 |           1
+ {1,2}  |                   2 |           2
+ {2,3}  |                   2 |           2
+(7 rows)
+
+/*
+ * The queries below should not return any rows since the hashset
+ * semantics should be identical to array semantics, given the array elements
+ * are distinct and both are compared as sorted arrays.
+ */
+\echo *** Testing: hashset_add()
+*** Testing: hashset_add()
+SELECT * FROM hashset_test_results_1
+WHERE
+    hashset_to_sorted_array(hashset_add)
+IS DISTINCT FROM
+    array_sort_distinct(array_append);
+ arg1 | arg2 | hashset_add | array_append | hashset_contains | = ANY(...) 
+------+------+-------------+--------------+------------------+------------
+(0 rows)
+
+\echo *** Testing: hashset_contains()
+*** Testing: hashset_contains()
+SELECT * FROM hashset_test_results_1
+WHERE
+    hashset_contains
+IS DISTINCT FROM
+    "= ANY(...)";
+ arg1 | arg2 | hashset_add | array_append | hashset_contains | = ANY(...) 
+------+------+-------------+--------------+------------------+------------
+(0 rows)
+
+\echo *** Testing: hashset_union()
+*** Testing: hashset_union()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_to_sorted_array(hashset_union)
+IS DISTINCT FROM
+    array_sort_distinct(array_union);
+ arg1 | arg2 | hashset_union | array_union | hashset_intersection | array_intersection | hashset_difference | array_difference | hashset_symmetric_difference | array_symmetric_difference | hashset_eq | array_eq | hashset_ne | array_ne 
+------+------+---------------+-------------+----------------------+--------------------+--------------------+------------------+------------------------------+----------------------------+------------+----------+------------+----------
+(0 rows)
+
+\echo *** Testing: hashset_intersection()
+*** Testing: hashset_intersection()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_to_sorted_array(hashset_intersection)
+IS DISTINCT FROM
+    array_sort_distinct(array_intersection);
+ arg1 | arg2 | hashset_union | array_union | hashset_intersection | array_intersection | hashset_difference | array_difference | hashset_symmetric_difference | array_symmetric_difference | hashset_eq | array_eq | hashset_ne | array_ne 
+------+------+---------------+-------------+----------------------+--------------------+--------------------+------------------+------------------------------+----------------------------+------------+----------+------------+----------
+(0 rows)
+
+\echo *** Testing: hashset_difference()
+*** Testing: hashset_difference()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_to_sorted_array(hashset_difference)
+IS DISTINCT FROM
+    array_sort_distinct(array_difference);
+ arg1 | arg2 | hashset_union | array_union | hashset_intersection | array_intersection | hashset_difference | array_difference | hashset_symmetric_difference | array_symmetric_difference | hashset_eq | array_eq | hashset_ne | array_ne 
+------+------+---------------+-------------+----------------------+--------------------+--------------------+------------------+------------------------------+----------------------------+------------+----------+------------+----------
+(0 rows)
+
+\echo *** Testing: hashset_symmetric_difference()
+*** Testing: hashset_symmetric_difference()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_to_sorted_array(hashset_symmetric_difference)
+IS DISTINCT FROM
+    array_sort_distinct(array_symmetric_difference);
+ arg1 | arg2 | hashset_union | array_union | hashset_intersection | array_intersection | hashset_difference | array_difference | hashset_symmetric_difference | array_symmetric_difference | hashset_eq | array_eq | hashset_ne | array_ne 
+------+------+---------------+-------------+----------------------+--------------------+--------------------+------------------+------------------------------+----------------------------+------------+----------+------------+----------
+(0 rows)
+
+\echo *** Testing: hashset_eq()
+*** Testing: hashset_eq()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_eq
+IS DISTINCT FROM
+    array_eq;
+ arg1 | arg2 | hashset_union | array_union | hashset_intersection | array_intersection | hashset_difference | array_difference | hashset_symmetric_difference | array_symmetric_difference | hashset_eq | array_eq | hashset_ne | array_ne 
+------+------+---------------+-------------+----------------------+--------------------+--------------------+------------------+------------------------------+----------------------------+------------+----------+------------+----------
+(0 rows)
+
+\echo *** Testing: hashset_ne()
+*** Testing: hashset_ne()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_ne
+IS DISTINCT FROM
+    array_ne;
+ arg1 | arg2 | hashset_union | array_union | hashset_intersection | array_intersection | hashset_difference | array_difference | hashset_symmetric_difference | array_symmetric_difference | hashset_eq | array_eq | hashset_ne | array_ne 
+------+------+---------------+-------------+----------------------+--------------------+--------------------+------------------+------------------------------+----------------------------+------------+----------+------------+----------
+(0 rows)
+
+\echo *** Testing: hashset_cardinality()
+*** Testing: hashset_cardinality()
+SELECT * FROM hashset_test_results_3
+WHERE
+    hashset_cardinality
+IS DISTINCT FROM
+    cardinality;
+ arg1 | hashset_cardinality | cardinality 
+------+---------------------+-------------
+(0 rows)
+
diff --git a/test/expected/basic.out b/test/expected/basic.out
new file mode 100644
index 0000000..79c3230
--- /dev/null
+++ b/test/expected/basic.out
@@ -0,0 +1,298 @@
+/*
+ * 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('{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_union('{1,2}'::int4hashset, '{2,3}'::int4hashset);
+ hashset_union 
+---------------
+ {3,1,2}
+(1 row)
+
+SELECT hashset_to_array('{1,2,3}'::int4hashset);
+ hashset_to_array 
+------------------
+ {3,2,1}
+(1 row)
+
+SELECT hashset_cardinality('{1,2,3}'::int4hashset); -- 3
+ hashset_cardinality 
+---------------------
+                   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..03cc7c3
--- /dev/null
+++ b/test/expected/reported_bugs.out
@@ -0,0 +1,138 @@
+/*
+ * Bug in hashset_add() and hashset_union() functions altering original hashset.
+ *
+ * Previously, the hashset_add() and hashset_union() 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_union() 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..f59494e
--- /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_cardinality(user_likes) FROM users WHERE user_id = 1;
+ hashset_cardinality 
+---------------------
+                   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/array-and-multiset-semantics.sql b/test/sql/array-and-multiset-semantics.sql
new file mode 100644
index 0000000..0db7065
--- /dev/null
+++ b/test/sql/array-and-multiset-semantics.sql
@@ -0,0 +1,232 @@
+CREATE OR REPLACE FUNCTION array_union(int4[], int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            $1 IS NULL OR $2 IS NULL
+        THEN
+            NULL
+        ELSE
+        COALESCE((
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM
+            (
+                SELECT unnest($1)
+                UNION
+                SELECT unnest($2)
+            ) q
+        ),'{}'::int4[])
+    END
+$$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION array_intersection(int4[], int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            $1 IS NULL OR $2 IS NULL
+        THEN
+            NULL
+        ELSE
+        COALESCE((
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM
+            (
+                SELECT unnest($1)
+                INTERSECT
+                SELECT unnest($2)
+            ) q
+        ),'{}'::int4[])
+    END
+$$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION array_difference(int4[], int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            $1 IS NULL OR $2 IS NULL
+        THEN
+            NULL
+        ELSE
+        COALESCE((
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM
+            (
+                SELECT unnest($1)
+                EXCEPT
+                SELECT unnest($2)
+            ) q
+        ),'{}'::int4[])
+    END
+$$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION array_symmetric_difference(int4[], int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            $1 IS NULL OR $2 IS NULL
+        THEN
+            NULL
+        ELSE
+        COALESCE((
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM
+            (
+                SELECT
+                    *
+                FROM
+                (
+                    SELECT unnest($1)
+                    UNION
+                    SELECT unnest($2)
+                ) AS q1
+                EXCEPT
+                SELECT
+                    *
+                FROM
+                (
+                    SELECT unnest($1)
+                    INTERSECT
+                    SELECT unnest($2)
+                ) AS q2
+            ) AS q3
+        ),'{}'::int4[])
+    END
+$$ LANGUAGE sql;
+
+CREATE OR REPLACE FUNCTION array_sort_distinct(int4[])
+RETURNS int4[]
+AS
+$$
+SELECT
+    CASE
+        WHEN
+            cardinality($1) = 0
+        THEN
+            '{}'::int4[]
+        ELSE
+        (
+            SELECT array_agg(DISTINCT unnest ORDER BY unnest) FROM unnest($1)
+        )
+    END
+$$ LANGUAGE sql;
+
+DROP TABLE IF EXISTS hashset_test_results_1;
+CREATE TABLE hashset_test_results_1 AS
+SELECT
+    arg1,
+    arg2,
+    hashset_add(arg1::int4hashset, arg2),
+    array_append(arg1::int4[], arg2),
+    hashset_contains(arg1::int4hashset, arg2),
+    arg2 = ANY(arg1::int4[]) AS "= ANY(...)"
+FROM (VALUES (NULL), ('{}'), ('{NULL}'), ('{1}'), ('{2}'), ('{1,2}'), ('{2,3}')) AS a(arg1)
+CROSS JOIN (VALUES (NULL::int4), (1::int4), (4::int4)) AS b(arg2);
+
+
+DROP TABLE IF EXISTS hashset_test_results_2;
+CREATE TABLE hashset_test_results_2 AS
+SELECT
+    arg1,
+    arg2,
+    hashset_union(arg1::int4hashset, arg2::int4hashset),
+    array_union(arg1::int4[], arg2::int4[]),
+    hashset_intersection(arg1::int4hashset, arg2::int4hashset),
+    array_intersection(arg1::int4[], arg2::int4[]),
+    hashset_difference(arg1::int4hashset, arg2::int4hashset),
+    array_difference(arg1::int4[], arg2::int4[]),
+    hashset_symmetric_difference(arg1::int4hashset, arg2::int4hashset),
+    array_symmetric_difference(arg1::int4[], arg2::int4[]),
+    hashset_eq(arg1::int4hashset, arg2::int4hashset),
+    array_eq(arg1::int4[], arg2::int4[]),
+    hashset_ne(arg1::int4hashset, arg2::int4hashset),
+    array_ne(arg1::int4[], arg2::int4[])
+FROM (VALUES (NULL), ('{}'), ('{NULL}'), ('{1}'), ('{1,NULL}'), ('{2}'), ('{1,2}'), ('{2,3}')) AS a(arg1)
+CROSS JOIN (VALUES (NULL), ('{}'), ('{NULL}'), ('{1}'), ('{1,NULL}'), ('{2}'), ('{1,2}'), ('{2,3}')) AS b(arg2);
+
+DROP TABLE IF EXISTS hashset_test_results_3;
+CREATE TABLE hashset_test_results_3 AS
+SELECT
+    arg1,
+    hashset_cardinality(arg1::int4hashset),
+    cardinality(arg1::int4[])
+FROM (VALUES (NULL), ('{}'), ('{NULL}'), ('{1}'), ('{2}'), ('{1,2}'), ('{2,3}')) AS a(arg1);
+
+SELECT * FROM hashset_test_results_1;
+SELECT * FROM hashset_test_results_2;
+SELECT * FROM hashset_test_results_3;
+
+/*
+ * The queries below should not return any rows since the hashset
+ * semantics should be identical to array semantics, given the array elements
+ * are distinct and both are compared as sorted arrays.
+ */
+
+\echo *** Testing: hashset_add()
+SELECT * FROM hashset_test_results_1
+WHERE
+    hashset_to_sorted_array(hashset_add)
+IS DISTINCT FROM
+    array_sort_distinct(array_append);
+
+\echo *** Testing: hashset_contains()
+SELECT * FROM hashset_test_results_1
+WHERE
+    hashset_contains
+IS DISTINCT FROM
+    "= ANY(...)";
+
+\echo *** Testing: hashset_union()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_to_sorted_array(hashset_union)
+IS DISTINCT FROM
+    array_sort_distinct(array_union);
+
+\echo *** Testing: hashset_intersection()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_to_sorted_array(hashset_intersection)
+IS DISTINCT FROM
+    array_sort_distinct(array_intersection);
+
+\echo *** Testing: hashset_difference()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_to_sorted_array(hashset_difference)
+IS DISTINCT FROM
+    array_sort_distinct(array_difference);
+
+\echo *** Testing: hashset_symmetric_difference()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_to_sorted_array(hashset_symmetric_difference)
+IS DISTINCT FROM
+    array_sort_distinct(array_symmetric_difference);
+
+\echo *** Testing: hashset_eq()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_eq
+IS DISTINCT FROM
+    array_eq;
+
+\echo *** Testing: hashset_ne()
+SELECT * FROM hashset_test_results_2
+WHERE
+    hashset_ne
+IS DISTINCT FROM
+    array_ne;
+
+\echo *** Testing: hashset_cardinality()
+SELECT * FROM hashset_test_results_3
+WHERE
+    hashset_cardinality
+IS DISTINCT FROM
+    cardinality;
diff --git a/test/sql/basic.sql b/test/sql/basic.sql
new file mode 100644
index 0000000..2bf5893
--- /dev/null
+++ b/test/sql/basic.sql
@@ -0,0 +1,107 @@
+/*
+ * 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('{123}'::int4hashset, 456);
+SELECT hashset_contains('{123,456}'::int4hashset, 456); -- true
+SELECT hashset_contains('{123,456}'::int4hashset, 789); -- false
+SELECT hashset_union('{1,2}'::int4hashset, '{2,3}'::int4hashset);
+SELECT hashset_to_array('{1,2,3}'::int4hashset);
+SELECT hashset_cardinality('{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..e7a53f1
--- /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_cardinality(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_cardinality: %', hashset_cardinality(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_cardinality: %', hashset_cardinality(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_cardinality: %', hashset_cardinality(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_cardinality: %', hashset_cardinality(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_cardinality: %', hashset_cardinality(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_cardinality: %', hashset_cardinality(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..9e6b617
--- /dev/null
+++ b/test/sql/reported_bugs.sql
@@ -0,0 +1,85 @@
+/*
+ * Bug in hashset_add() and hashset_union() functions altering original hashset.
+ *
+ * Previously, the hashset_add() and hashset_union() 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_union() 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..bf05ffa
--- /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_cardinality(user_likes) FROM users WHERE user_id = 1;
+SELECT hashset_sorted(user_likes) FROM users WHERE user_id = 1;
