From e35f1f9a7f4280ae7d122768debbdc988054c129 Mon Sep 17 00:00:00 2001
From: Alexandra Wang <alexandra.wang.oss@gmail.com>
Date: Mon, 12 Jan 2026 16:01:42 -0800
Subject: [PATCH v24 1/3] hstore: add read-only dot notation for key access

Allow accessing hstore values using dot notation, e.g., (h).keyname, as
an alternative to bracket notation h['key'].

For backward compatibility, when a key name matches an existing function
that takes hstore as its argument (e.g., akeys, avals, hstore_to_json),
the function call takes precedence. Users can still access such keys using
bracket notation.

To support this, add a new transform_partial callback in SubscriptRoutines
as an alternative to the existing transform callback. Unlike transform,
which requires all indirection elements to be A_Indices (bracket subscripts),
transform_partial receives raw indirection elements including String nodes
for field names. It returns the number of elements consumed, allowing the
parser to handle remaining elements (e.g., as nested field access or function
calls). Each subscripting type must provide exactly one of the two callbacks.

The parser's transformContainerSubscripts now returns the count of consumed
indirections, and transformIndirection uses a loop that tries container
subscripting first, then falls back to function/field resolution for any
remaining String nodes.

Also improve hstore's error message for subscript slice to be more specific.
---
 contrib/hstore/expected/hstore.out |  88 +++++++++++++++++++-
 contrib/hstore/hstore_subs.c       | 124 ++++++++++++++++++++++-------
 contrib/hstore/sql/hstore.sql      |  24 ++++++
 src/backend/parser/parse_expr.c    |  78 +++++++++++-------
 src/backend/parser/parse_node.c    |  92 +++++++++++++++------
 src/backend/parser/parse_target.c  |   6 +-
 src/backend/utils/adt/arraysubs.c  |   2 +
 src/backend/utils/adt/jsonbsubs.c  |   1 +
 src/include/nodes/subscripting.h   |  24 +++++-
 src/include/parser/parse_node.h    |   3 +-
 10 files changed, 357 insertions(+), 85 deletions(-)

diff --git a/contrib/hstore/expected/hstore.out b/contrib/hstore/expected/hstore.out
index 1836c9acf39..b0281dcbfb7 100644
--- a/contrib/hstore/expected/hstore.out
+++ b/contrib/hstore/expected/hstore.out
@@ -1607,7 +1607,7 @@ select f2['d'], f2['x'] is null as x_isnull from test_json_agg;
 select f2['d']['e'] from test_json_agg;  -- error
 ERROR:  hstore allows only one subscript
 select f2['d':'e'] from test_json_agg;  -- error
-ERROR:  hstore allows only one subscript
+ERROR:  hstore subscript does not support slices
 update test_json_agg set f2['d'] = f2['e'], f2['x'] = 'xyzzy';
 select f2 from test_json_agg;
                                                          f2                                                          
@@ -1633,3 +1633,89 @@ WHERE  hstore_hash(v)::bit(32) != hstore_hash_extended(v, 0)::bit(32)
 -------+----------+-----------+-----------
 (0 rows)
 
+-- for single-argument functions: (h).func_name calls func_name(h)
+select ('aa=>1, b=>2, cq=>3'::hstore).akeys;
+   akeys   
+-----------
+ {b,aa,cq}
+(1 row)
+
+select ('aa=>1, b=>2, cq=>3'::hstore).avals;
+  avals  
+---------
+ {2,1,3}
+(1 row)
+
+select ('aa=>1, b=>2, cq=>3'::hstore).hstore_to_json;
+          hstore_to_json          
+----------------------------------
+ {"b": "2", "aa": "1", "cq": "3"}
+(1 row)
+
+select ('aa=>1, b=>2, cq=>3'::hstore).hstore_to_jsonb;
+         hstore_to_jsonb          
+----------------------------------
+ {"b": "2", "aa": "1", "cq": "3"}
+(1 row)
+
+select ('aa=>1, b=>2'::hstore).nonexistent;
+ nonexistent 
+-------------
+ 
+(1 row)
+
+-- key with same name as function - function takes precedence with dot notation
+-- but bracket notation returns the key value
+select ('"akeys"=>"surprise", "aa"=>"1"'::hstore).akeys;
+   akeys    
+------------
+ {aa,akeys}
+(1 row)
+
+select ('"akeys"=>"surprise", "aa"=>"1"'::hstore)['akeys'];
+  hstore  
+----------
+ surprise
+(1 row)
+
+-- dot notation for key access
+select ('aa=>1, b=>2, cq=>3'::hstore).aa;
+ aa 
+----
+ 1
+(1 row)
+
+select ('aa=>1, b=>2, cq=>3'::hstore).b;
+ b 
+---
+ 2
+(1 row)
+
+select ('aa=>1, b=>2, cq=>3'::hstore).cq;
+ cq 
+----
+ 3
+(1 row)
+
+select ('aa=>1, b=>2, cq=>3'::hstore).missing;
+ missing 
+---------
+ 
+(1 row)
+
+-- dot notation with special key names
+select ('"a b"=>"space", "a.b"=>"dot"'::hstore)."a b";
+  a b  
+-------
+ space
+(1 row)
+
+select ('"a b"=>"space", "a.b"=>"dot"'::hstore)."a.b";
+ a.b 
+-----
+ dot
+(1 row)
+
+-- star expansion is not supported
+select ('aa=>1, b=>2'::hstore).*;
+ERROR:  type hstore is not composite
diff --git a/contrib/hstore/hstore_subs.c b/contrib/hstore/hstore_subs.c
index 45b8c962038..854a675937e 100644
--- a/contrib/hstore/hstore_subs.c
+++ b/contrib/hstore/hstore_subs.c
@@ -23,56 +23,119 @@
  */
 #include "postgres.h"
 
