From 912ad135bbc2ae9b041635d588b724336fc69549 Mon Sep 17 00:00:00 2001
From: Nikita Glukhov <n.gluhov@postgrespro.ru>
Date: Thu, 6 Apr 2017 23:34:14 +0300
Subject: [PATCH v1 5/8] Add jsonpath object constructors

---
 doc/src/sgml/func.sgml                       |  7 +++
 src/backend/utils/adt/jsonpath.c             | 70 +++++++++++++++++++++++++++-
 src/backend/utils/adt/jsonpath_exec.c        | 56 ++++++++++++++++++++++
 src/backend/utils/adt/jsonpath_gram.y        | 26 ++++++++++-
 src/include/utils/jsonpath.h                 | 18 +++++++
 src/test/regress/expected/jsonb_jsonpath.out | 44 +++++++++++++++++
 src/test/regress/expected/jsonpath.out       | 18 +++++++
 src/test/regress/sql/jsonb_jsonpath.sql      | 10 ++++
 src/test/regress/sql/jsonpath.sql            |  4 ++
 9 files changed, 251 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 7347e71..2fd1bae 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -12639,6 +12639,13 @@ table2-mapping
         <entry><literal>pg [$[*], 4, 5]</literal></entry>
         <entry><literal>[1, 2, 3, 4, 5]</literal></entry>
        </row>
+       <row>
+        <entry>Object constructor</entry>
+        <entry>Construct a JSON object by enumeration of its fields enclosed in braces</entry>
+        <entry><literal>{"x": "y"}</literal></entry>
+        <entry><literal>pg {a: 1, "b c": $.x}</literal></entry>
+        <entry><literal>{"a": 1, "b c": "y"}</literal></entry>
+       </row>
       </tbody>
      </tgroup>
     </table>
diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c
index 16031ba..3415c71 100644
--- a/src/backend/utils/adt/jsonpath.c
+++ b/src/backend/utils/adt/jsonpath.c
@@ -479,6 +479,40 @@ flattenJsonPathParseItem(JsonPathEncodingContext *cxt, JsonPathParseItem *item,
 				}
 			}
 			break;
+		case jpiObject:
+			{
+				int32		nfields = list_length(item->value.object.fields);
+				ListCell   *lc;
+				int			offset;
+
+				checkJsonPathExtensionsEnabled(cxt, item->type);
+
+				appendBinaryStringInfo(buf, (char *) &nfields, sizeof(nfields));
+
+				offset = buf->len;
+
+				appendStringInfoSpaces(buf, sizeof(int32) * 2 * nfields);
+
+				foreach(lc, item->value.object.fields)
+				{
+					JsonPathParseItem *field = lfirst(lc);
+					int32		keypos =
+						flattenJsonPathParseItem(cxt, field->value.args.left,
+												 nestingLevel,
+												 insideArraySubscript);
+					int32		valpos =
+						flattenJsonPathParseItem(cxt, field->value.args.right,
+												 nestingLevel,
+												 insideArraySubscript);
+					int32	   *ppos = (int32 *) &buf->data[offset];
+
+					ppos[0] = keypos - pos;
+					ppos[1] = valpos - pos;
+
+					offset += 2 * sizeof(int32);
+				}
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized jsonpath item type: %d", item->type);
 	}
@@ -795,6 +829,26 @@ printJsonPathItem(StringInfo buf, JsonPathItem *v, bool inKey,
 			}
 			appendStringInfoChar(buf, ']');
 			break;
+		case jpiObject:
+			appendStringInfoChar(buf, '{');
+
+			for (i = 0; i < v->content.object.nfields; i++)
+			{
+				JsonPathItem key;
+				JsonPathItem val;
+
+				jspGetObjectField(v, i, &key, &val);
+
+				if (i)
+					appendBinaryStringInfo(buf, ", ", 2);
+
+				printJsonPathItem(buf, &key, false, false);
+				appendBinaryStringInfo(buf, ": ", 2);
+				printJsonPathItem(buf, &val, false, val.type == jpiSequence);
+			}
+
+			appendStringInfoChar(buf, '}');
+			break;
 		default:
 			elog(ERROR, "unrecognized jsonpath item type: %d", v->type);
 	}
@@ -1011,6 +1065,11 @@ jspInitByBuffer(JsonPathItem *v, char *base, int32 pos)
 			read_int32_n(v->content.sequence.elems, base, pos,
 						 v->content.sequence.nelems);
 			break;
+		case jpiObject:
+			read_int32(v->content.object.nfields, base, pos);
+			read_int32_n(v->content.object.fields, base, pos,
+						 v->content.object.nfields * 2);
+			break;
 		default:
 			elog(ERROR, "unrecognized jsonpath item type: %d", v->type);
 	}
