commit da3b0242f3a11fc351e5eeb53883cdffe15f55d9
Author: Joel Jakobsson <joel@compiler.org>
Date:   Tue Jun 13 20:12:15 2023 +0200

    Switch to using hashfn.h, implement operators and send/recv
    
    * Introduce hashfn_id field in hashset to specify hash function ID
    * Replace custom hash function with Jenkins/lookup3 from hashfn.h
    * Implement hashset_send and hashset_recv and add C-test using libpq
    * Add operators and operator classes for hashset comparison, sorting
      and distinct queries

diff --git a/.gitignore b/.gitignore
index 0767074..91f216e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@ results/
 **/*.so
 regression.diffs
 regression.out
+.vscode
+test/c_tests/test_send_recv
diff --git a/Makefile b/Makefile
index ce88b4b..928e211 100644
--- a/Makefile
+++ b/Makefile
@@ -5,12 +5,29 @@ EXTENSION = hashset
 DATA = hashset--0.0.1.sql
 MODULES = hashset
 
-CFLAGS=`pg_config --includedir-server`
-
-REGRESS = prelude basic random table invalid
+# 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 random table invalid order
 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/hashset--0.0.1.sql b/hashset--0.0.1.sql
index a01ef48..7b79b73 100644
--- a/hashset--0.0.1.sql
+++ b/hashset--0.0.1.sql
@@ -107,3 +107,118 @@ CREATE AGGREGATE hashset(hashset) (
     COMBINEFUNC = hashset_agg_combine,
     PARALLEL = SAFE
 );
+
+
+CREATE OR REPLACE FUNCTION hashset_equals(hashset, hashset)
+    RETURNS bool
+    AS 'hashset', 'hashset_equals'
+    LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR = (
+    LEFTARG = hashset,
+    RIGHTARG = hashset,
+    PROCEDURE = hashset_equals,
+    COMMUTATOR = =,
+    HASHES
+);
+
+CREATE OR REPLACE FUNCTION hashset_neq(hashset, hashset)
+    RETURNS bool
+    AS 'hashset', 'hashset_neq'
+    LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR <> (
+    LEFTARG = hashset,
+    RIGHTARG = hashset,
+    PROCEDURE = hashset_neq,
+    COMMUTATOR = '<>',
+    NEGATOR = '=',
+    RESTRICT = neqsel,
+    JOIN = neqjoinsel,
+    HASHES
+);
+
+
+CREATE OR REPLACE FUNCTION hashset_hash(hashset)
+    RETURNS integer
+    AS 'hashset', 'hashset_hash'
+    LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR CLASS hashset_hash_ops
+    DEFAULT FOR TYPE hashset USING hash AS
+    OPERATOR 1 = (hashset, hashset),
+    FUNCTION 1 hashset_hash(hashset);
+
+CREATE OR REPLACE FUNCTION hashset_lt(hashset, hashset)
+    RETURNS bool
+    AS 'hashset', 'hashset_lt'
+    LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_le(hashset, hashset)
+    RETURNS boolean
+    AS 'hashset', 'hashset_le'
+    LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_gt(hashset, hashset)
+    RETURNS boolean
+    AS 'hashset', 'hashset_gt'
+    LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_ge(hashset, hashset)
+    RETURNS boolean
+    AS 'hashset', 'hashset_ge'
+    LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OR REPLACE FUNCTION hashset_cmp(hashset, hashset)
+    RETURNS integer
+    AS 'hashset', 'hashset_cmp'
+    LANGUAGE C IMMUTABLE STRICT;
+
+CREATE OPERATOR < (
+    LEFTARG = hashset,
+    RIGHTARG = hashset,
+    PROCEDURE = hashset_lt,
+    COMMUTATOR = >,
+    NEGATOR = >=,
+    RESTRICT = scalarltsel,
+    JOIN = scalarltjoinsel
+);
+
+CREATE OPERATOR <= (
+    PROCEDURE = hashset_le,
+    LEFTARG = hashset,
+    RIGHTARG = hashset,
+    COMMUTATOR = '>=',
+    NEGATOR = '>',
+    RESTRICT = scalarltsel,
+    JOIN = scalarltjoinsel
+);
+
+CREATE OPERATOR > (
+    PROCEDURE = hashset_gt,
+    LEFTARG = hashset,
+    RIGHTARG = hashset,
+    COMMUTATOR = '<',
+    NEGATOR = '<=',
+    RESTRICT = scalargtsel,
+    JOIN = scalargtjoinsel
+);
+
+CREATE OPERATOR >= (
+    PROCEDURE = hashset_ge,
+    LEFTARG = hashset,
+    RIGHTARG = hashset,
+    COMMUTATOR = '<=',
+    NEGATOR = '<',
+    RESTRICT = scalargtsel,
+    JOIN = scalargtjoinsel
+);
+
+CREATE OPERATOR CLASS hashset_btree_ops
+    DEFAULT FOR TYPE hashset USING btree AS
+    OPERATOR 1 < (hashset, hashset),
+    OPERATOR 2 <= (hashset, hashset),
+    OPERATOR 3 = (hashset, hashset),
+    OPERATOR 4 >= (hashset, hashset),
+    OPERATOR 5 > (hashset, hashset),
+    FUNCTION 1 hashset_cmp(hashset, hashset);
diff --git a/hashset.c b/hashset.c
index 7278e68..ec3ed44 100644
--- a/hashset.c
+++ b/hashset.c
@@ -19,6 +19,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "catalog/pg_type.h"
+#include "common/hashfn.h"
 
 PG_MODULE_MAGIC;
 
@@ -29,7 +30,8 @@ typedef struct hashset_t {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int32		flags;			/* reserved for future use (versioning, ...) */
 	int32		maxelements;	/* max number of element we have space for */