+#include "catalog/pg_collation_d.h"
 #include "executor/execExpr.h"
 #include "hstore.h"
+#include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "nodes/subscripting.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_expr.h"
+#include "parser/parse_func.h"
 #include "utils/builtins.h"
 
 
 /*
  * Finish parse analysis of a SubscriptingRef expression for hstore.
  *
- * Verify there's just one subscript, coerce it to text,
+ * Handles both bracket notation h['key'] and dot notation (h).key. Both notations
+ * are semantically identical.
+ *
+ * For dot notation, check single-element function call first, if not a function call,
+ * verify there's just one indirection, coerce it to text,
  * and set the result type of the SubscriptingRef node.
+ *
+ * Returns the number of indirection elements processed (0 or 1).
  */
-static void
+static int
 hstore_subscript_transform(SubscriptingRef *sbsref,
 						   List *indirection,
 						   ParseState *pstate,
-						   bool isSlice,
 						   bool isAssignment)
 {
 	A_Indices  *ai;
 	Node	   *subexpr;
 
-	/* We support only single-subscript, non-slice cases */
-	if (isSlice || list_length(indirection) != 1)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("hstore allows only one subscript"),
-				 parser_errposition(pstate,
-									exprLocation((Node *) indirection))));
-
-	/* Transform the subscript expression to type text */
-	ai = linitial_node(A_Indices, indirection);
-	Assert(ai->uidx != NULL && ai->lidx == NULL && !ai->is_slice);
-
-	subexpr = transformExpr(pstate, ai->uidx, pstate->p_expr_kind);
-	/* If it's not text already, try to coerce */
-	subexpr = coerce_to_target_type(pstate,
-									subexpr, exprType(subexpr),
-									TEXTOID, -1,
-									COERCION_ASSIGNMENT,
-									COERCE_IMPLICIT_CAST,
-									-1);
-	if (subexpr == NULL)
-		ereport(ERROR,
-				(errcode(ERRCODE_DATATYPE_MISMATCH),
-				 errmsg("hstore subscript must have type text"),
-				 parser_errposition(pstate, exprLocation(ai->uidx))));
+	Assert(list_length(indirection) >= 1);
+
+	if (IsA(linitial(indirection), String))
+	{
+		/*
+		 * Dot notation: (h).keyname
+		 *
+		 * For backward compatibility, check if a function with this name
+		 * exists that takes hstore. If so, return 0 to let the parser handle
+		 * it as a function call instead.
+		 */
+		char	   *fieldname = strVal(linitial(indirection));
+		Oid			argtypes[1] = {sbsref->refcontainertype};
+		Oid			funcoid;
+
+		funcoid = LookupFuncName(list_make1(makeString(pstrdup(fieldname))),
+								 1, argtypes, true);
+		if (OidIsValid(funcoid))
+			return 0;
+
+		/* We support only single-element indirection */
+		if (list_length(indirection) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("hstore allows only one subscript"),
+					 parser_errposition(pstate,
+										exprLocation((Node *) indirection))));
+
+		/* No conflicting function - treat as key lookup */
+		subexpr = (Node *) makeConst(TEXTOID,
+									 -1,
+									 DEFAULT_COLLATION_OID,
+									 -1,
+									 CStringGetTextDatum(fieldname),
+									 false,
+									 false);
+	}
+	else if (IsA(linitial(indirection), A_Indices))
+	{
+		/* We support only single-subscript, non-slice cases */
+		if (list_length(indirection) != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("hstore allows only one subscript"),
+					 parser_errposition(pstate,
+										exprLocation((Node *) indirection))));
+
+		ai = linitial_node(A_Indices, indirection);
+
+		if (ai->is_slice)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("hstore subscript does not support slices"),
+					 parser_errposition(pstate,
+										exprLocation((Node *) indirection))));
+
+		Assert(ai->uidx != NULL && ai->lidx == NULL);
+
+		/* Transform the subscript expression to type text */
+		subexpr = transformExpr(pstate, ai->uidx, pstate->p_expr_kind);
+		/* If it's not text already, try to coerce */
+		subexpr = coerce_to_target_type(pstate,
+										subexpr, exprType(subexpr),
+										TEXTOID, -1,
+										COERCION_ASSIGNMENT,
+										COERCE_IMPLICIT_CAST,
+										-1);
+		if (subexpr == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("hstore subscript must have type text"),
+					 parser_errposition(pstate,
+										exprLocation(ai->uidx))));
+	}
+	else
+	{
+		/* A_Star is handled by the parser before we get here */
+		Assert(false);
+		return 0;
+	}
 
 	/* ... and store the transformed subscript into the SubscriptingRef node */
 	sbsref->refupperindexpr = list_make1(subexpr);
