diff --git a/contrib/ltree/Makefile b/contrib/ltree/Makefile
index 770769a730..f50f2c9470 100644
--- a/contrib/ltree/Makefile
+++ b/contrib/ltree/Makefile
@@ -14,7 +14,7 @@ OBJS = \
ltxtquery_op.o
EXTENSION = ltree
-DATA = ltree--1.1--1.2.sql ltree--1.1.sql ltree--1.0--1.1.sql
+DATA = ltree--1.2--1.3.sql ltree--1.1--1.2.sql ltree--1.1.sql ltree--1.0--1.1.sql
PGFILEDESC = "ltree - hierarchical label data type"
HEADERS = ltree.h
diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out
index 984cd030cf..b0f99afea1 100644
--- a/contrib/ltree/expected/ltree.out
+++ b/contrib/ltree/expected/ltree.out
@@ -1433,8 +1433,27 @@ SELECT '{j.k.l.m, g.b.c.d.e}'::ltree[] ?~ 'A*@|g.b.c.d.e';
g.b.c.d.e
(1 row)
+-- Check that the ltree_hash() and ltree_hash_extended() function's lower
+-- 32 bits match when the seed is 0 and do not match when the seed != 0
+SELECT v as value, ltree_hash(v)::bit(32) as standard,
+ ltree_hash_extended(v, 0)::bit(32) as extended0,
+ ltree_hash_extended(v, 1)::bit(32) as extended1
+FROM (VALUES (NULL::ltree), (''::ltree), ('0'::ltree), ('0.1'::ltree),
+ ('0.1.2'::ltree), ('0'::ltree), ('0_asd.1_ASD'::ltree)) x(v)
+WHERE ltree_hash(v)::bit(32) != ltree_hash_extended(v, 0)::bit(32)
+ OR ltree_hash(v)::bit(32) = ltree_hash_extended(v, 1)::bit(32);
+ value | standard | extended0 | extended1
+-------+----------+-----------+-----------
+(0 rows)
+
CREATE TABLE ltreetest (t ltree);
\copy ltreetest FROM 'data/ltree.data'
+SELECT count(*) from ltreetest;
+ count
+-------
+ 1006
+(1 row)
+
SELECT * FROM ltreetest WHERE t < '12.3' order by t asc;
t
----------------------------------
@@ -7832,6 +7851,26 @@ SELECT * FROM ltreetest WHERE t ? '{23.*.1,23.*.2}' order by t asc;
23.3.32.21.5.14.10.17.1
(4 rows)
+drop index tstidx;
+create index tstidx on ltreetest using hash (t);
+set enable_seqscan=off;
+SELECT * FROM ltreetest WHERE t = '12.3' order by t asc;
+ t
+------
+ 12.3
+(1 row)
+
+set enable_hashagg = true;
+set enable_sort = false;
+SELECT count(*) FROM (
+SELECT t FROM (SELECT * FROM ltreetest UNION ALL SELECT * FROM ltreetest) t1 GROUP BY t
+) t2;
+ count
+-------
+ 1006
+(1 row)
+
+set enable_sort = true;
drop index tstidx;
create index tstidx on ltreetest using gist (t gist_ltree_ops(siglen=0));
ERROR: value 0 out of bounds for option "siglen"
diff --git a/contrib/ltree/ltree--1.2--1.3.sql b/contrib/ltree/ltree--1.2--1.3.sql
new file mode 100644
index 0000000000..d9111551dd
--- /dev/null
+++ b/contrib/ltree/ltree--1.2--1.3.sql
@@ -0,0 +1,21 @@
+/* contrib/ltree/ltree--1.2--1.3.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION ltree UPDATE TO '1.3'" to load this file. \quit
+
+CREATE FUNCTION ltree_hash(ltree)
+RETURNS integer
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE FUNCTION ltree_hash_extended(ltree, bigint)
+RETURNS bigint
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT IMMUTABLE PARALLEL SAFE;
+
+CREATE OPERATOR CLASS hash_ltree_ops
+DEFAULT FOR TYPE ltree USING hash
+AS
+ OPERATOR 1 = ,
+ FUNCTION 1 ltree_hash(ltree),
+ FUNCTION 2 ltree_hash_extended(ltree, bigint);
diff --git a/contrib/ltree/ltree.control b/contrib/ltree/ltree.control
index b408d64781..c2cbeda96c 100644
--- a/contrib/ltree/ltree.control
+++ b/contrib/ltree/ltree.control
@@ -1,6 +1,6 @@
# ltree extension
comment = 'data type for hierarchical tree-like structures'
-default_version = '1.2'
+default_version = '1.3'
module_pathname = '$libdir/ltree'
relocatable = true
trusted = true
diff --git a/contrib/ltree/ltree_op.c b/contrib/ltree/ltree_op.c
index da1db5fcd2..4199c70f7f 100644
--- a/contrib/ltree/ltree_op.c
+++ b/contrib/ltree/ltree_op.c
@@ -9,6 +9,7 @@
#include "access/htup_details.h"
#include "catalog/pg_statistic.h"
+#include "common/hashfn.h"
#include "ltree.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
@@ -37,6 +38,8 @@ PG_FUNCTION_INFO_V1(lca);
PG_FUNCTION_INFO_V1(ltree2text);
PG_FUNCTION_INFO_V1(text2ltree);
PG_FUNCTION_INFO_V1(ltreeparentsel);
+PG_FUNCTION_INFO_V1(ltree_hash);
+PG_FUNCTION_INFO_V1(ltree_hash_extended);
int
ltree_compare(const ltree *a, const ltree *b)
@@ -588,3 +591,68 @@ ltreeparentsel(PG_FUNCTION_ARGS)
PG_RETURN_FLOAT8((float8) selec);
}
+
+/*
+ * Hashes the elements of the path using the same logic as hash_array
+ */
+Datum
+ltree_hash(PG_FUNCTION_ARGS)
+{
+ ltree *a = PG_GETARG_LTREE_P(0);
+
+ uint32 result = 1;
+ int an = a->numlevel;
+ ltree_level *al = LTREE_FIRST(a);
+
+ while (an > 0)
+ {
+ uint32 levelHash = hash_any((unsigned char *) al->name, al->len);
+
+ result = (result << 5) - result + levelHash;
+
+ an--;
+ al = LEVEL_NEXT(al);
+ }
+
+ PG_FREE_IF_COPY(a, 0);
+ PG_RETURN_UINT32(result);
+}
+
+
+/*
+ * Hashes the elements of the path using the same logic as hash_array_extended
+ * (and ltree_hash). It differs from hash_array_extended only in how it handles
+ * zero length label paths. We return 1 + seed to ensure that the low 32 bits
+ * of the result match ltree_hash when the seed is 0, as required by the hash
+ * index support functions, but to also return a different value when there is
+ * a seed.
+ */
+Datum
+ltree_hash_extended(PG_FUNCTION_ARGS)
+{
+ ltree *a = PG_GETARG_LTREE_P(0);
+ const uint64 seed = PG_GETARG_INT64(1);
+
+ uint64 result = 1;
+ int an = a->numlevel;
+ ltree_level *al = LTREE_FIRST(a);
+
+ if (an == 0)
+ {
+ PG_FREE_IF_COPY(a, 0);
+ PG_RETURN_UINT64(result + seed);
+ }
+
+ while (an > 0)
+ {
+ uint64 levelHash = hash_any_extended((unsigned char *) al->name, al->len, seed);
+
+ result = (result << 5) - result + levelHash;
+
+ an--;
+ al = LEVEL_NEXT(al);
+ }
+
+ PG_FREE_IF_COPY(a, 0);
+ PG_RETURN_UINT64(result);
+}
diff --git a/contrib/ltree/ltreetest.sql b/contrib/ltree/ltreetest.sql
index d6996caf3c..388d5bb6f5 100644
--- a/contrib/ltree/ltreetest.sql
+++ b/contrib/ltree/ltreetest.sql
@@ -19,3 +19,4 @@ INSERT INTO test VALUES ('Top.Collections.Pictures.Astronomy.Galaxies');
INSERT INTO test VALUES ('Top.Collections.Pictures.Astronomy.Astronauts');
CREATE INDEX path_gist_idx ON test USING gist(path);
CREATE INDEX path_idx ON test USING btree(path);
+CREATE INDEX path_hash_idx ON test USING hash(path);
diff --git a/contrib/ltree/sql/ltree.sql b/contrib/ltree/sql/ltree.sql
index 402096f6c4..fa0b9dff91 100644
--- a/contrib/ltree/sql/ltree.sql
+++ b/contrib/ltree/sql/ltree.sql
@@ -282,9 +282,22 @@ SELECT ('{3456,1.2.3.4}'::ltree[] ?<@ '1.2.5') is null;
SELECT '{ltree.asd, tree.awdfg}'::ltree[] ?@ 'tree & aWdfg@'::ltxtquery;
SELECT '{j.k.l.m, g.b.c.d.e}'::ltree[] ?~ 'A*@|g.b.c.d.e';
+-- Check that the ltree_hash() and ltree_hash_extended() function's lower
+-- 32 bits match when the seed is 0 and do not match when the seed != 0
+SELECT v as value, ltree_hash(v)::bit(32) as standard,
+ ltree_hash_extended(v, 0)::bit(32) as extended0,
+ ltree_hash_extended(v, 1)::bit(32) as extended1
+FROM (VALUES (NULL::ltree), (''::ltree), ('0'::ltree), ('0.1'::ltree),
+ ('0.1.2'::ltree), ('0'::ltree), ('0_asd.1_ASD'::ltree)) x(v)
+WHERE ltree_hash(v)::bit(32) != ltree_hash_extended(v, 0)::bit(32)
+ OR ltree_hash(v)::bit(32) = ltree_hash_extended(v, 1)::bit(32);
+
+
CREATE TABLE ltreetest (t ltree);
\copy ltreetest FROM 'data/ltree.data'
+SELECT count(*) from ltreetest;
+
SELECT * FROM ltreetest WHERE t < '12.3' order by t asc;
SELECT * FROM ltreetest WHERE t <= '12.3' order by t asc;
SELECT * FROM ltreetest WHERE t = '12.3' order by t asc;
@@ -328,6 +341,20 @@ SELECT * FROM ltreetest WHERE t ~ '23.*.1' order by t asc;
SELECT * FROM ltreetest WHERE t ~ '23.*.2' order by t asc;
SELECT * FROM ltreetest WHERE t ? '{23.*.1,23.*.2}' order by t asc;
+drop index tstidx;
+create index tstidx on ltreetest using hash (t);
+set enable_seqscan=off;
+
+SELECT * FROM ltreetest WHERE t = '12.3' order by t asc;
+
+set enable_hashagg = true;
+set enable_sort = false;
+
+SELECT count(*) FROM (
+SELECT t FROM (SELECT * FROM ltreetest UNION ALL SELECT * FROM ltreetest) t1 GROUP BY t
+) t2;
+set enable_sort = true;
+
drop index tstidx;
create index tstidx on ltreetest using gist (t gist_ltree_ops(siglen=0));
create index tstidx on ltreetest using gist (t gist_ltree_ops(siglen=2025));
diff --git a/doc/src/sgml/ltree.sgml b/doc/src/sgml/ltree.sgml
index 00a6ae70da..5c109d2d5f 100644
--- a/doc/src/sgml/ltree.sgml
+++ b/doc/src/sgml/ltree.sgml
@@ -623,6 +623,13 @@ Europe & Russia*@ & !Transportation
>=, >
+
+
+ Hash index over ltree:
+ =
+
+
+
GiST index over ltree (gist_ltree_ops
@@ -712,6 +719,7 @@ INSERT INTO test VALUES ('Top.Collections.Pictures.Astronomy.Galaxies');
INSERT INTO test VALUES ('Top.Collections.Pictures.Astronomy.Astronauts');
CREATE INDEX path_gist_idx ON test USING GIST (path);
CREATE INDEX path_idx ON test USING BTREE (path);
+CREATE INDEX path_hash_idx ON test USING HASH (path);