@@ -1078,7 +1137,8 @@ jspGetNext(JsonPathItem *v, JsonPathItem *a)
 			   v->type == jpiKeyValue ||
 			   v->type == jpiStartsWith ||
 			   v->type == jpiSequence ||
-			   v->type == jpiArray);
+			   v->type == jpiArray ||
+			   v->type == jpiObject);
 
 		if (a)
 			jspInitByBuffer(a, v->base, v->nextPos);
@@ -1181,3 +1241,11 @@ jspGetSequenceElement(JsonPathItem *v, int i, JsonPathItem *elem)
 
 	jspInitByBuffer(elem, v->base, v->content.sequence.elems[i]);
 }
+
+void
+jspGetObjectField(JsonPathItem *v, int i, JsonPathItem *key, JsonPathItem *val)
+{
+	Assert(v->type == jpiObject);
+	jspInitByBuffer(key, v->base, v->content.object.fields[i].key);
+	jspInitByBuffer(val, v->base, v->content.object.fields[i].val);
+}
diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c
index 1dee30a..8afe56b 100644
--- a/src/backend/utils/adt/jsonpath_exec.c
+++ b/src/backend/utils/adt/jsonpath_exec.c
@@ -1194,6 +1194,62 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp,
 				break;
 			}
 
+		case jpiObject:
+			{
+				JsonbParseState *ps = NULL;
+				int			i;
+
+				pushJsonbValue(&ps, WJB_BEGIN_OBJECT, NULL);
+
+				for (i = 0; i < jsp->content.object.nfields; i++)
+				{
+					JsonbValue *jbv;
+					JsonbValue	jbvtmp;
+					JsonPathItem key;
+					JsonPathItem val;
+					JsonValueList key_list = {0};
+					JsonValueList val_list = {0};
+
+					jspGetObjectField(jsp, i, &key, &val);
+
+					res = executeItem(cxt, &key, jb, &key_list);
+					if (jperIsError(res))
+						return res;
+
+					if (JsonValueListLength(&key_list) != 1 ||
+						!(jbv = getScalar(JsonValueListHead(&key_list), jbvString)))
+						RETURN_ERROR(ereport(ERROR,
+											 (errcode(ERRCODE_SQL_JSON_SCALAR_REQUIRED),
+											  errmsg("key in jsonpath object constructor is not a single string"))));
+
+					res = executeItem(cxt, &val, jb, &val_list);
+					if (jperIsError(res))
+						return res;
+
+					if (jspIgnoreStructuralErrors(cxt) &&
+						JsonValueListIsEmpty(&val_list))
+						continue;	/* skip empty fields in lax mode */
+
+					if (JsonValueListLength(&val_list) != 1)
+						RETURN_ERROR(ereport(ERROR,
+											 (errcode(ERRCODE_SINGLETON_SQL_JSON_ITEM_REQUIRED),
+											  errmsg("value in jsonpath object constructor is not single"),
+											  errhint("Use jsonpath array syntax to wrap multi-item sequences into arrays"))));
+
+					pushJsonbValue(&ps, WJB_KEY, jbv);
+
+					jbv = JsonValueListHead(&val_list);
+					jbv = wrapJsonObjectOrArray(jbv, &jbvtmp);
+
+					pushJsonbValue(&ps, WJB_VALUE, jbv);
+				}
+
+				jb = pushJsonbValue(&ps, WJB_END_OBJECT, NULL);
+
+				res = executeNextItem(cxt, jsp, NULL, jb, found, false);
+				break;
+			}
+
 		default:
 			elog(ERROR, "unrecognized jsonpath item type: %d", jsp->type);
 	}
diff --git a/src/backend/utils/adt/jsonpath_gram.y b/src/backend/utils/adt/jsonpath_gram.y
index 3507c1f..93bbd55 100644
--- a/src/backend/utils/adt/jsonpath_gram.y
+++ b/src/backend/utils/adt/jsonpath_gram.y
@@ -57,6 +57,7 @@ static JsonPathParseItem *makeItemLikeRegex(JsonPathParseItem *expr,
 											JsonPathString *pattern,
 											JsonPathString *flags);
 static JsonPathParseItem *makeItemSequence(List *elems);