@@ -81,6 +144,8 @@ hstore_subscript_transform(SubscriptingRef *sbsref,
 	/* Determine the result type of the subscripting operation; always text */
 	sbsref->refrestype = TEXTOID;
 	sbsref->reftypmod = -1;
+
+	return 1;
 }
 
 /*
@@ -286,7 +351,8 @@ Datum
 hstore_subscript_handler(PG_FUNCTION_ARGS)
 {
 	static const SubscriptRoutines sbsroutines = {
-		.transform = hstore_subscript_transform,
+		.transform = NULL,
+		.transform_partial = hstore_subscript_transform,
 		.exec_setup = hstore_exec_setup,
 		.fetch_strict = true,	/* fetch returns NULL for NULL inputs */
 		.fetch_leakproof = true,	/* fetch returns NULL for bad subscript */
diff --git a/contrib/hstore/sql/hstore.sql b/contrib/hstore/sql/hstore.sql
index efef91292a3..b508b6d4e1f 100644
--- a/contrib/hstore/sql/hstore.sql
+++ b/contrib/hstore/sql/hstore.sql
@@ -395,3 +395,27 @@ FROM   (VALUES (NULL::hstore), (''), ('"a key" =>1'), ('c => null'),
        ('e => 012345'), ('g => 2.345e+4')) x(v)
 WHERE  hstore_hash(v)::bit(32) != hstore_hash_extended(v, 0)::bit(32)
        OR hstore_hash(v)::bit(32) = hstore_hash_extended(v, 1)::bit(32);
+
+-- for single-argument functions: (h).func_name calls func_name(h)
+select ('aa=>1, b=>2, cq=>3'::hstore).akeys;
+select ('aa=>1, b=>2, cq=>3'::hstore).avals;
+select ('aa=>1, b=>2, cq=>3'::hstore).hstore_to_json;
+select ('aa=>1, b=>2, cq=>3'::hstore).hstore_to_jsonb;
+select ('aa=>1, b=>2'::hstore).nonexistent;
+-- key with same name as function - function takes precedence with dot notation
+-- but bracket notation returns the key value
+select ('"akeys"=>"surprise", "aa"=>"1"'::hstore).akeys;
+select ('"akeys"=>"surprise", "aa"=>"1"'::hstore)['akeys'];
+
+-- dot notation for key access
+select ('aa=>1, b=>2, cq=>3'::hstore).aa;
+select ('aa=>1, b=>2, cq=>3'::hstore).b;
+select ('aa=>1, b=>2, cq=>3'::hstore).cq;
+select ('aa=>1, b=>2, cq=>3'::hstore).missing;
+
+-- dot notation with special key names
+select ('"a b"=>"space", "a.b"=>"dot"'::hstore)."a b";
+select ('"a b"=>"space", "a.b"=>"dot"'::hstore)."a.b";
+
+-- star expansion is not supported
+select ('aa=>1, b=>2'::hstore).*;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 56826db4c26..24cec4da1f5 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -439,44 +439,65 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
 {
 	Node	   *last_srf = pstate->p_last_srf;
 	Node	   *result = transformExprRecurse(pstate, ind->arg);
-	List	   *subscripts = NIL;
+	List	   *indirections = NIL;
 	int			location = exprLocation(result);
 	ListCell   *i;
 
 	/*
-	 * We have to split any field-selection operations apart from
-	 * subscripting.  Adjacent A_Indices nodes have to be treated as a single
-	 * multidimensional subscript operation.
+	 * Combine field names and subscripts into a single indirection list, as
+	 * some subscripting containers, such as hstore, support field access
+	 * using dot notation. Adjacent A_Indices nodes have to be treated as a
+	 * single multidimensional subscript operation.
 	 */
 	foreach(i, ind->indirection)
 	{
 		Node	   *n = lfirst(i);
 
-		if (IsA(n, A_Indices))
-			subscripts = lappend(subscripts, n);
-		else if (IsA(n, A_Star))
+		if (IsA(n, A_Indices) || IsA(n, String))
+			indirections = lappend(indirections, n);
+		else
 		{
+			Assert(IsA(n, A_Star));
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 					 errmsg("row expansion via \"*\" is not supported here"),
 					 parser_errposition(pstate, location)));
 		}
