Add json_strip_nulls and jsonb_strip_nulls functions.
authorAndrew Dunstan <andrew@dunslane.net>
Fri, 12 Dec 2014 14:00:43 +0000 (09:00 -0500)
committerAndrew Dunstan <andrew@dunslane.net>
Fri, 12 Dec 2014 14:00:43 +0000 (09:00 -0500)
The functions remove object fields, including in nested objects, that
have null as a value. In certain cases this can lead to considerably
smaller datums, with no loss of semantic information.

Andrew Dunstan, reviewed by Pavel Stehule.

doc/src/sgml/func.sgml
src/backend/utils/adt/jsonfuncs.c
src/include/catalog/catversion.h
src/include/catalog/pg_proc.h
src/include/utils/json.h
src/test/regress/expected/json.out
src/test/regress/expected/json_1.out
src/test/regress/expected/jsonb.out
src/test/regress/expected/jsonb_1.out
src/test/regress/sql/json.sql
src/test/regress/sql/jsonb.sql

index c3b6126943825e75d84557cf34577716b3890460..da138e1fee7f1597d28c5fba7ee8aafa56aa41c7 100644 (file)
@@ -10716,6 +10716,19 @@ table2-mapping
 </programlisting>
        </entry>
       </row>
+      <row>
+       <entry><para><literal>json_strip_nulls(from_json json)</literal>
+         </para><para><literal>jsonb_strip_nulls(from_json jsonb)</literal>
+       </para></entry>
+       <entry><para><type>json</type></para><para><type>jsonb</type></para></entry>
+       <entry>
+         Returns <replaceable>from_json</replaceable>
+         with all object fields that have null values omitted. Other null values
+         are untouched.
+       </entry>
+       <entry><literal>json_strip_nulls('[{"f1":1,"f2":null},2,null,3]')</literal></entry>
+       <entry><literal>[{"f1":1},2,null,3]</literal></entry>
+       </row>
      </tbody>
     </tgroup>
    </table>
@@ -10752,6 +10765,16 @@ table2-mapping
     </para>
   </note>
 
+  <note>
+    <para>
+      If the argument to <literal>json_strip_nulls</> contains duplicate
+      field names in any object, the result could be semantically somewhat
+      different, depending on the order in which they occur. This is not an
+      issue for <literal>jsonb_strip_nulls</> since jsonb values never have
+      duplicate object field names.
+    </para>
+  </note>
+
   <para>
     See also <xref linkend="functions-aggregate"> for the aggregate
     function <function>json_agg</function> which aggregates record
index 2d00dbed4bd67f564610476297735b91dd69f930..0a85ee81091ca9bb41a00976e79931ae55b8d945 100644 (file)
@@ -105,6 +105,15 @@ static void populate_recordset_object_end(void *state);
 static void populate_recordset_array_start(void *state);
 static void populate_recordset_array_element_start(void *state, bool isnull);
 
+/* semantic action functions for json_strip_nulls */
+static void sn_object_start(void *state);
+static void sn_object_end(void *state);
+static void sn_array_start(void *state);
+static void sn_array_end(void *state);
+static void sn_object_field_start (void *state, char *fname, bool isnull);
+static void sn_array_element_start (void *state, bool isnull);
+static void sn_scalar(void *state, char *token, JsonTokenType tokentype);
+
 /* worker function for populate_recordset and to_recordset */
 static Datum populate_recordset_worker(FunctionCallInfo fcinfo, const char *funcname,
                          bool have_record_arg);
@@ -225,6 +234,13 @@ typedef struct PopulateRecordsetState
    MemoryContext fn_mcxt;      /* used to stash IO funcs */
 } PopulateRecordsetState;
 
+/* state for json_strip_nulls */
+typedef struct StripnullState{
+   JsonLexContext *lex;
+   StringInfo  strval;
+   bool skip_next_null;
+} StripnullState;
+
 /* Turn a jsonb object into a record */
 static void make_row_from_rec_and_jsonb(Jsonb *element,
                            PopulateRecordsetState *state);
@@ -2996,3 +3012,184 @@ findJsonbValueFromContainerLen(JsonbContainer *container, uint32 flags,
 
    return findJsonbValueFromContainer(container, flags, &k);
 }