-	int32		nelements;			/* number of items added to the hashset */
+	int32		nelements;		/* number of items added to the hashset */
+	int32		hashfn_id;		/* ID of the hash function used */
 	char		data[FLEXIBLE_ARRAY_MEMBER];
 } hashset_t;
 
@@ -42,6 +44,7 @@ static Datum int32_to_array(FunctionCallInfo fcinfo, int32 * d, int len);
 #define PG_GETARG_HASHSET(x)	(hashset_t *) PG_DETOAST_DATUM(PG_GETARG_DATUM(x))
 #define CEIL_DIV(a, b) (((a) + (b) - 1) / (b))
 #define HASHSET_STEP 13
+#define JENKINS_LOOKUP3_HASHFN_ID 1
 
 PG_FUNCTION_INFO_V1(hashset_in);
 PG_FUNCTION_INFO_V1(hashset_out);
@@ -56,8 +59,15 @@ PG_FUNCTION_INFO_V1(hashset_agg_add_set);
 PG_FUNCTION_INFO_V1(hashset_agg_add);
 PG_FUNCTION_INFO_V1(hashset_agg_final);
 PG_FUNCTION_INFO_V1(hashset_agg_combine);
-
 PG_FUNCTION_INFO_V1(hashset_to_array);
+PG_FUNCTION_INFO_V1(hashset_equals);
+PG_FUNCTION_INFO_V1(hashset_neq);
+PG_FUNCTION_INFO_V1(hashset_hash);
+PG_FUNCTION_INFO_V1(hashset_lt);
+PG_FUNCTION_INFO_V1(hashset_le);
+PG_FUNCTION_INFO_V1(hashset_gt);
+PG_FUNCTION_INFO_V1(hashset_ge);
+PG_FUNCTION_INFO_V1(hashset_cmp);
 
 Datum hashset_in(PG_FUNCTION_ARGS);
 Datum hashset_out(PG_FUNCTION_ARGS);
@@ -72,8 +82,15 @@ Datum hashset_agg_add(PG_FUNCTION_ARGS);
 Datum hashset_agg_add_set(PG_FUNCTION_ARGS);
 Datum hashset_agg_final(PG_FUNCTION_ARGS);
 Datum hashset_agg_combine(PG_FUNCTION_ARGS);
-
 Datum hashset_to_array(PG_FUNCTION_ARGS);
+Datum hashset_equals(PG_FUNCTION_ARGS);
+Datum hashset_neq(PG_FUNCTION_ARGS);
+Datum hashset_hash(PG_FUNCTION_ARGS);
+Datum hashset_lt(PG_FUNCTION_ARGS);
+Datum hashset_le(PG_FUNCTION_ARGS);
+Datum hashset_gt(PG_FUNCTION_ARGS);
+Datum hashset_ge(PG_FUNCTION_ARGS);
+Datum hashset_cmp(PG_FUNCTION_ARGS);
 
 static hashset_t *
 hashset_allocate(int maxelements)
@@ -87,9 +104,8 @@ hashset_allocate(int maxelements)
 	 * i.e. the step size used in hashset_add_element()
 	 * and hashset_contains_element().
 	 */