-		else
+	}
+
+	while (indirections)
+	{
+		/* try processing container subscripts first */
+		int			transformed_count = 0;
+		Node	   *newresult = (Node *)
+			transformContainerSubscripts(pstate,
+										 result,
+										 exprType(result),
+										 exprTypmod(result),
+										 indirections,
+										 false,
+										 &transformed_count);
+
+		if (!newresult)
 		{
-			Node	   *newresult;
+			/*
+			 * generic subscripting failed; falling back to field selection
+			 * for a composite type, or a single-argument function.
+			 */
+			Node	   *n;
+
+			Assert(indirections);
 
-			Assert(IsA(n, String));
+			n = linitial(indirections);
 
-			/* process subscripts before this field selection */
-			if (subscripts)
-				result = (Node *) transformContainerSubscripts(pstate,
-															   result,
-															   exprType(result),
-															   exprTypmod(result),
-															   subscripts,
-															   false);
-			subscripts = NIL;
+			if (!IsA(n, String))
+				ereport(ERROR,
+						(errcode(ERRCODE_DATATYPE_MISMATCH),
+						 errmsg("cannot subscript type %s because it does not support subscripting",
+								format_type_be(exprType(result))),
+						 parser_errposition(pstate, exprLocation(result))));
 
+			/* try to parse function or field selection */
 			newresult = ParseFuncOrColumn(pstate,
 										  list_make1(n),
 										  list_make1(result),
@@ -484,19 +505,18 @@ transformIndirection(ParseState *pstate, A_Indirection *ind)
 										  NULL,
 										  false,
 										  location);
-			if (newresult == NULL)
+
+			if (!newresult)
 				unknown_attribute(pstate, result, strVal(n), location);
-			result = newresult;
+			else
+				transformed_count = 1;
 		}
+
+		/* remove the processed indirections */
+		indirections = list_delete_first_n(indirections, transformed_count);
+
+		result = newresult;
 	}
-	/* process trailing subscripts, if any */
-	if (subscripts)
-		result = (Node *) transformContainerSubscripts(pstate,
-													   result,
-													   exprType(result),
-													   exprTypmod(result),
-													   subscripts,
-													   false);
 
 	return result;
 }
diff --git a/src/backend/parser/parse_node.c b/src/backend/parser/parse_node.c
index dacd851cffd..d26026717b5 100644
--- a/src/backend/parser/parse_node.c
+++ b/src/backend/parser/parse_node.c
@@ -238,20 +238,22 @@ transformContainerType(Oid *containerType, int32 *containerTypmod)
  * containerTypMod	typmod for the container
  * indirection		Untransformed list of subscripts (must not be NIL)
  * isAssignment		True if this will become a container assignment.
