From cee7b780aae0141e74cc34a6aebc8e9c26dc1442 Mon Sep 17 00:00:00 2001 From: Claes Jakobsson Date: Mon, 26 Sep 2011 23:23:39 +0200 Subject: [PATCH] Added json_set_value(obj, path, value) which the item at path in obj to the supplied value --- Changes | 1 + pg-json--0.0.2.sql | 5 ++ pg-json.c | 128 ++++++++++++++++++++++++++++++++++++++++++++- test.sql | 5 ++ 4 files changed, 137 insertions(+), 2 deletions(-) diff --git a/Changes b/Changes index e8865ae..4d86e5b 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,7 @@ Revision history for Postgres extension pg-json 0.0.2 ... + - added json_set_value(json, text, json) which sets the value of a json value at a given path - added json_concat(json, json) which concatenates two structures and it's operator equivalent || 0.0.1 Mon Sep 05 diff --git a/pg-json--0.0.2.sql b/pg-json--0.0.2.sql index d56d9c9..50356ec 100644 --- a/pg-json--0.0.2.sql +++ b/pg-json--0.0.2.sql @@ -24,6 +24,11 @@ CREATE OR REPLACE FUNCTION json_get_value(data json, path text) AS '$libdir/pg-json' LANGUAGE C IMMUTABLE STRICT; +CREATE OR REPLACE FUNCTION json_set_value(data json, path text, value json) + RETURNS json + AS '$libdir/pg-json' + LANGUAGE C IMMUTABLE STRICT; + CREATE OR REPLACE FUNCTION json_equals(this json, that json) RETURNS boolean AS '$libdir/pg-json' diff --git a/pg-json.c b/pg-json.c index 25af31d..56aff02 100644 --- a/pg-json.c +++ b/pg-json.c @@ -63,8 +63,6 @@ Datum json_out(PG_FUNCTION_ARGS) { PG_RETURN_CSTRING(result); } -PG_FUNCTION_INFO_V1(json_get_value); - static json_t *get_value_at_path(json_t *obj, const char *path) { long ix = 0, offset = 0; json_t *rv = NULL; @@ -175,6 +173,8 @@ static text *json_t_to_text(json_t *rv) { return NULL; } +PG_FUNCTION_INFO_V1(json_get_value); + Datum json_get_value(PG_FUNCTION_ARGS) { Json* json = (Json *) PG_GETARG_POINTER(0); @@ -205,6 +205,130 @@ Datum json_get_value(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(t); } +PG_FUNCTION_INFO_V1(json_set_value); + +static void set_value_at_path(json_t *obj, const char *path, json_t *new_v) { + long ix = 0, offset = 0; + char *key; + const char *orig_path = path; + + if (*path == '[') { + if (!json_is_array(obj)) { + elog(ERROR, "Object is not an array for path '%s'", path); + return; + } + + offset = 1; + while (isdigit(path[offset])) { + offset++; + } + if (offset == 1) { + elog(ERROR, "Missing array index"); + return; + } + if (path[offset] != ']') { + elog(ERROR, "Invalid path, expected ']' but got '%c'", path[offset]); + return; + } + + ix = strtol(path + 1, NULL, 10); + + if (ix >= json_array_size(obj)) { + elog(ERROR, "Subscript out of range: %d > %d", (int) ix, (int) json_array_size(obj) - 1); + return; + } + + if (path[offset + 1] == '\0') { + json_array_set(obj, ix, new_v); + return; + } + else { + obj = json_array_get(obj, ix); + if (obj == NULL) { + return; + } + set_value_at_path(obj, path + offset + 1, new_v); + } + } + else { + /* Skip separators */ + if (*path == '.') { path++; } + + while(path[offset] != '\0' && path[offset] != '[' && path[offset] != '.') { + offset++; + } + + if (offset == 0) { + elog(ERROR, "No key at %s:%d", __FILE__, __LINE__); + return; + } + + key = calloc(offset + 1, sizeof(char)); + strncpy(key, path, offset); + + if (path[offset] == '\0') { + if (json_object_set_new(obj, key, new_v) != 0) { + elog(ERROR, "Failed to set key '%s'", key); + free(key); + return; + } + } + else { + obj = json_object_get(obj, key); + free(key); + + if (obj == NULL) { + return; + } + + set_value_at_path(obj, path + offset, new_v); + } + } +} + +Datum json_set_value(PG_FUNCTION_ARGS) +{ + Json* json; + size_t len; + json_t* root; + json_t* sv; + char* str; + + json_error_t error; + + json = (Json *) PG_GETARG_POINTER(0); + len = VARSIZE_ANY_EXHDR(json); + root = json_loadb(VARDATA(json), len, 0, &error); + if (!root) { + elog(ERROR, "Failed to parse json: %s", error.text); + } + + json = (Json *) PG_GETARG_POINTER(2); + len = VARSIZE_ANY_EXHDR(json); + sv = json_loadb(VARDATA(json), len, 0, &error); + if (!sv) { + elog(ERROR, "Failed to parse value: %s", error.text); + } + + set_value_at_path(root, (const char *) text_to_cstring(PG_GETARG_TEXT_P(1)), sv); + + str = json_dumps(root, JSON_COMPACT); + + /* Avoid leaks */ + json_decref(root); + json_decref(sv); + + /* Convert back to a json type */ + len = strlen(str); + json = (Json *) palloc(len + VARHDRSZ); + SET_VARSIZE(json, len + VARHDRSZ); + memcpy(VARDATA(json), str, len); + + free(str); + + PG_RETURN_POINTER(json); +} + /* Comparision */ static int json_compare(Json *ja, Json *jb) { size_t len; diff --git a/test.sql b/test.sql index 39863dc..104b14d 100644 --- a/test.sql +++ b/test.sql @@ -5,6 +5,7 @@ SELECT json_get_value('{"foo": "bar", "quax": "zorg"}','quax') = 'zorg'; SELECT json_get_value('{"foo": { "quax": "zorg" } }','foo.quax') = 'zorg'; SELECT json_get_value('{"foo": [{ "quax": "zorg" }] }','foo[0].quax') = 'zorg'; + -- ~ is the operator version of json_get_value SELECT '{"foo":"bar"}'::json ~ 'foo' = 'bar'; @@ -29,3 +30,7 @@ SELECT json_concat('[1, 2, 3]','[4, 5, 6]') = '[1, 2, 3, 4, 5, 6]'; SELECT '{"a":"b"}'::json || '{"c":"d"}'::json = '{"a":"b","c":"d"}'::json; SELECT '[1, 2, 3]'::json || '[4, 5, 6]'::json = '[1, 2, 3, 4, 5, 6]'::json; +-- Setting by path +SELECT json_equals(json_set_value('{"foo":"bar"}', 'foo', '{"baz":"quax"}'), '{"foo":{"baz":"quax"}}'); +SELECT json_equals(json_set_value('[1,2,3]', '[1]', '[1,2]'), '[1,[1,2],3]'); +SELECT json_equals(json_set_value('{"foo":[1,{"quax":1}]}', 'foo[1].quax', '[1,2]'), '{"foo":[1,{"quax":[1,2]}]}');