-	while (maxelements % HASHSET_STEP == 0) {
+	while (maxelements % HASHSET_STEP == 0)
 		maxelements++;
-	}
 
 	len = offsetof(hashset_t, data);
 	len += CEIL_DIV(maxelements, 8);
@@ -103,6 +119,7 @@ hashset_allocate(int maxelements)
 	set->flags = 0;
 	set->maxelements = maxelements;
 	set->nelements = 0;
+	set->hashfn_id = JENKINS_LOOKUP3_HASHFN_ID;
 
 	set->flags |= 0;
 
@@ -220,36 +237,72 @@ hashset_out(PG_FUNCTION_ARGS)
 	PG_RETURN_CSTRING(str.data);
 }
 
-Datum
-hashset_recv(PG_FUNCTION_ARGS)
-{
-	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
-	hashset_t	*set;
-
-	set = (hashset_t *) palloc0(sizeof(hashset_t));
-	set->flags = pq_getmsgint(buf, 4);
-	set->maxelements = pq_getmsgint64(buf);
-	set->nelements = pq_getmsgint(buf, 4);
-
-	PG_RETURN_POINTER(set);
-}
 
 Datum
 hashset_send(PG_FUNCTION_ARGS)
 {
 	hashset_t  *set = (hashset_t *) PG_DETOAST_DATUM(PG_GETARG_DATUM(0));
 	StringInfoData buf;
+	int32 data_size;
 
+	/* Begin constructing the message */
 	pq_begintypsend(&buf);
 
+	/* Send the non-data fields */
 	pq_sendint(&buf, set->flags, 4);
-	pq_sendint64(&buf, set->maxelements);
+	pq_sendint(&buf, set->maxelements, 4);
 	pq_sendint(&buf, set->nelements, 4);
+	pq_sendint(&buf, set->hashfn_id, 4);
+
+	/* Compute and send the size of the data field */
+	data_size = VARSIZE(set) - offsetof(hashset_t, data);
+	pq_sendbytes(&buf, set->data, data_size);
 
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
 
+Datum
+hashset_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	hashset_t	*set;
+	int32		data_size;
+	Size		total_size;
+	const char	*binary_data;
+
+	/* Read fields from buffer */
+	int32 flags = pq_getmsgint(buf, 4);
+	int32 maxelements = pq_getmsgint(buf, 4);
+	int32 nelements = pq_getmsgint(buf, 4);
+	int32 hashfn_id = pq_getmsgint(buf, 4);
+
+	/* Compute the size of the data field */
+	data_size = buf->len - buf->cursor;
+
+	/* Read the binary data */
+	binary_data = pq_getmsgbytes(buf, data_size);
+
+	/* Compute total size of hashset_t */
+	total_size = offsetof(hashset_t, data) + data_size;
+
+	/* Allocate memory for hashset including the data field */
+	set = (hashset_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->maxelements = maxelements;
+	set->nelements = nelements;
+	set->hashfn_id = hashfn_id;
+	memcpy(set->data, binary_data, data_size);
+
+	PG_RETURN_POINTER(set);
+}
+
+
 Datum
 hashset_to_array(PG_FUNCTION_ARGS)
 {
@@ -317,10 +370,10 @@ int32_to_array(FunctionCallInfo fcinfo, int32 *d, int len)
 static hashset_t *
 hashset_resize(hashset_t * set)
 {
-	int		i;
+	int			i;
 	hashset_t	*new = hashset_allocate(set->maxelements * 2);
-	char	*bitmap;
-	int32	*values;
+	char		*bitmap;
+	int32		*values;
 
 	/* Calculate the pointer to the bitmap and values array */
 	bitmap = set->data;
@@ -350,7 +403,16 @@ hashset_add_element(hashset_t *set, int32 value)
 	if (set->nelements > set->maxelements * 0.75)
 		set = hashset_resize(set);
 
-	hash = ((uint32) value * 7691 + 4201) % set->maxelements;
+	if (set->hashfn_id == JENKINS_LOOKUP3_HASHFN_ID)
+	{
+		hash = hash_bytes_uint32((uint32) value) % set->maxelements;
+	}
+	else
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid hash function ID: \"%d\"", set->hashfn_id)));
+	}
 
 	bitmap = set->data;
 	values = (int32 *) (set->data + CEIL_DIV(set->maxelements, 8));