- */
+ * nSubscripts		Output parameter for number of transformed subscripts.
+*/
 SubscriptingRef *
 transformContainerSubscripts(ParseState *pstate,
 							 Node *containerBase,
 							 Oid containerType,
 							 int32 containerTypMod,
 							 List *indirection,
-							 bool isAssignment)
+							 bool isAssignment,
+							 int *nSubscripts)
 {
 	SubscriptingRef *sbsref;
 	const SubscriptRoutines *sbsroutines;
 	Oid			elementType;
-	bool		isSlice = false;
-	ListCell   *idx;
+
+	*nSubscripts = 0;
 
 	/*
 	 * Determine the actual container type, smashing any domain.  In the
@@ -267,28 +269,15 @@ transformContainerSubscripts(ParseState *pstate,
 	 */
 	sbsroutines = getSubscriptingRoutines(containerType, &elementType);
 	if (!sbsroutines)
+	{
+		if (!isAssignment)
+			return NULL;
+
 		ereport(ERROR,
 				(errcode(ERRCODE_DATATYPE_MISMATCH),
 				 errmsg("cannot subscript type %s because it does not support subscripting",
 						format_type_be(containerType)),
 				 parser_errposition(pstate, exprLocation(containerBase))));
-
-	/*
-	 * Detect whether any of the indirection items are slice specifiers.
-	 *
-	 * A list containing only simple subscripts refers to a single container
-	 * element.  If any of the items are slice specifiers (lower:upper), then
-	 * the subscript expression means a container slice operation.
-	 */
-	foreach(idx, indirection)
-	{
-		A_Indices  *ai = lfirst_node(A_Indices, idx);
-
-		if (ai->is_slice)
-		{
-			isSlice = true;
-			break;
-		}
 	}
 
 	/*
@@ -309,8 +298,65 @@ transformContainerSubscripts(ParseState *pstate,
 	 * Call the container-type-specific logic to transform the subscripts and
 	 * determine the subscripting result type.
 	 */
-	sbsroutines->transform(sbsref, indirection, pstate,
-						   isSlice, isAssignment);
+	Assert((sbsroutines->transform_partial == NULL) ^ (sbsroutines->transform == NULL));
+	if (sbsroutines->transform_partial != NULL)
+	{
+		/*
+		 * If the container type provides a partial transform function, use it
+		 * here. This function can accept any node types in the indirection
+		 * list as input, and is responsible for identifying and transforming
+		 * as many leading elements as it can handle, which may be only a
+		 * prefix of the indirection list. For example, it might process
+		 * A_Indices nodes, String nodes (for hstore dot-notation), or other
+		 * node types, depending on the container's requirements. It returns
+		 * the number of elements it transformed.
+		 */
+		*nSubscripts = sbsroutines->transform_partial(sbsref, indirection, pstate, isAssignment);
+	}
+	else
+	{
+		/*
+		 * Full transform only accepts bracket subscripts (A_Indices nodes).
+		 * We pre-collect the leading A_Indices nodes from the indirection
+		 * list, then call the transform function to process this prefix of
+		 * subscripts.
+		 */
+		List	   *subscriptlist = NIL;
+		ListCell   *lc;
+		bool		isSlice = false;
+
+		/* Collect leading A_Indices subscripts */
+		foreach(lc, indirection)
+		{
+			Node	   *n = lfirst(lc);
+
+			if (IsA(n, A_Indices))
+			{
+				A_Indices  *ai = (A_Indices *) n;
+
+				subscriptlist = lappend(subscriptlist, n);
+				if (ai->is_slice)
+					isSlice = true;
+			}
+			else
+				break;
+		}
+
+		if (subscriptlist)
+			sbsroutines->transform(sbsref, subscriptlist, pstate, isSlice, isAssignment);
+
+		*nSubscripts = list_length(subscriptlist);
+	}
+
+	if (*nSubscripts == 0)
+	{
+		/* Fallback to field selection in caller */
+		if (!isAssignment)
+			return NULL;
+
+		/* This should not happen with well-behaved transform functions */
+		elog(ERROR, "subscripting transform function failed to consume any indirection elements");
+	}
 
 	/*
 	 * Verify we got a valid type (this defends, for example, against someone
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index b5a2f915b67..ccfebc2955a 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -922,6 +922,7 @@ transformAssignmentSubscripts(ParseState *pstate,
 	Oid			typeNeeded;
 	int32		typmodNeeded;
 	Oid			collationNeeded;
+	int			nSubscripts = 0;
 
 	Assert(subscripts != NIL);
 
@@ -936,7 +937,10 @@ transformAssignmentSubscripts(ParseState *pstate,
 										  containerType,
 										  containerTypMod,
 										  subscripts,
-										  true);
+										  true,
+										  &nSubscripts);
+
+	Assert(nSubscripts == list_length(subscripts));
 
 	typeNeeded = sbsref->refrestype;
 	typmodNeeded = sbsref->reftypmod;
diff --git a/src/backend/utils/adt/arraysubs.c b/src/backend/utils/adt/arraysubs.c
index 63c78ed12cd..fb60845f56a 100644
--- a/src/backend/utils/adt/arraysubs.c
+++ b/src/backend/utils/adt/arraysubs.c
@@ -541,6 +541,7 @@ array_subscript_handler(PG_FUNCTION_ARGS)
 {
 	static const SubscriptRoutines sbsroutines = {
 		.transform = array_subscript_transform,
+		.transform_partial = NULL,
 		.exec_setup = array_exec_setup,
 		.fetch_strict = true,	/* fetch returns NULL for NULL inputs */
 		.fetch_leakproof = true,	/* fetch returns NULL for bad subscript */
@@ -568,6 +569,7 @@ raw_array_subscript_handler(PG_FUNCTION_ARGS)
 {
 	static const SubscriptRoutines sbsroutines = {
 		.transform = array_subscript_transform,
+		.transform_partial = NULL,
 		.exec_setup = array_exec_setup,
 		.fetch_strict = true,	/* fetch returns NULL for NULL inputs */
 		.fetch_leakproof = true,	/* fetch returns NULL for bad subscript */
diff --git a/src/backend/utils/adt/jsonbsubs.c b/src/backend/utils/adt/jsonbsubs.c
index 6bf55f19d6a..85a8d6a0f5d 100644
--- a/src/backend/utils/adt/jsonbsubs.c
+++ b/src/backend/utils/adt/jsonbsubs.c
@@ -403,6 +403,7 @@ jsonb_subscript_handler(PG_FUNCTION_ARGS)
 {
 	static const SubscriptRoutines sbsroutines = {
 		.transform = jsonb_subscript_transform,
+		.transform_partial = NULL,
 		.exec_setup = jsonb_exec_setup,
 		.fetch_strict = true,	/* fetch returns NULL for NULL inputs */
 		.fetch_leakproof = true,	/* fetch returns NULL for bad subscript */
diff --git a/src/include/nodes/subscripting.h b/src/include/nodes/subscripting.h
index 301f21dac2f..1e4957e2cd8 100644
--- a/src/include/nodes/subscripting.h
+++ b/src/include/nodes/subscripting.h
@@ -98,6 +98,25 @@ typedef void (*SubscriptTransform) (SubscriptingRef *sbsref,
 									bool isSlice,
 									bool isAssignment);
 
+/*
+ * SubscriptTransformPartial is an alternative to SubscriptTransform for
+ * container types that can accept non-A_Indices indirection as input
+ * (e.g., hstore accepts dot-notation (String node) for field access).
+
+ * It may transform a prefix of the indirection list and leave the rest
+ * unprocessed. It returns the number of indirections it transformed.
+ * The caller will then remove that many items from the head of the
+ * list, and handle the remaining indirections differently or to raise
+ * an error as needed.
+ *
+ * If transform_partial is NULL, the complete transform function is used,
+ * which accepts only A_Indices (bracket) nodes.
+ */
+typedef int (*SubscriptTransformPartial) (SubscriptingRef *sbsref,
+										  List *indirection,
+										  ParseState *pstate,
+										  bool isAssignment);
+
 /*
  * The exec_setup method is called during executor-startup compilation of a
  * SubscriptingRef node in an expression.  It must fill *methods with pointers
@@ -157,7 +176,10 @@ typedef void (*SubscriptExecSetup) (const SubscriptingRef *sbsref,
 /* Struct returned by the SQL-visible subscript handler function */
 typedef struct SubscriptRoutines
 {
-	SubscriptTransform transform;	/* parse analysis function */
+	SubscriptTransform transform;	/* parse analysis function, or NULL */
+	SubscriptTransformPartial transform_partial;	/* alternative parse
+													 * analysis function, or
+													 * NULL */
 	SubscriptExecSetup exec_setup;	/* expression compilation function */
 	bool		fetch_strict;	/* is fetch SubscriptingRef strict? */
 	bool		fetch_leakproof;	/* is fetch SubscriptingRef leakproof? */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index a9bffb8a78f..e72d7298821 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -362,7 +362,8 @@ extern SubscriptingRef *transformContainerSubscripts(ParseState *pstate,
 													 Oid containerType,
 													 int32 containerTypMod,
 													 List *indirection,
-													 bool isAssignment);
+													 bool isAssignment,
+													 int *consumed_count);
 extern Const *make_const(ParseState *pstate, A_Const *aconst);
 
 #endif							/* PARSE_NODE_H */
-- 
2.50.1 (Apple Git-155)