+static JsonPathParseItem *makeItemObject(List *fields);
 
 /*
  * Bison doesn't allocate anything that needs to live across parser calls,
@@ -103,8 +104,9 @@ static JsonPathParseItem *makeItemSequence(List *elems);
 					any_path accessor_op key predicate delimited_predicate
 					index_elem starts_with_initial expr_or_predicate
 					datetime_template opt_datetime_template expr_seq expr_or_seq
+					object_field
 
-%type	<elems>		accessor_expr expr_list
+%type	<elems>		accessor_expr expr_list object_field_list
 
 %type	<indexs>	index_list
 
@@ -219,6 +221,18 @@ path_primary:
 	| '(' expr_seq ')'				{ $$ = $2; }
 	| '[' ']'						{ $$ = makeItemUnary(jpiArray, NULL); }
 	| '[' expr_or_seq ']'			{ $$ = makeItemUnary(jpiArray, $2); }
+	| '{' object_field_list '}'		{ $$ = makeItemObject($2); }
+	;
+
+object_field_list:
+	/* EMPTY */								{ $$ = NIL; }
+	| object_field							{ $$ = list_make1($1); }
+	| object_field_list ',' object_field	{ $$ = lappend($1, $3); }
+	;
+
+object_field:
+	key_name ':' expr_or_predicate
+		{ $$ = makeItemBinary(jpiObjectField, makeItemString(&$1), $3); }
 	;
 
 accessor_expr:
@@ -578,6 +592,16 @@ makeItemSequence(List *elems)
 	return v;
 }
 
+static JsonPathParseItem *
+makeItemObject(List *fields)
+{
+	JsonPathParseItem *v = makeItemType(jpiObject);
+
+	v->value.object.fields = fields;
+
+	return v;
+}
+
 /*
  * Convert from XQuery regex flags to those recognized by our regex library.
  */
diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h
index 1b2b964..9aefe70 100644
--- a/src/include/utils/jsonpath.h
+++ b/src/include/utils/jsonpath.h
@@ -89,6 +89,8 @@ typedef enum JsonPathItemType
 	jpiLikeRegex,				/* LIKE_REGEX predicate */
 	jpiSequence,				/* sequence constructor: 'expr, ...' */
 	jpiArray,					/* array constructor: '[expr, ...]' */
+	jpiObject,					/* object constructor: '{ key : value, ... }' */
+	jpiObjectField,				/* element of object constructor: 'key : value' */
 } JsonPathItemType;
 
 /* XQuery regex mode flags for LIKE_REGEX predicate */
@@ -157,6 +159,16 @@ typedef struct JsonPathItem
 
 		struct
 		{
+			int32		nfields;
+			struct
+			{
+				int32		key;
+				int32		val;
+			}		   *fields;
+		}			object;
+
+		struct
+		{
 			char	   *data;	/* for bool, numeric and string/key */
 			int32		datalen;	/* filled only for string/key */
 		}			value;
@@ -185,6 +197,8 @@ extern char *jspGetString(JsonPathItem *v, int32 *len);
 extern bool jspGetArraySubscript(JsonPathItem *v, JsonPathItem *from,
 								 JsonPathItem *to, int i);
 extern void jspGetSequenceElement(JsonPathItem *v, int i, JsonPathItem *elem);
+extern void jspGetObjectField(JsonPathItem *v, int i,
+							  JsonPathItem *key, JsonPathItem *val);
 
 extern const char *jspOperationName(JsonPathItemType type);
 
@@ -242,6 +256,10 @@ struct JsonPathParseItem
 			List   *elems;
 		} sequence;
 
+		struct {
+			List   *fields;
+		} object;
+
 		/* scalars */
 		Numeric numeric;
 		bool		boolean;
diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out
index cd56876..8a12757 100644
--- a/src/test/regress/expected/jsonb_jsonpath.out
+++ b/src/test/regress/expected/jsonb_jsonpath.out
@@ -2635,3 +2635,47 @@ select jsonb_path_query('[[1, 2], [3, 4, 5], [], [6, 7]]', 'pg [$[*][*] ? (@ > 3
  [4, 5, 6, 7]
 (1 row)
 
+-- extension: object constructors
+select jsonb_path_query('[1, 2, 3]', 'pg {}');
+ jsonb_path_query 
+------------------
+ {}
+(1 row)
+
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": [$[*], 4, 5]}');
+        jsonb_path_query        
+--------------------------------
+ {"a": 5, "b": [1, 2, 3, 4, 5]}
+(1 row)
+
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": [$[*], 4, 5]}.*');
+ jsonb_path_query 
+------------------
+ 5
+ [1, 2, 3, 4, 5]
+(2 rows)
+
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": [$[*], 4, 5]}[*]');
+        jsonb_path_query        
+--------------------------------
+ {"a": 5, "b": [1, 2, 3, 4, 5]}
+(1 row)
+
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": ($[*], 4, 5)}');
+ERROR:  value in jsonpath object constructor is not single
+HINT:  Use jsonpath array syntax to wrap multi-item sequences into arrays
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": {x: $, y: $[1] > 2, z: "foo"}}');
+                    jsonb_path_query                     
+---------------------------------------------------------
+ {"a": 5, "b": {"x": [1, 2, 3], "y": false, "z": "foo"}}
+(1 row)
+
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": $[*] ? (@ > 3), c: "foo"}');
+   jsonb_path_query   
+----------------------
+ {"a": 5, "c": "foo"}
+(1 row)
+
+select jsonb_path_query('[1, 2, 3]', 'pg strict {a: 2 + 3, "b": $[*] ? (@ > 3), c: "foo"}');
+ERROR:  value in jsonpath object constructor is not single
+HINT:  Use jsonpath array syntax to wrap multi-item sequences into arrays
diff --git a/src/test/regress/expected/jsonpath.out b/src/test/regress/expected/jsonpath.out
index e212deb..a669327 100644
--- a/src/test/regress/expected/jsonpath.out
+++ b/src/test/regress/expected/jsonpath.out
@@ -616,6 +616,24 @@ select 'pg [[1, 2], ([(3, 4, 5), 6], []), $.a[*]]'::jsonpath;
  pg [[1, 2], ([(3, 4, 5), 6], []), $."a"[*]]
 (1 row)
 
+select 'pg {}'::jsonpath;
+ jsonpath 
+----------
+ pg {}
+(1 row)
+
+select 'pg {a: 1 + 2}'::jsonpath;
+    jsonpath     
+-----------------
+ pg {"a": 1 + 2}
+(1 row)
+
+select 'pg {a: 1 + 2, b : (1,2), c: [$[*],4,5], d: { "e e e": "f f f" }}'::jsonpath;
+                                 jsonpath                                 
+--------------------------------------------------------------------------
+ pg {"a": 1 + 2, "b": (1, 2), "c": [$[*], 4, 5], "d": {"e e e": "f f f"}}
+(1 row)
+
 select '$ ? (@.a < 1)'::jsonpath;
    jsonpath    
 ---------------
diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql
index be5c7c8..f4de5d0 100644
--- a/src/test/regress/sql/jsonb_jsonpath.sql
+++ b/src/test/regress/sql/jsonb_jsonpath.sql
@@ -598,3 +598,13 @@ select jsonb_path_query('[1, 2, 3]', 'pg [(1, (2, $[*])), (4, 5)]');
 select jsonb_path_query('[1, 2, 3]', 'pg [[1, 2], [$[*], 4], 5, [(1,2)?(@ > 5)]]');
 select jsonb_path_query('[1, 2, 3]', 'pg strict [1, 2, $[*].a, 4, 5]');
 select jsonb_path_query('[[1, 2], [3, 4, 5], [], [6, 7]]', 'pg [$[*][*] ? (@ > 3)]');
+
+-- extension: object constructors
+select jsonb_path_query('[1, 2, 3]', 'pg {}');
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": [$[*], 4, 5]}');
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": [$[*], 4, 5]}.*');
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": [$[*], 4, 5]}[*]');
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": ($[*], 4, 5)}');
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": {x: $, y: $[1] > 2, z: "foo"}}');
+select jsonb_path_query('[1, 2, 3]', 'pg {a: 2 + 3, "b": $[*] ? (@ > 3), c: "foo"}');
+select jsonb_path_query('[1, 2, 3]', 'pg strict {a: 2 + 3, "b": $[*] ? (@ > 3), c: "foo"}');
diff --git a/src/test/regress/sql/jsonpath.sql b/src/test/regress/sql/jsonpath.sql
index 23ae376..555470b 100644
--- a/src/test/regress/sql/jsonpath.sql
+++ b/src/test/regress/sql/jsonpath.sql
@@ -116,6 +116,10 @@ select 'pg $[(1, (2, $.a)), 3, (4, 5)]'::jsonpath;
 select 'pg []'::jsonpath;
 select 'pg [[1, 2], ([(3, 4, 5), 6], []), $.a[*]]'::jsonpath;
 
+select 'pg {}'::jsonpath;
+select 'pg {a: 1 + 2}'::jsonpath;
+select 'pg {a: 1 + 2, b : (1,2), c: [$[*],4,5], d: { "e e e": "f f f" }}'::jsonpath;
+
 select '$ ? (@.a < 1)'::jsonpath;
 select '$ ? (@.a < -1)'::jsonpath;
 select '$ ? (@.a < +1)'::jsonpath;
-- 
2.7.4