@@ -386,13 +448,23 @@ hashset_add_element(hashset_t *set, int32 value)
 static bool
 hashset_contains_element(hashset_t *set, int32 value)
 {
-	int		byte;
-	int		bit;
-	uint32	hash;
+	int     byte;
+	int     bit;
+	uint32  hash;
 	char   *bitmap;
 	int32  *values;
+	int     num_probes = 0; /* Add a counter for the number of probes */
 
-	hash = ((uint32) value * 7691 + 4201) % set->maxelements;
+	if (set->hashfn_id == JENKINS_LOOKUP3_HASHFN_ID)
+	{
+		hash = hash_bytes_uint32((uint32) value) % set->maxelements;
+	}
+	else
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("invalid hash function ID: \"%d\"", set->hashfn_id)));
+	}
 
 	bitmap = set->data;
 	values = (int32 *) (set->data + CEIL_DIV(set->maxelements, 8));
@@ -412,6 +484,12 @@ hashset_contains_element(hashset_t *set, int32 value)
 
 		/* move to the next element */
 		hash = (hash + HASHSET_STEP) % set->maxelements;
+
+		num_probes++; /* Increment the number of probes */
+
+		/* Check if we have probed all slots */
+		if (num_probes >= set->maxelements)
+			return false; /* Avoid infinite loop */
 	}
 }
 
@@ -692,3 +770,248 @@ hashset_agg_combine(PG_FUNCTION_ARGS)
 
 	PG_RETURN_POINTER(dst);
 }