+
+/*
+ * Semantic actions for json_strip_nulls.
+ *
+ * Simply repeat the input on the output unless we encounter
+ * a null object field. State for this is set when the field
+ * is started and reset when the scalar action (which must be next)
+ * is called.
+ */
+
+static void
+sn_object_start(void *state)
+{
+   StripnullState *_state = (StripnullState *) state;
+   appendStringInfoCharMacro(_state->strval, '{');
+}
+
+static void
+sn_object_end(void *state)
+{
+   StripnullState *_state = (StripnullState *) state;
+   appendStringInfoCharMacro(_state->strval, '}');
+}
+
+static void
+sn_array_start(void *state)
+{
+   StripnullState *_state = (StripnullState *) state;
+   appendStringInfoCharMacro(_state->strval, '[');
+}
+
+static void
+sn_array_end(void *state)
+{
+   StripnullState *_state = (StripnullState *) state;
+   appendStringInfoCharMacro(_state->strval, ']');
+}
+
+static void
+sn_object_field_start (void *state, char *fname, bool isnull)
+{
+   StripnullState *_state = (StripnullState *) state;
+
+   if (isnull)
+   {
+       /*
+        * The next thing must be a scalar or isnull couldn't be true,
+        * so there is no danger of this state being carried down
+        * into a nested  object or array. The flag will be reset in the
+        * scalar action.
+        */
+       _state->skip_next_null = true;
+       return;
+   }
+
+   if (_state->strval->data[_state->strval->len - 1] != '{')
+       appendStringInfoCharMacro(_state->strval, ',');
+
+   /*
+    * Unfortunately we don't have the quoted and escaped string any more,
+    * so we have to re-escape it.
+    */
+   escape_json(_state->strval,fname);
+
+   appendStringInfoCharMacro(_state->strval, ':');
+}
+
+static void
+sn_array_element_start (void *state, bool isnull)
+{
+   StripnullState *_state = (StripnullState *) state;
+
+   if (_state->strval->data[_state->strval->len - 1] != '[')
+       appendStringInfoCharMacro(_state->strval, ',');
+}
+
+static void
+sn_scalar(void *state, char *token, JsonTokenType tokentype)
+{
+   StripnullState *_state = (StripnullState *) state;
+
+   if (_state->skip_next_null)
+   {
+       Assert (tokentype == JSON_TOKEN_NULL);
+       _state->skip_next_null = false;
+       return;
+   }
+
+   if (tokentype == JSON_TOKEN_STRING)
+       escape_json(_state->strval, token);
+   else
+       appendStringInfoString(_state->strval, token);
+}
+
+/*
+ * SQL function json_strip_nulls(json) -> json
+ */
+Datum
+json_strip_nulls(PG_FUNCTION_ARGS)
+{
+   text       *json = PG_GETARG_TEXT_P(0);
+   StripnullState  *state;
+   JsonLexContext *lex;
+   JsonSemAction *sem;
+
+   lex = makeJsonLexContext(json, true);
+   state = palloc0(sizeof(StripnullState));
+   sem = palloc0(sizeof(JsonSemAction));
+
+   state->strval = makeStringInfo();
+   state->skip_next_null = false;
+   state->lex = lex;
+
+   sem->semstate = (void *) state;
+   sem->object_start = sn_object_start;
+   sem->object_end = sn_object_end;
+   sem->array_start = sn_array_start;
+   sem->array_end = sn_array_end;
+   sem->scalar = sn_scalar;
+   sem->array_element_start = sn_array_element_start;
+   sem->object_field_start = sn_object_field_start;
+
+   pg_parse_json(lex, sem);
+
+   PG_RETURN_TEXT_P(cstring_to_text_with_len(state->strval->data,
+                                             state->strval->len));
+
+}
+
+/*
+ * SQL function jsonb_strip_nulls(jsonb) -> jsonb
+ */
+Datum
+jsonb_strip_nulls(PG_FUNCTION_ARGS)
+{
+   Jsonb * jb = PG_GETARG_JSONB(0);
+   JsonbIterator *it;
+   JsonbParseState *parseState = NULL;
+   JsonbValue *res = NULL;
+   int type;
+   JsonbValue v,k;
+   bool last_was_key = false;
+
+   if (JB_ROOT_IS_SCALAR(jb))
+       PG_RETURN_POINTER(jb);
+
+   it = JsonbIteratorInit(&jb->root);
+
+   while ((type = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
+   {
+       Assert( ! (type == WJB_KEY && last_was_key));
+
+       if (type == WJB_KEY)
+       {
+           /* stash the key until we know if it has a null value */
+           k = v;
+           last_was_key = true;
+           continue;
+       }
+
+       if (last_was_key)
+       {
+           /* if the last element was a key this one can't be */
+           last_was_key = false;
+
+           /* skip this field if value is null */
+           if (type == WJB_VALUE && v.type == jbvNull)
+               continue;
+
+           /* otherwise, do a delayed push of the key */
+           res = pushJsonbValue(&parseState, WJB_KEY, &k);
+       }
+
+       if (type == WJB_VALUE || type == WJB_ELEM)
+           res = pushJsonbValue(&parseState, type, &v);
+       else
+           res = pushJsonbValue(&parseState, type, NULL);
+   }
+
+   PG_RETURN_POINTER(JsonbValueToJsonb(res));
+}
index bf8397671e6683e619e308202ce5a26a7a4c3896..74bb0283e2d1a08f41f808e91ba447254a58c723 100644 (file)
@@ -53,6 +53,6 @@
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 201412022
+#define CATALOG_VERSION_NO 201412121
 
 #endif
index 910cfc6bb1e4bdb78182a79ce5716377eefe41a7..e7db60e95afeadaa9610bc66e3ce1b8ddbd15652 100644 (file)
@@ -4292,6 +4292,8 @@ DATA(insert OID = 3203 (  json_object  PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0
 DESCR("map text arrays of keys and values to json object");
 DATA(insert OID = 3176 (  to_json     PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 114 "2283" _null_ _null_ _null_ _null_ to_json _null_ _null_ _null_ ));
 DESCR("map input to json");
+DATA(insert OID = 3261 (  json_strip_nulls    PGNSP PGUID 12 1 0 0 0 f f f f t f i 1 0 114 "114" _null_ _null_ _null_ _null_ json_strip_nulls _null_ _null_ _null_ ));
+DESCR("remove object fields with null values from json");
 
 DATA(insert OID = 3947 (  json_object_field            PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 114 "114 25" _null_ _null_ "{from_json, field_name}" _null_ json_object_field _null_ _null_ _null_ ));
 DATA(insert OID = 3948 (  json_object_field_text   PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 25  "114 25" _null_ _null_ "{from_json, field_name}" _null_ json_object_field_text _null_ _null_ _null_ ));
@@ -4654,6 +4656,9 @@ DESCR("I/O");
 DATA(insert OID =  3803 (  jsonb_send      PGNSP PGUID 12 1 0 0 0 f f f f t f i 1 0 17 "3802" _null_ _null_ _null_ _null_  jsonb_send _null_ _null_ _null_ ));
 DESCR("I/O");
 
+DATA(insert OID = 3262 (  jsonb_strip_nulls       PGNSP PGUID 12 1 0 0 0 f f f f t f i 1 0 3802 "3802" _null_ _null_ _null_ _null_ jsonb_strip_nulls _null_ _null_ _null_ ));
+DESCR("remove object fields with null values from jsonb");
+
 DATA(insert OID = 3478 (  jsonb_object_field           PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 3802 "3802 25" _null_ _null_ "{from_json, field_name}" _null_ jsonb_object_field _null_ _null_ _null_ ));
 DATA(insert OID = 3214 (  jsonb_object_field_text  PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 25  "3802 25" _null_ _null_ "{from_json, field_name}" _null_ jsonb_object_field_text _null_ _null_ _null_ ));
 DATA(insert OID = 3215 (  jsonb_array_element      PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 3802 "3802 23" _null_ _null_ "{from_json, element_index}" _null_ jsonb_array_element _null_ _null_ _null_ ));
index 82cc48b7113e1b0bb30da53f5e96fedb85d18299..cada47e96e71a67f04cc1dfcfa0eeadedd9a277d 100644 (file)
@@ -63,6 +63,7 @@ extern Datum json_populate_record(PG_FUNCTION_ARGS);
 extern Datum json_populate_recordset(PG_FUNCTION_ARGS);
 extern Datum json_to_record(PG_FUNCTION_ARGS);
 extern Datum json_to_recordset(PG_FUNCTION_ARGS);
+extern Datum json_strip_nulls(PG_FUNCTION_ARGS);
 
 extern Datum jsonb_object_field(PG_FUNCTION_ARGS);
 extern Datum jsonb_object_field_text(PG_FUNCTION_ARGS);
@@ -80,5 +81,6 @@ extern Datum jsonb_populate_record(PG_FUNCTION_ARGS);
 extern Datum jsonb_populate_recordset(PG_FUNCTION_ARGS);
 extern Datum jsonb_to_record(PG_FUNCTION_ARGS);
 extern Datum jsonb_to_recordset(PG_FUNCTION_ARGS);
+extern Datum jsonb_strip_nulls(PG_FUNCTION_ARGS);
 
 #endif   /* JSON_H */
index bb4d9ed4bebca91f9c1e37b54fc1f8dbc17832f3..e435d3e16502b23e236007058a983aab2f200272 100644 (file)
@@ -1586,3 +1586,53 @@ select * from json_to_recordset('[{"a":1,"b":{"d":"foo"},"c":true},{"a":2,"c":fa
  2 | {"d":"bar"} | f
 (2 rows)
 
+-- json_strip_nulls
+select json_strip_nulls(null);
+ json_strip_nulls 
+------------------
+(1 row)
+
+select json_strip_nulls('1');
+ json_strip_nulls 
+------------------
+ 1
+(1 row)
+
+select json_strip_nulls('"a string"');
+ json_strip_nulls 
+------------------
+ "a string"
+(1 row)
+
+select json_strip_nulls('null');
+ json_strip_nulls 
+------------------
+ null
+(1 row)
+
+select json_strip_nulls('[1,2,null,3,4]');
+ json_strip_nulls 
+------------------
+ [1,2,null,3,4]
+(1 row)
+
+select json_strip_nulls('{"a":1,"b":null,"c":[2,null,3],"d":{"e":4,"f":null}}');
+          json_strip_nulls          
+------------------------------------
+ {"a":1,"c":[2,null,3],"d":{"e":4}}
+(1 row)
+
+select json_strip_nulls('[1,{"a":1,"b":null,"c":2},3]');
+  json_strip_nulls   
+---------------------
+ [1,{"a":1,"c":2},3]
+(1 row)
+
+-- an empty object is not null and should not be stripped
+select json_strip_nulls('{"a": {"b": null, "c": null}, "d": {} }');
+ json_strip_nulls 
+------------------
+ {"a":{},"d":{}}
+(1 row)
+
index 83c1d7d492c16dd35b1c51ca9fef4b3e9bbd967e..106b481fab91bf82a6b9c339875e2c2884d42272 100644 (file)
@@ -1582,3 +1582,53 @@ select * from json_to_recordset('[{"a":1,"b":{"d":"foo"},"c":true},{"a":2,"c":fa
  2 | {"d":"bar"} | f
 (2 rows)
 
+-- json_strip_nulls
+select json_strip_nulls(null);
+ json_strip_nulls 
+------------------
+(1 row)
+
+select json_strip_nulls('1');
+ json_strip_nulls 
+------------------
+ 1
+(1 row)
+
+select json_strip_nulls('"a string"');
+ json_strip_nulls 
+------------------
+ "a string"
+(1 row)
+
+select json_strip_nulls('null');
+ json_strip_nulls 
+------------------
+ null
+(1 row)
+
+select json_strip_nulls('[1,2,null,3,4]');
+ json_strip_nulls 
+------------------
+ [1,2,null,3,4]
+(1 row)
+
+select json_strip_nulls('{"a":1,"b":null,"c":[2,null,3],"d":{"e":4,"f":null}}');
+          json_strip_nulls          
+------------------------------------
+ {"a":1,"c":[2,null,3],"d":{"e":4}}
+(1 row)
+
+select json_strip_nulls('[1,{"a":1,"b":null,"c":2},3]');
+  json_strip_nulls   
+---------------------
+ [1,{"a":1,"c":2},3]
+(1 row)
+
+-- an empty object is not null and should not be stripped
+select json_strip_nulls('{"a": {"b": null, "c": null}, "d": {} }');
+ json_strip_nulls 
+------------------
+ {"a":{},"d":{}}
+(1 row)
+
index 9146f59435b62f4a4862dc79791b1aa313c87804..3e1d7694c1ae4f0340e0d12c136e054f78d552ed 100644 (file)
@@ -2472,3 +2472,53 @@ SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'e';
  f
 (1 row)
 
+-- jsonb_strip_nulls
+select jsonb_strip_nulls(null);
+ jsonb_strip_nulls 
+-------------------
+(1 row)
+
+select jsonb_strip_nulls('1');
+ jsonb_strip_nulls 
+-------------------
+ 1
+(1 row)
+
+select jsonb_strip_nulls('"a string"');
+ jsonb_strip_nulls 
+-------------------
+ "a string"
+(1 row)
+
+select jsonb_strip_nulls('null');
+ jsonb_strip_nulls 
+-------------------
+ null
+(1 row)
+
+select jsonb_strip_nulls('[1,2,null,3,4]');
+ jsonb_strip_nulls  
+--------------------
+ [1, 2, null, 3, 4]
+(1 row)
+
+select jsonb_strip_nulls('{"a":1,"b":null,"c":[2,null,3],"d":{"e":4,"f":null}}');
+             jsonb_strip_nulls              
+--------------------------------------------
+ {"a": 1, "c": [2, null, 3], "d": {"e": 4}}
+(1 row)
+
+select jsonb_strip_nulls('[1,{"a":1,"b":null,"c":2},3]');
+    jsonb_strip_nulls     
+--------------------------
+ [1, {"a": 1, "c": 2}, 3]
+(1 row)
+
+-- an empty object is not null and should not be stripped
+select jsonb_strip_nulls('{"a": {"b": null, "c": null}, "d": {} }');
+ jsonb_strip_nulls  
+--------------------
+ {"a": {}, "d": {}}
+(1 row)
+
index 83d61f8c7e0ddcf508abdba6ff895c98956aa56f..35da6f48106e34ecc2b58f1af8eb9914b64d79ca 100644 (file)
@@ -2472,3 +2472,53 @@ SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'e';
  f
 (1 row)
 
+-- jsonb_strip_nulls
+select jsonb_strip_nulls(null);
+ jsonb_strip_nulls 
+-------------------
+(1 row)
+
+select jsonb_strip_nulls('1');
+ jsonb_strip_nulls 
+-------------------
+ 1
+(1 row)
+
+select jsonb_strip_nulls('"a string"');
+ jsonb_strip_nulls 
+-------------------
+ "a string"
+(1 row)
+
+select jsonb_strip_nulls('null');
+ jsonb_strip_nulls 
+-------------------
+ null
+(1 row)
+
+select jsonb_strip_nulls('[1,2,null,3,4]');
+ jsonb_strip_nulls  
+--------------------
+ [1, 2, null, 3, 4]
+(1 row)
+
+select jsonb_strip_nulls('{"a":1,"b":null,"c":[2,null,3],"d":{"e":4,"f":null}}');
+             jsonb_strip_nulls              
+--------------------------------------------
+ {"a": 1, "c": [2, null, 3], "d": {"e": 4}}
+(1 row)
+
+select jsonb_strip_nulls('[1,{"a":1,"b":null,"c":2},3]');
+    jsonb_strip_nulls     
+--------------------------
+ [1, {"a": 1, "c": 2}, 3]
+(1 row)
+
+-- an empty object is not null and should not be stripped
+select jsonb_strip_nulls('{"a": {"b": null, "c": null}, "d": {} }');
+ jsonb_strip_nulls  
+--------------------
+ {"a": {}, "d": {}}
+(1 row)
+
index c9801321e09069570e7e79db341ba7848c7af065..36a6674ff91092e0dc525edfdff516102eaa5c9e 100644 (file)
@@ -509,3 +509,23 @@ select * from json_to_recordset('[{"a":1,"b":"foo","d":false},{"a":2,"b":"bar","
 
 select * from json_to_recordset('[{"a":1,"b":{"d":"foo"},"c":true},{"a":2,"c":false,"b":{"d":"bar"}}]')
     as x(a int, b json, c boolean);
+
+
+-- json_strip_nulls
+
+select json_strip_nulls(null);
+
+select json_strip_nulls('1');
+
+select json_strip_nulls('"a string"');
+
+select json_strip_nulls('null');
+
+select json_strip_nulls('[1,2,null,3,4]');
+
+select json_strip_nulls('{"a":1,"b":null,"c":[2,null,3],"d":{"e":4,"f":null}}');
+
+select json_strip_nulls('[1,{"a":1,"b":null,"c":2},3]');
+
+-- an empty object is not null and should not be stripped
+select json_strip_nulls('{"a": {"b": null, "c": null}, "d": {} }');
index f1ed021be2d3f4d2724428e4432b26e3aca00a08..e8e6117c530025e5bffce7d4804fbe22fb5c054d 100644 (file)
@@ -542,3 +542,22 @@ SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'b';
 SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'c';
 SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'd';
 SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'e';
+
+-- jsonb_strip_nulls
+
+select jsonb_strip_nulls(null);
+
+select jsonb_strip_nulls('1');
+
+select jsonb_strip_nulls('"a string"');
+
+select jsonb_strip_nulls('null');
+
+select jsonb_strip_nulls('[1,2,null,3,4]');
+
+select jsonb_strip_nulls('{"a":1,"b":null,"c":[2,null,3],"d":{"e":4,"f":null}}');
+
+select jsonb_strip_nulls('[1,{"a":1,"b":null,"c":2},3]');
+
+-- an empty object is not null and should not be stripped
+select jsonb_strip_nulls('{"a": {"b": null, "c": null}, "d": {} }');