+
+
+Datum
+hashset_equals(PG_FUNCTION_ARGS)
+{
+	hashset_t *a = PG_GETARG_HASHSET(0);
+	hashset_t *b = PG_GETARG_HASHSET(1);
+
+	char *bitmap_a;
+	int32 *values_a;
+	int i;
+
+	/*
+	 * Check if the number of elements is the same
+	 */
+	if (a->nelements != b->nelements)
+		PG_RETURN_BOOL(false);
+
+	bitmap_a = a->data;
+	values_a = (int32 *)(a->data + CEIL_DIV(a->maxelements, 8));
+
+	/*
+	 * Check if every element in a is also in b
+	 */
+	for (i = 0; i < a->maxelements; i++)
+	{
+		int byte = (i / 8);
+		int bit = (i % 8);
+
+		if (bitmap_a[byte] & (0x01 << bit))
+		{
+			int32 value = values_a[i];
+
+			if (!hashset_contains_element(b, value))
+				PG_RETURN_BOOL(false);
+		}
+	}
+
+	/*
+	 * All elements in a are in b and the number of elements is the same,
+	 * so the sets must be equal.
+	 */
+	PG_RETURN_BOOL(true);
+}
+
+
+Datum
+hashset_neq(PG_FUNCTION_ARGS)
+{
+    hashset_t *a = PG_GETARG_HASHSET(0);
+    hashset_t *b = PG_GETARG_HASHSET(1);
+
+    /* If a is not equal to b, then they are not equal */
+    if (!DatumGetBool(DirectFunctionCall2(hashset_equals, PointerGetDatum(a), PointerGetDatum(b))))
+        PG_RETURN_BOOL(true);
+
+    PG_RETURN_BOOL(false);
+}
+
+
+Datum hashset_hash(PG_FUNCTION_ARGS)
+{
+    hashset_t *set = PG_GETARG_HASHSET(0);
+
+    /* Initial hash value */
+    uint32 hash = 0;
+
+    /* Access the data array */
+    char *bitmap = set->data;
+    int32 *values = (int32 *)(set->data + CEIL_DIV(set->maxelements, 8));
+
+    /* Iterate through all elements */
+    for (int32 i = 0; i < set->maxelements; i++)
+    {
+        int byte = i / 8;
+        int bit = i % 8;
+
+        /* Check if the current position is occupied */
+        if (bitmap[byte] & (0x01 << bit))
+        {
+            /* Combine the hash value of the current element with the total hash */
+            hash = hash_combine(hash, hash_uint32(values[i]));
+        }
+    }
+
+    /* Return the final hash value */
+    PG_RETURN_INT32(hash);
+}
+
+
+Datum
+hashset_lt(PG_FUNCTION_ARGS)
+{
+	hashset_t *a = PG_GETARG_HASHSET(0);
+	hashset_t *b = PG_GETARG_HASHSET(1);
+
+	char *bitmap_a, *bitmap_b;
+	int32 *values_a, *values_b;
+	int i;
+
+	bitmap_a = a->data;
+	values_a = (int32 *)(a->data + CEIL_DIV(a->maxelements, 8));
+
+	bitmap_b = b->data;
+	values_b = (int32 *)(b->data + CEIL_DIV(b->maxelements, 8));
+
+	/* Compare elements in a lexicographic manner */
+	for (i = 0; i < Min(a->maxelements, b->maxelements); i++)
+	{
+		int byte = (i / 8);
+		int bit = (i % 8);
+
+		bool has_elem_a = bitmap_a[byte] & (0x01 << bit);
+		bool has_elem_b = bitmap_b[byte] & (0x01 << bit);
+
+		if (has_elem_a && has_elem_b)
+		{
+			int32 value_a = values_a[i];
+			int32 value_b = values_b[i];
+
+			if (value_a < value_b)
+				PG_RETURN_BOOL(true);
+			else if (value_a > value_b)
+				PG_RETURN_BOOL(false);
+
+		}
+		else if (has_elem_a)
+			PG_RETURN_BOOL(false);
+		else if (has_elem_b)
+			PG_RETURN_BOOL(true);
+	}
+
+	/*
+	 * If all elements are equal up to the shorter hashset length,
+	 * then the hashset with fewer elements is considered "less than"
+	 */
+	if (a->maxelements < b->maxelements)
+		PG_RETURN_BOOL(true);
+	else
+		PG_RETURN_BOOL(false);
+}
+
+
+Datum
+hashset_le(PG_FUNCTION_ARGS)
+{
+	hashset_t *a = PG_GETARG_HASHSET(0);
+	hashset_t *b = PG_GETARG_HASHSET(1);
+
+	/* If a equals b, or a is less than b, then a is less than or equal to b */
+	if (DatumGetBool(DirectFunctionCall2(hashset_equals, PointerGetDatum(a), PointerGetDatum(b))) ||
+		DatumGetBool(DirectFunctionCall2(hashset_lt, PointerGetDatum(a), PointerGetDatum(b))))
+		PG_RETURN_BOOL(true);
+
+	PG_RETURN_BOOL(false);
+}
+
+
+Datum
+hashset_gt(PG_FUNCTION_ARGS)
+{
+	hashset_t *a = PG_GETARG_HASHSET(0);
+	hashset_t *b = PG_GETARG_HASHSET(1);
+
+	/* If a is not less than or equal to b, then a is greater than b */
+	if (!DatumGetBool(DirectFunctionCall2(hashset_le, PointerGetDatum(a), PointerGetDatum(b))))
+		PG_RETURN_BOOL(true);
+
+	PG_RETURN_BOOL(false);
+}
+
+
+Datum
+hashset_ge(PG_FUNCTION_ARGS)
+{
+	hashset_t *a = PG_GETARG_HASHSET(0);
+	hashset_t *b = PG_GETARG_HASHSET(1);
+
+	/* If a equals b, or a is not less than b, then a is greater than or equal to b */
+	if (DatumGetBool(DirectFunctionCall2(hashset_equals, PointerGetDatum(a), PointerGetDatum(b))) ||
+		!DatumGetBool(DirectFunctionCall2(hashset_lt, PointerGetDatum(a), PointerGetDatum(b))))
+		PG_RETURN_BOOL(true);
+
+	PG_RETURN_BOOL(false);
+}
+
+
+Datum
+hashset_cmp(PG_FUNCTION_ARGS)
+{
+	hashset_t *a = PG_GETARG_HASHSET(0);
+	hashset_t *b = PG_GETARG_HASHSET(1);
+
+	char *bitmap_a, *bitmap_b;
+	int32 *values_a, *values_b;
+	int i;
+
+	bitmap_a = a->data;
+	values_a = (int32 *)(a->data + CEIL_DIV(a->maxelements, 8));
+
+	bitmap_b = b->data;
+	values_b = (int32 *)(b->data + CEIL_DIV(b->maxelements, 8));
+
+	/*
+	 * Iterate through the elements
+	 */
+	for (i = 0; i < Min(a->maxelements, b->maxelements); i++)
+	{
+		int byte = (i / 8);
+		int bit = (i % 8);
+
+		bool a_contains = bitmap_a[byte] & (0x01 << bit);
+		bool b_contains = bitmap_b[byte] & (0x01 << bit);
+
+		if (a_contains && b_contains)
+		{
+			int32 value_a = values_a[i];
+			int32 value_b = values_b[i];
+
+			if (value_a < value_b)
+				PG_RETURN_INT32(-1);
+			else if (value_a > value_b)
+				PG_RETURN_INT32(1);
+		}
+		else if (a_contains)
+		{
+			PG_RETURN_INT32(1);
+		}
+		else if (b_contains)
+		{
+			PG_RETURN_INT32(-1);
+		}
+	}
+
+	/*
+	 * If we got here, the elements in the overlap are equal.
+	 * We need to check the number of elements to determine the order.
+	 */
+	if (a->nelements < b->nelements)
+		PG_RETURN_INT32(-1);
+	else if (a->nelements > b->nelements)
+		PG_RETURN_INT32(1);
+	else
+		PG_RETURN_INT32(0);
+}
diff --git a/test/c_tests/test_send_recv.c b/test/c_tests/test_send_recv.c
new file mode 100644
index 0000000..5655b8b
--- /dev/null
+++ b/test/c_tests/test_send_recv.c
@@ -0,0 +1,84 @@
+#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	*conninfo = "host=localhost port=5432";
+	PGconn		*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 hashset)");
+
+	/* 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}'::hashset)";
+	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::text) 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/order.out b/test/expected/order.out
new file mode 100644
index 0000000..8d3ff61
--- /dev/null
+++ b/test/expected/order.out
@@ -0,0 +1,118 @@
+CREATE TABLE IF NOT EXISTS test_hashset_order (hashset_col hashset);
+INSERT INTO test_hashset_order (hashset_col) VALUES ('{1,2,3}'::hashset);
+INSERT INTO test_hashset_order (hashset_col) VALUES ('{3,2,1}'::hashset);
+INSERT INTO test_hashset_order (hashset_col) VALUES ('{4,5,6}'::hashset);
+SELECT COUNT(DISTINCT hashset_col) FROM test_hashset_order;
+ count 
+-------
+     2
+(1 row)
+
+SELECT '{2}'::hashset < '{1}'::hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::hashset < '{2}'::hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::hashset < '{3}'::hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::hashset <= '{1}'::hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::hashset <= '{2}'::hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::hashset <= '{3}'::hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::hashset > '{1}'::hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::hashset > '{2}'::hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::hashset > '{3}'::hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::hashset >= '{1}'::hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::hashset >= '{2}'::hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::hashset >= '{3}'::hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::hashset = '{1}'::hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::hashset = '{2}'::hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::hashset = '{3}'::hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::hashset <> '{1}'::hashset; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT '{2}'::hashset <> '{2}'::hashset; -- false
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT '{2}'::hashset <> '{3}'::hashset; -- true
+ ?column? 
+----------
+ t
+(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/order.sql b/test/sql/order.sql
new file mode 100644
index 0000000..e6c9323
--- /dev/null
+++ b/test/sql/order.sql
@@ -0,0 +1,29 @@
+CREATE TABLE IF NOT EXISTS test_hashset_order (hashset_col hashset);
+INSERT INTO test_hashset_order (hashset_col) VALUES ('{1,2,3}'::hashset);
+INSERT INTO test_hashset_order (hashset_col) VALUES ('{3,2,1}'::hashset);
+INSERT INTO test_hashset_order (hashset_col) VALUES ('{4,5,6}'::hashset);
+SELECT COUNT(DISTINCT hashset_col) FROM test_hashset_order;
+
+SELECT '{2}'::hashset < '{1}'::hashset; -- false
+SELECT '{2}'::hashset < '{2}'::hashset; -- false
+SELECT '{2}'::hashset < '{3}'::hashset; -- true
+
+SELECT '{2}'::hashset <= '{1}'::hashset; -- false
+SELECT '{2}'::hashset <= '{2}'::hashset; -- true
+SELECT '{2}'::hashset <= '{3}'::hashset; -- true
+
+SELECT '{2}'::hashset > '{1}'::hashset; -- true
+SELECT '{2}'::hashset > '{2}'::hashset; -- false
+SELECT '{2}'::hashset > '{3}'::hashset; -- false
+
+SELECT '{2}'::hashset >= '{1}'::hashset; -- true
+SELECT '{2}'::hashset >= '{2}'::hashset; -- true
+SELECT '{2}'::hashset >= '{3}'::hashset; -- false
+
+SELECT '{2}'::hashset = '{1}'::hashset; -- false
+SELECT '{2}'::hashset = '{2}'::hashset; -- true
+SELECT '{2}'::hashset = '{3}'::hashset; -- false
+
+SELECT '{2}'::hashset <> '{1}'::hashset; -- true
+SELECT '{2}'::hashset <> '{2}'::hashset; -- false
+SELECT '{2}'::hashset <> '{3}'::hashset; -- true
