($INBOX_DIR/description missing)help / color / mirror / Atom feed
[PATCH v44 3/3] Replace assuming a composite object on a scalar 107+ messages / 3 participants [nested] [flat]
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v44 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --i5uwshohlpbxyckd-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v45 3/3] Replace assuming a composite object on a scalar @ 2021-01-07 08:04 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-07 08:04 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 22 ++++++++++++++++++++-- src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 9af015d222..924762e128 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -612,8 +612,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu <literal>path</literal> argument in <literal>jsonb_set</literal> function, e.g. in case of arrays it is a 0-based operation or that negative integers that appear in <literal>path</literal> count from the end of JSON arrays. - The result of subscripting expressions is always jsonb data type. An - example of subscripting syntax: + The result of subscripting expressions is always jsonb data type. + + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Every + affected value must conform to the path defined by the subscript(s). If the + path contradicts structure of modified <type>jsonb</type> for any individual + value (e.g. path <literal>val['a']['b']['c']</literal> assumes keys + <literal>'a'</literal> and <literal>'b'</literal> have object values + assigned to them, but if <literal>val['a']</literal> or + <literal>val['b']</literal> is null, a string, or a number, then the path + contradicts with the existing structure), an error is raised even if other + values do conform. + </para> + <para> + + An example of subscripting syntax: <programlisting> -- Extract value by key SELECT ('{"a": 1}'::jsonb)['a']; @@ -628,6 +643,9 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- needs to be of jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if jsonb_field is {"a": 1} +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Select records using where clause with subscripting. Since the result of -- subscripting is jsonb and we basically want to compare two jsonb objects, we -- need to put the value in double quotes to be able to convert it to jsonb. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index b7c268b53f..0df808c36d 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --2v7pubbjx3p2om6u-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v49 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 39 +++++++++++++++++++++++------ src/backend/utils/adt/jsonfuncs.c | 27 ++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 ++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..e16dd6973d 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,24 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Subscript + paths must be traversible for all affected values insofar as they exist. For + instance, the path <literal>val['a']['b']['c']</literal> can be traversed all + the way to <literal>c</literal> if every <literal>val</literal>, + <literal>val['a']</literal>, and <literal>val['a']['b']</literal> is an + object. If any <literal>val['a']</literal> or <literal>val['a']['b']</literal> + is not defined, it will be created as an empty object and filled as + necessary. However, if any <literal>val</literal> itself or one of the + intermediary values is defined as a non-object such as a string, number, or + <literal>jsonb</literal> <literal>null</literal>, traversal cannot proceed so + an error is raised and the transaction aborted. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +647,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. @@ -639,8 +659,9 @@ SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"'; <type>jsonb</type> assignment via subscripting handles a few edge cases differently from <literal>jsonb_set</literal>. When a source <type>jsonb</type> - is <literal>NULL</literal>, assignment via subscripting will proceed as if - it was an empty JSON object: + value is <literal>NULL</literal>, assignment via subscripting will proceed + as if it was an empty JSON value of the type (object or array) implied by the + subscript key: <programlisting> -- Where jsonb_field was NULL, it is now {"a": 1} @@ -661,17 +682,19 @@ UPDATE table_name SET jsonb_field[2] = '2'; </programlisting> A <type>jsonb</type> value will accept assignments to nonexistent subscript - paths as long as the last existing path key is an object or an array. Since - the final subscript is not traversed, it may be an object key. Nested arrays - will be created and <literal>NULL</literal>-padded according to the path until - the value can be placed appropriately. + paths as long as the last existing element to be traversed is an object or + array, as implied by the corresponding subscript (the element indicated by + the last subscript in the path is not traversed and may be anything). Nested + array and object structures will be created, and in the former case + <literal>null</literal>-padded, as specified by the subscript path until the + assigned value can be placed. <programlisting> -- Where jsonb_field was {}, it is now {'a': [{'b': 1}]} UPDATE table_name SET jsonb_field['a'][0]['b'] = '1'; --- Where jsonb_field was [], it is now [{'a': 1}] -UPDATE table_name SET jsonb_field[0]['a'] = '1'; +-- Where jsonb_field was [], it is now [null, {'a': 1}] +UPDATE table_name SET jsonb_field[1]['a'] = '1'; </programlisting> </para> diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --3x42k255okwqcjpg-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v48 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 27 +++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 27 +++++++++++++++++++++++++++ src/test/regress/sql/jsonb.sql | 17 +++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..9138b7950e 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4926,6 +4926,20 @@ setPath(JsonbIterator **it, Datum *path_elems, switch (r) { case WJB_BEGIN_ARRAY: + /* + * If instructed complain about attempts to replace whithin a raw + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not raw scalar. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1) && + v.val.array.rawScalar) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + (void) pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); @@ -4943,6 +4957,19 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. This happens even when current level is equal to + * path_len, because the last path key should also correspond to an + * object or an array, not an element or value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level <= path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..cf0ce0e44c 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,33 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's an element or a value +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +-- trying replace assuming a composite object, but it's a raw scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json[0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..1a9d21741f 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,23 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's an element or a value + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b'] = '1'; +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + +-- trying replace assuming a composite object, but it's a raw scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, 'null'); +update test_jsonb_subscript set test_json[0] = '1'; +update test_jsonb_subscript set test_json[0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --m4t3wpskod5t4yye-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* [PATCH v46 3/3] Replace assuming a composite object on a scalar @ 2021-01-20 15:53 Dmitrii Dolgov <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Dmitrii Dolgov @ 2021-01-20 15:53 UTC (permalink / raw) For jsonb subscripting assignment it could happen that the provided path assumes an object or an array at some level, but the source jsonb has a scalar value there. Originally the update operation will be skipped and no message will be shown that nothing happened. Return an error to indicate such situations. --- doc/src/sgml/json.sgml | 19 +++++++++++++++++++ src/backend/utils/adt/jsonfuncs.c | 11 +++++++++++ src/test/regress/expected/jsonb.out | 12 ++++++++++++ src/test/regress/sql/jsonb.sql | 8 ++++++++ 4 files changed, 50 insertions(+) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 07bd19f974..deeb9e66e0 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -614,8 +614,23 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu The result of a subscripting expression is always of the jsonb data type. </para> + <para> + <command>UPDATE</command> statements may use subscripting in the + <literal>SET</literal> clause to modify <type>jsonb</type> values. Object + values being traversed must exist as specified by the subscript path. For + instance, the path <literal>val['a']['b']['c']</literal> assumes that + <literal>val</literal>, <literal>val['a']</literal>, and <literal>val['a']['b']</literal> + are all objects in every record being updated (<literal>val['a']['b']</literal> + may or may not contain a field named <literal>c</literal>, as long as it's an + object). If any individual <literal>val</literal>, <literal>val['a']</literal>, + or <literal>val['a']['b']</literal> is a non-object such as a string, a number, + or <literal>NULL</literal>, an error is raised even if other values do conform. + Array values are not subject to this restriction, as detailed below. + </para> + <para> An example of subscripting syntax: + <programlisting> -- Extract object value by key @@ -631,6 +646,10 @@ SELECT ('[1, "2", null]'::jsonb)[1]; -- value must be of the jsonb type as well UPDATE table_name SET jsonb_field['key'] = '1'; +-- This will raise an error if any record's jsonb_field['a']['b'] is something +-- other than an object. For example, the value {"a": 1} has no 'b' key. +UPDATE table_name SET jsonb_field['a']['b']['c'] = '1'; + -- Filter records using a WHERE clause with subscripting. Since the result of -- subscripting is jsonb, the value we compare it against must also be jsonb. -- The double quotes make "value" also a valid jsonb string. diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index f14f6c3191..ebc0b06f5b 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -4943,6 +4943,17 @@ setPath(JsonbIterator **it, Datum *path_elems, break; case WJB_ELEM: case WJB_VALUE: + /* + * If instructed complain about attempts to replace whithin a + * scalar value. + */ + if ((op_type & JB_PATH_FILL_GAPS) && (level < path_len - 1)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot replace existing key"), + errdetail("The path assumes key is a composite object, " + "but it is a scalar value."))); + res = pushJsonbValue(st, r, &v); break; default: diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5b5510c4fd..1d33457788 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5134,6 +5134,18 @@ select * from test_jsonb_subscript; 1 | {"a": [null, {"c": [null, null, 1]}]} (1 row) +-- trying replace assuming a composite object, but it's a scalar +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. +update test_jsonb_subscript set test_json['a'][0][0] = '1'; +ERROR: cannot replace existing key +DETAIL: The path assumes key is a composite object, but it is a scalar value. -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); to_tsvector diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 0320db0ea4..c62a2f9aec 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1371,6 +1371,14 @@ insert into test_jsonb_subscript values (1, '{"a": []}'); update test_jsonb_subscript set test_json['a'][1]['c'][2] = '1'; select * from test_jsonb_subscript; +-- trying replace assuming a composite object, but it's a scalar + +delete from test_jsonb_subscript; +insert into test_jsonb_subscript values (1, '{"a": 1}'); +update test_jsonb_subscript set test_json['a']['b']['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0]['c'] = '1'; +update test_jsonb_subscript set test_json['a'][0][0] = '1'; + -- jsonb to tsvector select to_tsvector('{"a": "aaa bbb ddd ccc", "b": ["eee fff ggg"], "c": {"d": "hhh iii"}}'::jsonb); -- 2.21.0 --mygb4tp4jvuwryfl-- ^ permalink raw reply [nested|flat] 107+ messages in thread
* Re: WIP Incremental JSON Parser @ 2024-02-21 20:26 Jacob Champion <[email protected]> 2024-02-22 09:38 ` Re: WIP Incremental JSON Parser Andrew Dunstan <[email protected]> 0 siblings, 1 reply; 107+ messages in thread From: Jacob Champion @ 2024-02-21 20:26 UTC (permalink / raw) To: Andrew Dunstan <[email protected]>; +Cc: Robert Haas <[email protected]>; Peter Smith <[email protected]>; PostgreSQL Hackers <[email protected]> On Wed, Feb 21, 2024 at 6:50 AM Jacob Champion <[email protected]> wrote: > On Tue, Feb 20, 2024 at 9:32 PM Andrew Dunstan <[email protected]> wrote: > > *sigh* That's weird. I wonder why you can reproduce it and I can't. Can > > you give me details of the build? OS, compiler, path to source, build > > setup etc.? Anything that might be remotely relevant. This construction seems suspect, in json_lex_number(): > if (lex->incremental && !lex->inc_state->is_last_chunk && > len >= lex->input_length) > { > appendStringInfoString(&lex->inc_state->partial_token, > lex->token_start); > return JSON_INCOMPLETE; > } appendStringInfoString() isn't respecting the end of the chunk: if there's extra data after the chunk boundary (as AppendIncrementalManifestData() does) then all of that will be stuck onto the end of the partial_token. I'm about to context-switch off of this for the day, but I can work on a patch tomorrow if that'd be helpful. It looks like this is not the only call to appendStringInfoString(). --Jacob ^ permalink raw reply [nested|flat] 107+ messages in thread
* Re: WIP Incremental JSON Parser 2024-02-21 20:26 Re: WIP Incremental JSON Parser Jacob Champion <[email protected]> @ 2024-02-22 09:38 ` Andrew Dunstan <[email protected]> 0 siblings, 0 replies; 107+ messages in thread From: Andrew Dunstan @ 2024-02-22 09:38 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Robert Haas <[email protected]>; Peter Smith <[email protected]>; PostgreSQL Hackers <[email protected]> On 2024-02-21 We 15:26, Jacob Champion wrote: > On Wed, Feb 21, 2024 at 6:50 AM Jacob Champion > <[email protected]> wrote: >> On Tue, Feb 20, 2024 at 9:32 PM Andrew Dunstan <[email protected]> wrote: >>> *sigh* That's weird. I wonder why you can reproduce it and I can't. Can >>> you give me details of the build? OS, compiler, path to source, build >>> setup etc.? Anything that might be remotely relevant. > This construction seems suspect, in json_lex_number(): > >> if (lex->incremental && !lex->inc_state->is_last_chunk && >> len >= lex->input_length) >> { >> appendStringInfoString(&lex->inc_state->partial_token, >> lex->token_start); >> return JSON_INCOMPLETE; >> } > appendStringInfoString() isn't respecting the end of the chunk: if > there's extra data after the chunk boundary (as > AppendIncrementalManifestData() does) then all of that will be stuck > onto the end of the partial_token. > > I'm about to context-switch off of this for the day, but I can work on > a patch tomorrow if that'd be helpful. It looks like this is not the > only call to appendStringInfoString(). > Yeah, the issue seems to be with chunks of json that are not null-terminated. We don't require that they be so this code was buggy. It wasn't picked up earlier because the tests that I wrote did put a null byte at the end. Patch 5 in this series fixes those issues and adjusts most of the tests to add some trailing junk to the pieces of json, so we can be sure that this is done right. cheers andrew -- Andrew Dunstan EDB: https://www.enterprisedb.com Attachments: [text/x-patch] v8-0001-Introduce-a-non-recursive-JSON-parser.patch (421.1K, ../../[email protected]/2-v8-0001-Introduce-a-non-recursive-JSON-parser.patch) download | inline diff: From 49b423d52007d299d9606b3a3664244ad5e37a10 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan <[email protected]> Date: Thu, 21 Sep 2023 10:55:03 -0400 Subject: [PATCH v8 1/5] Introduce a non-recursive JSON parser This parser uses an explicit prediction stack, unlike the present recursive descent parser where the parser state is represented on the call stack. This difference makes the new parser suitable for usi in incremental parsing of huge JSON documents that cannot be conveniently handles piece-wise by the recursive descent parser. One potential use for this will be in parsing large backup manifests associated with incremental backups. Because this parser is somewhat slower than the recursive descent parser, it is not replacing that parser, but is an additional parser available to callers. For testing purposes, if the build is done with -DFORCE_JSON_PSTACK, all JSON parsing is done with the non-recursive parser, in which case only trivial regression differences in error messages should be observed. --- src/common/jsonapi.c | 832 +++++++++++++++++- src/include/common/jsonapi.h | 23 + src/include/pg_config_manual.h | 7 + src/test/modules/Makefile | 1 + src/test/modules/test_json_parser/Makefile | 36 + src/test/modules/test_json_parser/README | 26 + .../t/001_test_json_parser_incremental.pl | 24 + .../test_json_parser_incremental.c | 77 ++ .../test_json_parser/test_json_parser_perf.c | 86 ++ src/test/modules/test_json_parser/tiny.json | 378 ++++++++ src/tools/pgindent/typedefs.list | 5 + 11 files changed, 1486 insertions(+), 9 deletions(-) create mode 100644 src/test/modules/test_json_parser/Makefile create mode 100644 src/test/modules/test_json_parser/README create mode 100644 src/test/modules/test_json_parser/t/001_test_json_parser_incremental.pl create mode 100644 src/test/modules/test_json_parser/test_json_parser_incremental.c create mode 100644 src/test/modules/test_json_parser/test_json_parser_perf.c create mode 100644 src/test/modules/test_json_parser/tiny.json diff --git a/src/common/jsonapi.c b/src/common/jsonapi.c index 32931ded82..25fca8851d 100644 --- a/src/common/jsonapi.c +++ b/src/common/jsonapi.c @@ -43,6 +43,169 @@ typedef enum /* contexts of JSON parser */ JSON_PARSE_END, /* saw the end of a document, expect nothing */ } JsonParseContext; +/* + * Setup for table-driven parser. + * These enums need to be separate from the JsonTokenType and from each other + * so we can have all of them on the prediction stack, which consists of + * tokens, non-terminals, and semantic action markers. + */ + +typedef enum +{ + JSON_NT_JSON = 32, + JSON_NT_ARRAY_ELEMENTS, + JSON_NT_MORE_ARRAY_ELEMENTS, + JSON_NT_KEY_PAIRS, + JSON_NT_MORE_KEY_PAIRS, +} JsonNonTerminal; + +typedef enum +{ + JSON_SEM_OSTART = 64, + JSON_SEM_OEND, + JSON_SEM_ASTART, + JSON_SEM_AEND, + JSON_SEM_OFIELD_INIT, + JSON_SEM_OFIELD_START, + JSON_SEM_OFIELD_END, + JSON_SEM_AELEM_START, + JSON_SEM_AELEM_END, + JSON_SEM_SCALAR_INIT, + JSON_SEM_SCALAR_CALL, +} JsonParserSem; + +/* + * struct containing the 3 stacks used in non-recursive parsing, + * and the token and value for scalars that need to be preserved + * across calls. + */ +typedef struct JsonParserStack +{ + int stack_size; + char *prediction; + int pred_index; + /* these two are indexed by lex_level */ + char **fnames; + bool *fnull; + JsonTokenType scalar_tok; + char *scalar_val; +} JsonParserStack; + +/* + * struct containing state used when there is a possible partial token at the + * end of a json chunk when we are doing incremental parsing. + */ +typedef struct JsonIncrementalState +{ + bool is_last_chunk; + bool partial_completed; + StringInfoData partial_token; +} JsonIncrementalState; + +/* + * constants and macros used in the nonrecursive parser + */ +#define JSON_NUM_TERMINALS 13 +#define JSON_NUM_NONTERMINALS 6 +#define JSON_NT_OFFSET JSON_NT_JSON +/* for indexing the table */ +#define OFS(NT) (NT) - JSON_NT_OFFSET +/* classify items we get off the stack */ +#define IS_SEM(x) ((x) & 0x40) +#define IS_NT(x) ((x) & 0x20) + +/* + * These productions are stored in reverse order right to left so that when + * they are pushed on the stack what we expect next is at the top of the stack. + */ +static char JSON_PROD_EPSILON[] = {0}; /* epsilon - an empty production */ + +/* JSON -> string */ +static char JSON_PROD_SCALAR_STRING[] = {JSON_SEM_SCALAR_CALL, JSON_TOKEN_STRING, JSON_SEM_SCALAR_INIT, 0}; + +/* JSON -> number */ +static char JSON_PROD_SCALAR_NUMBER[] = {JSON_SEM_SCALAR_CALL, JSON_TOKEN_NUMBER, JSON_SEM_SCALAR_INIT, 0}; + +/* JSON -> 'true' */ +static char JSON_PROD_SCALAR_TRUE[] = {JSON_SEM_SCALAR_CALL, JSON_TOKEN_TRUE, JSON_SEM_SCALAR_INIT, 0}; + +/* JSON -> 'false' */ +static char JSON_PROD_SCALAR_FALSE[] = {JSON_SEM_SCALAR_CALL, JSON_TOKEN_FALSE, JSON_SEM_SCALAR_INIT, 0}; + +/* JSON -> 'null' */ +static char JSON_PROD_SCALAR_NULL[] = {JSON_SEM_SCALAR_CALL, JSON_TOKEN_NULL, JSON_SEM_SCALAR_INIT, 0}; + +/* JSON -> '{' KEY_PAIRS '}' */ +static char JSON_PROD_OBJECT[] = {JSON_SEM_OEND, JSON_TOKEN_OBJECT_END, JSON_NT_KEY_PAIRS, JSON_TOKEN_OBJECT_START, JSON_SEM_OSTART, 0}; + +/* JSON -> '[' ARRAY_ELEMENTS ']' */ +static char JSON_PROD_ARRAY[] = {JSON_SEM_AEND, JSON_TOKEN_ARRAY_END, JSON_NT_ARRAY_ELEMENTS, JSON_TOKEN_ARRAY_START, JSON_SEM_ASTART, 0}; + +/* ARRAY_ELEMENTS -> JSON MORE_ARRAY_ELEMENTS */ +static char JSON_PROD_ARRAY_ELEMENTS[] = {JSON_NT_MORE_ARRAY_ELEMENTS, JSON_SEM_AELEM_END, JSON_NT_JSON, JSON_SEM_AELEM_START, 0}; + +/* MORE_ARRAY_ELEMENTS -> ',' JSON MORE_ARRAY_ELEMENTS */ +static char JSON_PROD_MORE_ARRAY_ELEMENTS[] = {JSON_NT_MORE_ARRAY_ELEMENTS, JSON_SEM_AELEM_END, JSON_NT_JSON, JSON_SEM_AELEM_START, JSON_TOKEN_COMMA, 0}; + +/* KEY_PAIRS -> string ':' JSON MORE_KEY_PAIRS */ +static char JSON_PROD_KEY_PAIRS[] = {JSON_NT_MORE_KEY_PAIRS, JSON_SEM_OFIELD_END, JSON_NT_JSON, JSON_SEM_OFIELD_START, JSON_TOKEN_COLON, JSON_TOKEN_STRING, JSON_SEM_OFIELD_INIT, 0}; + +/* MORE_KEY_PAIRS -> ',' string ':' JSON MORE_KEY_PAIRS */ +static char JSON_PROD_MORE_KEY_PAIRS[] = {JSON_NT_MORE_KEY_PAIRS, JSON_SEM_OFIELD_END, JSON_NT_JSON, JSON_SEM_OFIELD_START, JSON_TOKEN_COLON, JSON_TOKEN_STRING, JSON_SEM_OFIELD_INIT, JSON_TOKEN_COMMA, 0}; + +/* + * Note: there are also epsilon productions for ARRAY_ELEMENTS, + * MORE_ARRAY_ELEMENTS, KEY_PAIRS and MORE_KEY_PAIRS + * They are all the same as none require any semantic actions. + */ + +/* + * Table connecting the productions with their director sets of + * terminal symbols. + * Any combination not specified here represents an error. + */ + +typedef struct +{ + size_t len; + char *prod; +} td_entry; + +#define TD_ENTRY(PROD) { sizeof(PROD) - 1, (PROD) } + +static td_entry td_parser_table[JSON_NUM_NONTERMINALS][JSON_NUM_TERMINALS] = +{ + /* JSON */ + [OFS(JSON_NT_JSON)][JSON_TOKEN_STRING] = TD_ENTRY(JSON_PROD_SCALAR_STRING), + [OFS(JSON_NT_JSON)][JSON_TOKEN_NUMBER] = TD_ENTRY(JSON_PROD_SCALAR_NUMBER), + [OFS(JSON_NT_JSON)][JSON_TOKEN_TRUE] = TD_ENTRY(JSON_PROD_SCALAR_TRUE), + [OFS(JSON_NT_JSON)][JSON_TOKEN_FALSE] = TD_ENTRY(JSON_PROD_SCALAR_FALSE), + [OFS(JSON_NT_JSON)][JSON_TOKEN_NULL] = TD_ENTRY(JSON_PROD_SCALAR_NULL), + [OFS(JSON_NT_JSON)][JSON_TOKEN_ARRAY_START] = TD_ENTRY(JSON_PROD_ARRAY), + [OFS(JSON_NT_JSON)][JSON_TOKEN_OBJECT_START] = TD_ENTRY(JSON_PROD_OBJECT), + /* ARRAY_ELEMENTS */ + [OFS(JSON_NT_ARRAY_ELEMENTS)][JSON_TOKEN_ARRAY_START] = TD_ENTRY(JSON_PROD_ARRAY_ELEMENTS), + [OFS(JSON_NT_ARRAY_ELEMENTS)][JSON_TOKEN_OBJECT_START] = TD_ENTRY(JSON_PROD_ARRAY_ELEMENTS), + [OFS(JSON_NT_ARRAY_ELEMENTS)][JSON_TOKEN_STRING] = TD_ENTRY(JSON_PROD_ARRAY_ELEMENTS), + [OFS(JSON_NT_ARRAY_ELEMENTS)][JSON_TOKEN_NUMBER] = TD_ENTRY(JSON_PROD_ARRAY_ELEMENTS), + [OFS(JSON_NT_ARRAY_ELEMENTS)][JSON_TOKEN_TRUE] = TD_ENTRY(JSON_PROD_ARRAY_ELEMENTS), + [OFS(JSON_NT_ARRAY_ELEMENTS)][JSON_TOKEN_FALSE] = TD_ENTRY(JSON_PROD_ARRAY_ELEMENTS), + [OFS(JSON_NT_ARRAY_ELEMENTS)][JSON_TOKEN_NULL] = TD_ENTRY(JSON_PROD_ARRAY_ELEMENTS), + [OFS(JSON_NT_ARRAY_ELEMENTS)][JSON_TOKEN_ARRAY_END] = TD_ENTRY(JSON_PROD_EPSILON), + /* MORE_ARRAY_ELEMENTS */ + [OFS(JSON_NT_MORE_ARRAY_ELEMENTS)][JSON_TOKEN_COMMA] = TD_ENTRY(JSON_PROD_MORE_ARRAY_ELEMENTS), + [OFS(JSON_NT_MORE_ARRAY_ELEMENTS)][JSON_TOKEN_ARRAY_END] = TD_ENTRY(JSON_PROD_EPSILON), + /* KEY_PAIRS */ + [OFS(JSON_NT_KEY_PAIRS)][JSON_TOKEN_STRING] = TD_ENTRY(JSON_PROD_KEY_PAIRS), + [OFS(JSON_NT_KEY_PAIRS)][JSON_TOKEN_OBJECT_END] = TD_ENTRY(JSON_PROD_EPSILON), + /* MORE_KEY_PAIRS */ + [OFS(JSON_NT_MORE_KEY_PAIRS)][JSON_TOKEN_COMMA] = TD_ENTRY(JSON_PROD_MORE_KEY_PAIRS), + [OFS(JSON_NT_MORE_KEY_PAIRS)][JSON_TOKEN_OBJECT_END] = TD_ENTRY(JSON_PROD_EPSILON), +}; + +/* the GOAL production. Not stored in the table, but will be the initial contents of the prediction stack */ +static char JSON_PROD_GOAL[] = {JSON_TOKEN_END, JSON_NT_JSON, 0}; + static inline JsonParseErrorType json_lex_string(JsonLexContext *lex); static inline JsonParseErrorType json_lex_number(JsonLexContext *lex, char *s, bool *num_err, int *total_len); @@ -60,7 +223,7 @@ JsonSemAction nullSemAction = NULL, NULL, NULL, NULL, NULL }; -/* Recursive Descent parser support routines */ +/* Parser support routines */ /* * lex_peek @@ -111,6 +274,8 @@ IsValidJsonNumber(const char *str, int len) if (len <= 0) return false; + dummy_lex.incremental = false; + /* * json_lex_number expects a leading '-' to have been eaten already. * @@ -174,6 +339,139 @@ makeJsonLexContextCstringLen(JsonLexContext *lex, char *json, return lex; } + +/* + * makeJsonLexContextIncremental + * + * Similar to above but set up for use in incremental parsing. That means we + * need explicit stacks for predictions, field names and null indicators, but + * we don't need the input, that will be handed in bit by bit to the + * parse routine. We also need an accumulator for partial tokens in case + * the boundary between chunks happns to fall in the middle of a token. + */ +#define JS_STACK_CHUNK_SIZE 64 +#define JS_MAX_PROD_LEN 10 /* more than we need */ +#define JSON_TD_MAX_STACK 6400 /* hard coded for now - this is a REALLY high + * number */ + +JsonLexContext * +makeJsonLexContextIncremental(JsonLexContext *lex, int encoding, + bool need_escapes) +{ + if (lex == NULL) + { + lex = palloc0(sizeof(JsonLexContext)); + lex->flags |= JSONLEX_FREE_STRUCT; + } + else + memset(lex, 0, sizeof(JsonLexContext)); + + lex->line_number = 1; + lex->input_encoding = encoding; + lex->incremental = true; + lex->inc_state = palloc0(sizeof(JsonIncrementalState)); + initStringInfo(&(lex->inc_state->partial_token)); + lex->pstack = palloc(sizeof(JsonParserStack)); + lex->pstack->stack_size = JS_STACK_CHUNK_SIZE; + lex->pstack->prediction = palloc(JS_STACK_CHUNK_SIZE * JS_MAX_PROD_LEN); + lex->pstack->pred_index = 0; + lex->pstack->fnames = palloc(JS_STACK_CHUNK_SIZE * sizeof(char *)); + lex->pstack->fnull = palloc(JS_STACK_CHUNK_SIZE * sizeof(bool)); + if (need_escapes) + { + lex->strval = makeStringInfo(); + lex->flags |= JSONLEX_FREE_STRVAL; + } + return lex; +} + +static inline void +inc_lex_level(JsonLexContext *lex) +{ + lex->lex_level += 1; + /* could possibly use something like max_stack_depth * 64 here */ + if (lex->lex_level > JSON_TD_MAX_STACK) + { +#ifndef FRONTEND + elog(ERROR, "maximum number of levels for json is %d", JSON_TD_MAX_STACK); +#else + fprintf(stderr, "maximum number of levels for json is %d", JSON_TD_MAX_STACK); + exit(1); +#endif + } + if (lex->incremental && lex->lex_level >= lex->pstack->stack_size) + { + lex->pstack->stack_size += JS_STACK_CHUNK_SIZE; + lex->pstack->prediction = + repalloc(lex->pstack->prediction, + lex->pstack->stack_size * JS_MAX_PROD_LEN); + if (lex->pstack->fnames) + lex->pstack->fnames = + repalloc(lex->pstack->fnames, + lex->pstack->stack_size * sizeof(char *)); + if (lex->pstack->fnull) + lex->pstack->fnull = + repalloc(lex->pstack->fnull, lex->pstack->stack_size * sizeof(bool)); + } +} + +static inline void +dec_lex_level(JsonLexContext *lex) +{ + lex->lex_level -= 1; +} + +static inline void +push_prediction(JsonParserStack *pstack, td_entry entry) +{ + memcpy(pstack->prediction + pstack->pred_index, entry.prod, entry.len); + pstack->pred_index += entry.len; +} + +static inline char +pop_prediction(JsonParserStack *pstack) +{ + Assert(pstack->pred_index > 0); + return pstack->prediction[--pstack->pred_index]; +} + +static inline char +next_prediction(JsonParserStack *pstack) +{ + Assert(pstack->pred_index > 0); + return pstack->prediction[pstack->pred_index - 1]; +} + +static inline bool +have_prediction(JsonParserStack *pstack) +{ + return pstack->pred_index > 0; +} + +static inline void +set_fname(JsonLexContext *lex, char *fname) +{ + lex->pstack->fnames[lex->lex_level] = fname; +} + +static inline char * +get_fname(JsonLexContext *lex) +{ + return lex->pstack->fnames[lex->lex_level]; +} + +static inline void +set_fnull(JsonLexContext *lex, bool fnull) +{ + lex->pstack->fnull[lex->lex_level] = fnull; +} + +static inline bool +get_fnull(JsonLexContext *lex) +{ + return lex->pstack->fnull[lex->lex_level]; +} + /* * Free memory in a JsonLexContext. There's no need for this if a *lex * pointer was given when the object was made and need_escapes was false, @@ -188,7 +486,18 @@ freeJsonLexContext(JsonLexContext *lex) pfree(lex->strval); } if (lex->flags & JSONLEX_FREE_STRUCT) + { + if (lex->incremental) + { + pfree(lex->inc_state->partial_token.data); + pfree(lex->inc_state); + pfree(lex->pstack->prediction); + pfree(lex->pstack->fnames); + pfree(lex->pstack->fnull); + pfree(lex->pstack); + } pfree(lex); + } } /* @@ -200,10 +509,32 @@ freeJsonLexContext(JsonLexContext *lex) * makeJsonLexContext(). sem is a structure of function pointers to semantic * action routines to be called at appropriate spots during parsing, and a * pointer to a state object to be passed to those routines. + * + * If FORCE_JSON_PSTACK is defined then the routine will call the non-recursive + * JSON parser. This is a useful way to validate that it's doing the right + * think at least for non-incremental cases. If this is on we expect to see + * regression diffs relating to error messages about stack depth, but no + * other differences. */ JsonParseErrorType pg_parse_json(JsonLexContext *lex, JsonSemAction *sem) { +#ifdef FORCE_JSON_PSTACK + + lex->incremental = true; + lex->inc_state = palloc0(sizeof(JsonIncrementalState)); + initStringInfo(&(lex->inc_state->partial_token)); + lex->pstack = palloc(sizeof(JsonParserStack)); + lex->pstack->stack_size = JS_STACK_CHUNK_SIZE; + lex->pstack->prediction = palloc(JS_STACK_CHUNK_SIZE * JS_MAX_PROD_LEN); + lex->pstack->pred_index = 0; + lex->pstack->fnames = palloc(JS_STACK_CHUNK_SIZE * sizeof(char *)); + lex->pstack->fnull = palloc(JS_STACK_CHUNK_SIZE * sizeof(bool)); + + return pg_parse_json_incremental(lex, sem, lex->input, lex->input_length, true); + +#else + JsonTokenType tok; JsonParseErrorType result; @@ -231,6 +562,7 @@ pg_parse_json(JsonLexContext *lex, JsonSemAction *sem) result = lex_expect(JSON_PARSE_END, lex, JSON_TOKEN_END); return result; +#endif } /* @@ -286,6 +618,333 @@ json_count_array_elements(JsonLexContext *lex, int *elements) return JSON_SUCCESS; } +/* + * pg_parse_json_incremental + * + * Routine for incremental parsing of json. This uses the non-recursive top + * down method of the Dragon Book Algorithm 4.3. It's somewhat slower than + * the Recursive Descent pattern used above, so we only use it for incremental + * parsing of JSON. + * + * The lexing context needs to be set up by a call to + * makeJsonLexContextIncremental(). sem is a structure of function pointers + * to semantic action routines, which should function exactly as those used + * in the recursive descent parser. + * + * This routine can be called repeatedly with chunks of JSON. On the final + * chunk is_last must be set to true. len is the length of the json chunk, + * which does not need to be null terminated. + */ +JsonParseErrorType +pg_parse_json_incremental(JsonLexContext *lex, + JsonSemAction *sem, + char *json, + int len, + bool is_last) +{ + JsonTokenType tok; + JsonParseErrorType result; + JsonParseContext ctx = JSON_PARSE_VALUE; + JsonParserStack *pstack = lex->pstack; + + + if (lex->incremental) + { + lex->input = lex->token_terminator = lex->line_start = json; + lex->input_length = len; + lex->inc_state->is_last_chunk = is_last; + } + else + { +#ifdef FRONTEND + fprintf(stderr, "JsonLexContext not set up for incremental parsing"); + exit(1); +#else + elog(ERROR, "JsonLexContext not set up for incremental parsing"); +#endif + } + + /* get the initial token */ + result = json_lex(lex); + if (result != JSON_SUCCESS) + return result; + + tok = lex_peek(lex); + + /* use prediction stack for incremental parsing */ + + if (!have_prediction(pstack)) + { + td_entry goal = TD_ENTRY(JSON_PROD_GOAL); + + push_prediction(pstack, goal); + } + + while (have_prediction(pstack)) + { + char top = pop_prediction(pstack); + td_entry entry; + + /* + * these first two branches are the guts of the Table Driven method + */ + if (top == tok) + { + /* + * tok can only be a terminal symbol, so top must be too. the + * token matches the top of the stack, so get the next token. + */ + if (tok < JSON_TOKEN_END) + { + result = json_lex(lex); + if (result != JSON_SUCCESS) + return result; + tok = lex_peek(lex); + } + } + else if (IS_NT(top) && (entry = td_parser_table[OFS(top)][tok]).prod != NULL) + { + /* + * the token is in the director set for a production of the + * non-terminal at the top of the stack, so push the reversed RHS + * of the production onto the stack. + */ + push_prediction(pstack, entry); + } + else if (IS_SEM(top)) + { + /* + * top is a semantic action marker, so take action accordingly. + * It's important to have these markers in the prediction stack + * before any token they might need so we don't advance the token + * prematurely. Note in a couple of cases we need to do something + * both before and after the token. + */ + switch (top) + { + case JSON_SEM_OSTART: + { + json_struct_action ostart = sem->object_start; + + if (ostart != NULL) + (*ostart) (sem->semstate); + inc_lex_level(lex); + } + break; + case JSON_SEM_OEND: + { + json_struct_action oend = sem->object_end; + + dec_lex_level(lex); + if (oend != NULL) + (*oend) (sem->semstate); + } + break; + case JSON_SEM_ASTART: + { + json_struct_action astart = sem->array_start; + + if (astart != NULL) + (*astart) (sem->semstate); + inc_lex_level(lex); + } + break; + case JSON_SEM_AEND: + { + json_struct_action aend = sem->array_end; + + dec_lex_level(lex); + if (aend != NULL) + (*aend) (sem->semstate); + } + break; + case JSON_SEM_OFIELD_INIT: + { + /* + * all we do here is save out the field name. We have + * to wait to get past the ':' to see if the next + * value is null so we can call the semantic routine + */ + char *fname = NULL; + json_ofield_action ostart = sem->object_field_start; + json_ofield_action oend = sem->object_field_end; + + if ((ostart != NULL || oend != NULL) && lex->strval != NULL) + { + fname = pstrdup(lex->strval->data); + } + set_fname(lex, fname); + } + break; + case JSON_SEM_OFIELD_START: + { + /* + * the current token should be the first token of the + * value + */ + bool isnull = tok == JSON_TOKEN_NULL; + json_ofield_action ostart = sem->object_field_start; + + set_fnull(lex, isnull); + + if (ostart != NULL) + { + char *fname = get_fname(lex); + + (*ostart) (sem->semstate, fname, isnull); + } + } + break; + case JSON_SEM_OFIELD_END: + { + json_ofield_action oend = sem->object_field_end; + + if (oend != NULL) + { + char *fname = get_fname(lex); + bool isnull = get_fnull(lex); + + (*oend) (sem->semstate, fname, isnull); + } + } + break; + case JSON_SEM_AELEM_START: + { + json_aelem_action astart = sem->array_element_start; + bool isnull = tok == JSON_TOKEN_NULL; + + set_fnull(lex, isnull); + + if (astart != NULL) + (*astart) (sem->semstate, isnull); + } + break; + case JSON_SEM_AELEM_END: + { + json_aelem_action aend = sem->array_element_end; + + if (aend != NULL) + { + bool isnull = get_fnull(lex); + + (*aend) (sem->semstate, isnull); + } + } + break; + case JSON_SEM_SCALAR_INIT: + { + json_scalar_action sfunc = sem->scalar; + + pstack->scalar_val = NULL; + + if (sfunc != NULL) + { + /* + * extract the de-escaped string value, or the raw + * lexeme + */ + /* + * XXX copied from RD parser but looks like a + * buglet + */ + if (tok == JSON_TOKEN_STRING) + { + if (lex->strval != NULL) + pstack->scalar_val = pstrdup(lex->strval->data); + } + else + { + int tlen = (lex->token_terminator - lex->token_start); + + pstack->scalar_val = palloc(tlen + 1); + memcpy(pstack->scalar_val, lex->token_start, tlen); + pstack->scalar_val[tlen] = '\0'; + } + pstack->scalar_tok = tok; + } + } + break; + case JSON_SEM_SCALAR_CALL: + { + /* + * We'd like to be able to get rid of this business of + * two bits of scalar action, but we can't. It breaks + * certain semantic actions which expect that when + * called the lexer has consumed the item. See for + * example get_scalar() in jsonfuncs.c. + */ + json_scalar_action sfunc = sem->scalar; + + if (sfunc != NULL) + (*sfunc) (sem->semstate, pstack->scalar_val, pstack->scalar_tok); + } + break; + default: + /* should not happen */ + break; + } + } + else + { + /* + * The token didn't match the stack top if it's a terminal nor a + * production for the stack top if it's a non-terminal. + */ + switch (top) + { + case JSON_TOKEN_STRING: + if (next_prediction(pstack) == JSON_TOKEN_COLON) + ctx = JSON_PARSE_STRING; + else + ctx = JSON_PARSE_VALUE; + break; + case JSON_TOKEN_NUMBER: + case JSON_TOKEN_TRUE: + case JSON_TOKEN_FALSE: + case JSON_TOKEN_NULL: + case JSON_TOKEN_ARRAY_START: + case JSON_TOKEN_OBJECT_START: + ctx = JSON_PARSE_VALUE; + break; + case JSON_TOKEN_ARRAY_END: + ctx = JSON_PARSE_ARRAY_NEXT; + break; + case JSON_TOKEN_OBJECT_END: + ctx = JSON_PARSE_OBJECT_NEXT; + break; + case JSON_TOKEN_COMMA: + if (next_prediction(pstack) == JSON_TOKEN_STRING) + ctx = JSON_PARSE_OBJECT_NEXT; + else + ctx = JSON_PARSE_ARRAY_NEXT; + break; + case JSON_TOKEN_COLON: + ctx = JSON_PARSE_OBJECT_LABEL; + break; + case JSON_TOKEN_END: + ctx = JSON_PARSE_END; + break; + + /* + * mostly we don't need to worry about non-terminals here, + * but there are a few cases where we do. + */ + case JSON_NT_MORE_KEY_PAIRS: + ctx = JSON_PARSE_OBJECT_NEXT; + break; + case JSON_NT_KEY_PAIRS: + ctx = JSON_PARSE_OBJECT_START; + break; + default: + ctx = JSON_PARSE_VALUE; + } + return report_parse_error(ctx, lex); + } + } + + return JSON_SUCCESS; +} + /* * Recursive Descent parse routines. There is one for each structural * element in a json document: @@ -591,8 +1250,138 @@ json_lex(JsonLexContext *lex) char *const end = lex->input + lex->input_length; JsonParseErrorType result; - /* Skip leading whitespace. */ + if (lex->incremental && lex->inc_state->partial_completed) + { + /* + * We just lexed a completed partial token on the last call, so reset + * everything + */ + resetStringInfo(&(lex->inc_state->partial_token)); + lex->token_terminator = lex->input; + lex->inc_state->partial_completed = false; + } + s = lex->token_terminator; + + if (lex->incremental && lex->inc_state->partial_token.len) + { + /* + * We have a partial token. Extend it and if completed lex it by a + * recursive call + */ + StringInfo ptok = &(lex->inc_state->partial_token); + int added = 0; + bool tok_done = false; + JsonLexContext dummy_lex; + JsonParseErrorType partial_result; + + if (ptok->data[0] == '"') + { + /* + * It's a string. Accumulate characters until we reach an + * unescaped '"'. + */ + int escapes = 0; + + for (int i = ptok->len - 1; i > 0; i--) + { + /* count the trailing backslashes on the partial token */ + if (ptok->data[i] == '\\') + escapes++; + else + break; + } + + for (int i = 0; i < lex->input_length; i++) + { + char c = lex->input[i]; + + appendStringInfoCharMacro(ptok, c); + added++; + if (c == '"' && escapes % 2 == 0) + { + tok_done = true; + break; + } + if (c == '\\') + escapes++; + else + escapes = 0; + } + } + else + { + /* not a string */ + char c = ptok->data[0]; + + if (c == '-' || (c >= '0' && c <= '9')) + { + /* for numbers look for possible numeric continuations */ + size_t nums = strspn(lex->input, "+-.eE0123456789"); + + for (int i = 0; i < nums; i++) + { + char cc = lex->input[i]; + + appendStringInfoCharMacro(ptok, cc); + added++; + } + } + /* add any remaining alpha_numeric chars */ + for (int i = added; i < lex->input_length; i++) + { + char cc = lex->input[i]; + + if (JSON_ALPHANUMERIC_CHAR(cc)) + { + appendStringInfoCharMacro(ptok, cc); + added++; + } + else + { + tok_done = true; + break; + } + } + } + + if (!tok_done) + { + if (lex->inc_state->is_last_chunk) + return JSON_INVALID_TOKEN; + else + return JSON_INCOMPLETE; + } + + lex->input += added; + lex->input_length -= added; + + dummy_lex.input = dummy_lex.token_terminator = + dummy_lex.line_start = ptok->data; + dummy_lex.line_number = lex->line_number; + dummy_lex.input_length = ptok->len; + dummy_lex.input_encoding = lex->input_encoding; + dummy_lex.incremental = false; + dummy_lex.strval = lex->strval; + + partial_result = json_lex(&dummy_lex); + if (partial_result != JSON_SUCCESS) + return partial_result; + lex->token_type = dummy_lex.token_type; + lex->line_number = dummy_lex.line_number; + + /* + * Need to point here so semantic routines can get the correct token. + * We will readjust on the next call to json_lex. + */ + lex->token_start = ptok->data; + lex->token_terminator = ptok->data + ptok->len; + lex->inc_state->partial_completed = true; + return JSON_SUCCESS; + /* end of partial token processing */ + } + + /* Skip leading whitespace. */ while (s < end && (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r')) { if (*s++ == '\n') @@ -704,6 +1493,14 @@ json_lex(JsonLexContext *lex) return JSON_INVALID_TOKEN; } + if (lex->incremental && !lex->inc_state->is_last_chunk && + p == lex->input + lex->input_length) + { + appendStringInfoString( + &(lex->inc_state->partial_token), s); + return JSON_INCOMPLETE; + } + /* * We've got a real alphanumeric token here. If it * happens to be true, false, or null, all is well. If @@ -728,7 +1525,10 @@ json_lex(JsonLexContext *lex) } /* end of switch */ } - return JSON_SUCCESS; + if (lex->incremental && lex->token_type == JSON_TOKEN_END && !lex->inc_state->is_last_chunk) + return JSON_INCOMPLETE; + else + return JSON_SUCCESS; } /* @@ -750,8 +1550,14 @@ json_lex_string(JsonLexContext *lex) int hi_surrogate = -1; /* Convenience macros for error exits */ -#define FAIL_AT_CHAR_START(code) \ +#define FAIL_OR_INCOMPLETE_AT_CHAR_START(code) \ do { \ + if (lex->incremental && !lex->inc_state->is_last_chunk) \ + { \ + appendStringInfoString(&lex->inc_state->partial_token, \ + lex->token_start); \ + return JSON_INCOMPLETE; \ + } \ lex->token_terminator = s; \ return code; \ } while (0) @@ -772,7 +1578,7 @@ json_lex_string(JsonLexContext *lex) s++; /* Premature end of the string. */ if (s >= end) - FAIL_AT_CHAR_START(JSON_INVALID_TOKEN); + FAIL_OR_INCOMPLETE_AT_CHAR_START(JSON_INVALID_TOKEN); else if (*s == '"') break; else if (*s == '\\') @@ -780,7 +1586,7 @@ json_lex_string(JsonLexContext *lex) /* OK, we have an escape character. */ s++; if (s >= end) - FAIL_AT_CHAR_START(JSON_INVALID_TOKEN); + FAIL_OR_INCOMPLETE_AT_CHAR_START(JSON_INVALID_TOKEN); else if (*s == 'u') { int i; @@ -790,7 +1596,7 @@ json_lex_string(JsonLexContext *lex) { s++; if (s >= end) - FAIL_AT_CHAR_START(JSON_INVALID_TOKEN); + FAIL_OR_INCOMPLETE_AT_CHAR_START(JSON_INVALID_TOKEN); else if (*s >= '0' && *s <= '9') ch = (ch * 16) + (*s - '0'); else if (*s >= 'a' && *s <= 'f') @@ -975,7 +1781,7 @@ json_lex_string(JsonLexContext *lex) lex->token_terminator = s + 1; return JSON_SUCCESS; -#undef FAIL_AT_CHAR_START +#undef FAIL_OR_INCOMPLETE_AT_CHAR_START #undef FAIL_AT_CHAR_END } @@ -1084,7 +1890,14 @@ json_lex_number(JsonLexContext *lex, char *s, if (total_len != NULL) *total_len = len; - if (num_err != NULL) + if (lex->incremental && !lex->inc_state->is_last_chunk && + len >= lex->input_length) + { + appendStringInfoString(&lex->inc_state->partial_token, + lex->token_start); + return JSON_INCOMPLETE; + } + else if (num_err != NULL) { /* let the caller handle any error */ *num_err = error; @@ -1173,6 +1986,7 @@ json_errdetail(JsonParseErrorType error, JsonLexContext *lex) { switch (error) { + case JSON_INCOMPLETE: case JSON_SUCCESS: /* fall through to the error code after switch */ break; diff --git a/src/include/common/jsonapi.h b/src/include/common/jsonapi.h index 02943cdad8..66844aefec 100644 --- a/src/include/common/jsonapi.h +++ b/src/include/common/jsonapi.h @@ -36,6 +36,7 @@ typedef enum JsonTokenType typedef enum JsonParseErrorType { JSON_SUCCESS, + JSON_INCOMPLETE, JSON_ESCAPING_INVALID, JSON_ESCAPING_REQUIRED, JSON_EXPECTED_ARRAY_FIRST, @@ -57,6 +58,9 @@ typedef enum JsonParseErrorType JSON_SEM_ACTION_FAILED, /* error should already be reported */ } JsonParseErrorType; +/* Parser state private to jsonapi.c */ +typedef struct JsonParserStack JsonParserStack; +typedef struct JsonIncrementalState JsonIncrementalState; /* * All the fields in this structure should be treated as read-only. @@ -83,11 +87,14 @@ typedef struct JsonLexContext char *token_start; char *token_terminator; char *prev_token_terminator; + bool incremental; JsonTokenType token_type; int lex_level; bits32 flags; int line_number; /* line number, starting from 1 */ char *line_start; /* where that line starts within input */ + JsonParserStack *pstack; + JsonIncrementalState *inc_state; StringInfo strval; } JsonLexContext; @@ -140,6 +147,12 @@ typedef struct JsonSemAction extern JsonParseErrorType pg_parse_json(JsonLexContext *lex, JsonSemAction *sem); +extern JsonParseErrorType pg_parse_json_incremental(JsonLexContext *lex, + JsonSemAction *sem, + char *json, + int len, + bool is_last); + /* the null action object used for pure validation */ extern PGDLLIMPORT JsonSemAction nullSemAction; @@ -175,6 +188,16 @@ extern JsonLexContext *makeJsonLexContextCstringLen(JsonLexContext *lex, int len, int encoding, bool need_escapes); + +/* + * make a JsonLexContext suitable for incremental parsing. + * the string chunks will be handed to pg_parse_json_incremental, + * so there's no need for them here. + */ +extern JsonLexContext *makeJsonLexContextIncremental(JsonLexContext *lex, + int encoding, + bool need_escapes); + extern void freeJsonLexContext(JsonLexContext *lex); /* lex one token */ diff --git a/src/include/pg_config_manual.h b/src/include/pg_config_manual.h index a512552182..f941ee2faf 100644 --- a/src/include/pg_config_manual.h +++ b/src/include/pg_config_manual.h @@ -240,6 +240,13 @@ *------------------------------------------------------------------------ */ +/* + * Force use of the non-recursive JSON parser in all cases. This is useful + * to validate the working of the parser, and the regression tests should + * pass except for some different error messages about the stack limit. + */ +/* #define FORCE_JSON_PSTACK */ + /* * Include Valgrind "client requests", mostly in the memory allocator, so * Valgrind understands PostgreSQL memory contexts. This permits detecting diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index e32c8925f6..d30e5b9a9c 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -22,6 +22,7 @@ SUBDIRS = \ test_extensions \ test_ginpostinglist \ test_integerset \ + test_json_parser \ test_lfind \ test_misc \ test_oat_hooks \ diff --git a/src/test/modules/test_json_parser/Makefile b/src/test/modules/test_json_parser/Makefile new file mode 100644 index 0000000000..04350c68db --- /dev/null +++ b/src/test/modules/test_json_parser/Makefile @@ -0,0 +1,36 @@ + +PGFILEDESC = "standalone json parser tester" +PGAPPICON = win32 + +TAP_TESTS = 1 + +OBJS = test_json_parser_incremental.o test_json_parser_perf.o + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_json_parser +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +all: test_json_parser_incremental$(X) test_json_parser_perf$(X) + +%.o: $(top_srcdir)/$(subdir)/%.c + +PARSER_LIBS = $(top_builddir)/src/common/libpgcommon.a $(top_builddir)/src/port/libpgport.a + +test_json_parser_incremental$(X): test_json_parser_incremental.o $(PARSER_LIBS) + $(CC) $(CFLAGS) $^ -o $@ + +test_json_parser_perf$(X): test_json_parser_perf.o $(PARSER_LIBS) + $(CC) $(CFLAGS) $^ -o $@ + +speed-check: test_json_parser_perf$(X) + @echo Standard parser: + time ./test_json_parser_perf 10000 $(top_srcdir)/$(subdir)/tiny.json + @echo Incremental parser: + time ./test_json_parser_perf -i 10000 $(top_srcdir)/$(subdir)/tiny.json diff --git a/src/test/modules/test_json_parser/README b/src/test/modules/test_json_parser/README new file mode 100644 index 0000000000..8241209380 --- /dev/null +++ b/src/test/modules/test_json_parser/README @@ -0,0 +1,26 @@ +Module `test_json_parser` +========================= + +This module contains two programs for testing the json parsers. + +- `test_json_parser_incremental` is for testing the incremental parser, It + reads in a file and pases it in very small chunks (60 bytes at a time) to + the incremental parser. It's not meant to be a speed test but to test the + accuracy of the incremental parser. It takes one argument: the name of the + input file. +- `test_json_parser_perf` is for speed testing both the standard + recursive descent parser and the non-recursive incremental + parser. If given the `-i` flag it uses the non-recursive parser, + otherwise the stardard parser. The remaining flags are the number of + parsing iterations and the file containing the input. Even when + using the non-recursive parser, the input is passed to the parser in a + single chunk. The results are thus comparable to those of the + standard parser. + +The easiest way to use these is to run `make check` and `make speed-check` + +The sample input file is a small extract from a list of `delicious` +bookmarks taken some years ago, all wrapped in a single json +array. 10,000 iterations of parsing this file gives a reasonable +benchmark, and that is what the `speed-check` target does. + diff --git a/src/test/modules/test_json_parser/t/001_test_json_parser_incremental.pl b/src/test/modules/test_json_parser/t/001_test_json_parser_incremental.pl new file mode 100644 index 0000000000..fc9718baf3 --- /dev/null +++ b/src/test/modules/test_json_parser/t/001_test_json_parser_incremental.pl @@ -0,0 +1,24 @@ + +use strict; +use warnings; + +use PostgreSQL::Test::Utils; +use Test::More; +use FindBin; + +my $test_file = "$FindBin::RealBin/../tiny.json"; + +my $exe = "$ENV{TESTDATADIR}/../test_json_parser_incremental"; + +my ($stdout, $stderr) = run_command( [$exe, $test_file] ); + +ok($stdout =~ /SUCCESS/, "test succeeds"); +ok(!$stderr, "no error output"); + + +done_testing(); + + + + + diff --git a/src/test/modules/test_json_parser/test_json_parser_incremental.c b/src/test/modules/test_json_parser/test_json_parser_incremental.c new file mode 100644 index 0000000000..edb51ef403 --- /dev/null +++ b/src/test/modules/test_json_parser/test_json_parser_incremental.c @@ -0,0 +1,77 @@ +/*------------------------------------------------------------------------- + * + * test_json_parser_incremental.c + * Test program for incremental JSON parser + * + * Copyright (c) 2023, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_json_parser/test_json_parser_incremental.c + * + * This progam tests incremental parsing of json. The input is fed into + * the parser in very small chunks. In practice you would normally use + * much larger chunks, but doing this makes it more likely that the + * full range of incement handling, especially in the lexer, is exercised. + * + * The argument specifies the file containing the JSON input. + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" +#include "common/jsonapi.h" +#include "lib/stringinfo.h" +#include "mb/pg_wchar.h" +#include <stdio.h> + +int +main(int argc, char **argv) +{ + /* max delicious line length is less than this */ + char buff[6001]; + FILE *json_file; + JsonParseErrorType result; + JsonLexContext lex; + StringInfoData json; + int n_read; + + makeJsonLexContextIncremental(&lex, PG_UTF8, false); + initStringInfo(&json); + + json_file = fopen(argv[1], "r"); + while ((n_read = fread(buff, 1, 60, json_file)) > 0) + { + appendBinaryStringInfo(&json, buff, n_read); + if (!feof(json_file)) + { + result = pg_parse_json_incremental(&lex, &nullSemAction, + json.data, json.len, + false); + if (result != JSON_INCOMPLETE) + { + fprintf(stderr, + "unexpected result %d (expecting %d) on token %s\n", + result, JSON_INCOMPLETE, lex.token_start); + exit(1); + } + resetStringInfo(&json); + } + else + { + result = pg_parse_json_incremental(&lex, &nullSemAction, + json.data, json.len, + true); + if (result != JSON_SUCCESS) + { + fprintf(stderr, + "unexpected result %d (expecting %d) on final chunk\n", + result, JSON_SUCCESS); + exit(1); + } + printf("SUCCESS!\n"); + break; + } + } + fclose(json_file); + exit(0); +} diff --git a/src/test/modules/test_json_parser/test_json_parser_perf.c b/src/test/modules/test_json_parser/test_json_parser_perf.c new file mode 100644 index 0000000000..517dc8529a --- /dev/null +++ b/src/test/modules/test_json_parser/test_json_parser_perf.c @@ -0,0 +1,86 @@ +/*------------------------------------------------------------------------- + * + * test_json_parser_perf.c + * Performancet est program for both flavors of the JSON parser + * + * Copyright (c) 2023, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_json_parser/test_json_parser_perf.c + * + * This progam tests either the standard (recursive descent) JSON parser + * or the incremental (table driven) parser, but without breaking the input + * into chunks in the latter case. Thus it can be used to compare the pure + * parsing speed of the two parsers. If the "-i" option is used, then the + * table driven parser is used. Otherwise, the recursive descent parser is + * used. + * + * The remaining arguments are the number of parsing iterations to be done + * and the file containing the JSON input. + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" +#include "common/jsonapi.h" +#include "lib/stringinfo.h" +#include "mb/pg_wchar.h" +#include <stdio.h> +#include <string.h> + +int +main(int argc, char **argv) +{ + /* max delicious line length is less than this */ + char buff[6001]; + FILE *json_file; + JsonParseErrorType result; + JsonLexContext *lex; + StringInfoData json; + int n_read; + int iter; + int use_inc = 0; + + initStringInfo(&json); + + if (strcmp(argv[1], "-i") == 0) + { + use_inc = 1; + argv++; + } + + sscanf(argv[1], "%d", &iter); + + json_file = fopen(argv[2], "r"); + while ((n_read = fread(buff, 1, 6000, json_file)) > 0) + { + appendBinaryStringInfo(&json, buff, n_read); + } + fclose(json_file); + for (int i = 0; i < iter; i++) + { + if (use_inc) + { + lex = makeJsonLexContextIncremental(NULL, PG_UTF8, false); + result = pg_parse_json_incremental(lex, &nullSemAction, + json.data, json.len, + true); + freeJsonLexContext(lex); + } + else + { + lex = makeJsonLexContextCstringLen(NULL, json.data, json.len, + PG_UTF8, false); + result = pg_parse_json(lex, &nullSemAction); + freeJsonLexContext(lex); + } + if (result != JSON_SUCCESS) + { + fprintf(stderr, + "unexpected result %d (expecting %d) on parse\n", + result, JSON_SUCCESS); + exit(1); + } + } + exit(0); +} diff --git a/src/test/modules/test_json_parser/tiny.json b/src/test/modules/test_json_parser/tiny.json new file mode 100644 index 0000000000..1f47e3741b --- /dev/null +++ b/src/test/modules/test_json_parser/tiny.json @@ -0,0 +1,378 @@ +[{ +"updated": "Tue, 08 Sep 2009 23:28:55 +0000", +"links": [{"href" +: "http://www.theatermania.com/broadway/", +"type": "text/html", "rel": "alternate"}], "title": "TheaterMania", "author": "mcasas1", "comments": "http://delicious.com/url/b5b3cbf9a9176fe43c27d7b4af94a422", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "TheaterMania"}, "link": "http://www.theatermania.com/broadway/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b5b3cbf9a9176fe43c27d7b4af94a422", "id": "http://delicious.com/url/b5b3cbf9a9176fe43c27d7b4af94a422#mcasas1", "tags": [{"term": "NYC", "scheme": "http://delicious.com/mcasas1/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 15:07:02 +0000", "links": [{"href": "http://snagajob.com/", "type": "text/html", "rel": "alternate"}], "title": "Jobs & Employment | Full & Part Time Job Search | SnagAJob.com", "author": "Irvinn", "comments": "http://delicious.com/url/90c6b38a375cf62280f9ebc89823f570", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Jobs & Employment | Full & Part Time Job Search | SnagAJob.com"}, "link": "http://snagajob.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/90c6b38a375cf62280f9ebc89823f570", "id": "http://delicious.com/url/90c6b38a375cf62280f9ebc89823f570#Irvinn", "tags": [{"term": "Jobs", "scheme": "http://delicious.com/Irvinn/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 08:27:38 +0000", "links": [{"href": "http://www.usabilityweb.nl/2009/09/ikea-haar-slimme-toepassing-van-mbti-principes/", "type": "text/html", "rel": "alternate"}], "title": "Usabilityweb \u00bb Blog Archive \u00bb IKEA haar slimme toepassing van MBTI principes", "author": "NicoDruif", "comments": "http://delicious.com/url/5b3bc7d41a07f732dae9f3bac8859c50", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Usabilityweb \u00bb Blog Archive \u00bb IKEA haar slimme toepassing van MBTI principes"}, "link": "http://www.usabilityweb.nl/2009/09/ikea-haar-slimme-toepassing-van-mbti-principes/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5b3bc7d41a07f732dae9f3bac8859c50", "id": "http://delicious.com/url/5b3bc7d41a07f732dae9f3bac8859c50#NicoDruif", "tags": [{"term": "design", "scheme": "http://delicious.com/NicoDruif/", "label": null}, {"term": "usability", "scheme": "http://delicious.com/NicoDruif/", "label": null}, {"term": "inspiration", "scheme": "http://delicious.com/NicoDruif/", "label": null}, {"term": "interactiondesign", "scheme": "http://delicious.com/NicoDruif/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 08:11:20 +0000", "links": [{"href": "http://www.cegui.org.uk/wiki/index.php/Main_Page", "type": "text/html", "rel": "alternate"}], "title": "CEGUIWiki", "author": "renato.cron", "comments": "http://delicious.com/url/fa4eb9b9833c073a50c1a9db4bebc007", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "CEGUIWiki"}, "link": "http://www.cegui.org.uk/wiki/index.php/Main_Page", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/fa4eb9b9833c073a50c1a9db4bebc007", "id": "http://delicious.com/url/fa4eb9b9833c073a50c1a9db4bebc007#renato.cron", "tags": [{"term": "gui", "scheme": "http://delicious.com/renato.cron/", "label": null}, {"term": "programming", "scheme": "http://delicious.com/renato.cron/", "label": null}, {"term": "opensource", "scheme": "http://delicious.com/renato.cron/", "label": null}, {"term": "c++", "scheme": "http://delicious.com/renato.cron/", "label": null}, {"term": "gamedev", "scheme": "http://delicious.com/renato.cron/", "label": null}, {"term": "library", "scheme": "http://delicious.com/renato.cron/", "label": null}, {"term": "3d", "scheme": "http://delicious.com/renato.cron/", "label": null}, {"term": "graphics", "scheme": "http://delicious.com/renato.cron/", "label": null}, {"term": "openGl", "scheme": "http://delicious.com/renato.cron/", "label": null}, {"term": "games", "scheme": "http://delicious.com/renato.cron/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 08:45:00 +0000", "links": [{"href": "http://www.mcfc.co.uk/", "type": "text/html", "rel": "alternate"}], "title": "Home - Manchester City FC", "author": "cainarachi", "comments": "http://delicious.com/url/b7cdad040b7e1d0aec0c93b1b8c4bb41", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Home - Manchester City FC"}, "link": "http://www.mcfc.co.uk/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b7cdad040b7e1d0aec0c93b1b8c4bb41", "id": "http://delicious.com/url/b7cdad040b7e1d0aec0c93b1b8c4bb41#cainarachi", "tags": [{"term": "bonssites", "scheme": "http://delicious.com/cainarachi/", "label": null}, {"term": "html", "scheme": "http://delicious.com/cainarachi/", "label": null}, {"term": "webdesign", "scheme": "http://delicious.com/cainarachi/", "label": null}, {"term": "css", "scheme": "http://delicious.com/cainarachi/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 13:22:03 +0000", "links": [{"href": "http://www.dailymotion.com/swf/x3skf8", "type": "text/html", "rel": "alternate"}], "title": "http://www.dailymotion.com/swf/x3skf8", "author": "siljedahl", "comments": "http://delicious.com/url/d54fed3724a99387b30e775e986467b7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "http://www.dailymotion.com/swf/x3skf8"}, "link": "http://www.dailymotion.com/swf/x3skf8", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d54fed3724a99387b30e775e986467b7", "id": "http://delicious.com/url/d54fed3724a99387b30e775e986467b7#siljedahl", "tags": [{"term": "videoklipp", "scheme": "http://delicious.com/siljedahl/", "label": null}, {"term": "fildelning", "scheme": "http://delicious.com/siljedahl/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 10:40:37 +0000", "links": [{"href": "http://www.informationisbeautiful.net/visualizations/the-hierarchy-of-digital-distractions/", "type": "text/html", "rel": "alternate"}], "title": "http://www.informationisbeautiful.net/visualizations/the-hierarchy-of-digital-distractions/", "author": "Torill", "comments": "http://delicious.com/url/2868389724d31da197acca438602c7ac", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "http://www.informationisbeautiful.net/visualizations/the-hierarchy-of-digital-distractions/"}, "link": "http://www.informationisbeautiful.net/visualizations/the-hierarchy-of-digital-distractions/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2868389724d31da197acca438602c7ac", "id": "http://delicious.com/url/2868389724d31da197acca438602c7ac#Torill", "tags": [{"term": "internet", "scheme": "http://delicious.com/Torill/", "label": null}, {"term": "culture", "scheme": "http://delicious.com/Torill/", "label": null}, {"term": "web", "scheme": "http://delicious.com/Torill/", "label": null}, {"term": "digital", "scheme": "http://delicious.com/Torill/", "label": null}, {"term": "information", "scheme": "http://delicious.com/Torill/", "label": null}, {"term": "humour", "scheme": "http://delicious.com/Torill/", "label": null}, {"term": "procrastination", "scheme": "http://delicious.com/Torill/", "label": null}, {"term": "infographics", "scheme": "http://delicious.com/Torill/", "label": null}, {"term": "visualization", "scheme": "http://delicious.com/Torill/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 23:40:08 +0000", "links": [{"href": "http://technet.microsoft.com/en-us/sysinternals/bb545046.aspx", "type": "text/html", "rel": "alternate"}], "title": "File and Disk Utilities: Sysinternals Center, Microsoft TechNet", "author": "kevross7", "comments": "http://delicious.com/url/16333ab089d09ccbc6989a8a69d6194a", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "File and Disk Utilities: Sysinternals Center, Microsoft TechNet"}, "link": "http://technet.microsoft.com/en-us/sysinternals/bb545046.aspx", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/16333ab089d09ccbc6989a8a69d6194a", "id": "http://delicious.com/url/16333ab089d09ccbc6989a8a69d6194a#kevross7", "tags": [{"term": "PCTech", "scheme": "http://delicious.com/kevross7/", "label": null}, {"term": "PCSoftware", "scheme": "http://delicious.com/kevross7/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 04:16:52 +0000", "links": [{"href": "http://www.elchiguirebipolar.com/", "type": "text/html", "rel": "alternate"}], "title": "El Chig\u00fcire Bipolar", "author": "meste48", "comments": "http://delicious.com/url/3b16d7fabb6188d23c78ddadee0153f3", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "El Chig\u00fcire Bipolar"}, "link": "http://www.elchiguirebipolar.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3b16d7fabb6188d23c78ddadee0153f3", "id": "http://delicious.com/url/3b16d7fabb6188d23c78ddadee0153f3#meste48", "tags": [{"term": "noticias", "scheme": "http://delicious.com/meste48/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 01:06:19 +0000", "links": [{"href": "http://christiandefense.org/", "type": "text/html", "rel": "alternate"}], "title": "Department of Christian Defense", "author": "tomz0rs", "comments": "http://delicious.com/url/0d49ca3d623b642d3d2d251ca9665d6a", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Department of Christian Defense"}, "link": "http://christiandefense.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0d49ca3d623b642d3d2d251ca9665d6a", "id": "http://delicious.com/url/0d49ca3d623b642d3d2d251ca9665d6a#tomz0rs", "tags": [{"term": "Edward_Dalcour", "scheme": "http://delicious.com/tomz0rs/", "label": null}, {"term": "Christianity", "scheme": "http://delicious.com/tomz0rs/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 15:43:52 +0000", "links": [{"href": "http://www.wikihow.com/Paint-a-Bike", "type": "text/html", "rel": "alternate"}], "title": "How to Paint a Bike - wikiHow", "author": "bad_dave", "comments": "http://delicious.com/url/d526f901dde2937ccc8366e546fd1898", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "How to Paint a Bike - wikiHow"}, "link": "http://www.wikihow.com/Paint-a-Bike", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d526f901dde2937ccc8366e546fd1898", "id": "http://delicious.com/url/d526f901dde2937ccc8366e546fd1898#bad_dave", "tags": [{"term": "bikes", "scheme": "http://delicious.com/bad_dave/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 15:34:12 +0000", "links": [{"href": "http://www.swgraphic.com/v2/", "type": "text/html", "rel": "alternate"}], "title": "SW GRAPHIC | Portfolio of Sarah Wu", "author": "oscarbarber", "comments": "http://delicious.com/url/88c271441cb9f3690f2cc0cde308825e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "SW GRAPHIC | Portfolio of Sarah Wu"}, "link": "http://www.swgraphic.com/v2/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/88c271441cb9f3690f2cc0cde308825e", "id": "http://delicious.com/url/88c271441cb9f3690f2cc0cde308825e#oscarbarber", "tags": [{"term": "portfolio", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "Inspiration", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "webdesign", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "design", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "clean", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "blue", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "purple", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "slider", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "grey", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "pink", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "background", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "illustration", "scheme": "http://delicious.com/oscarbarber/", "label": null}, {"term": "agency", "scheme": "http://delicious.com/oscarbarber/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 12:24:31 +0000", "links": [{"href": "http://lucullian.blogspot.com/", "type": "text/html", "rel": "alternate"}], "title": "Lucullian delights - an Italian experience", "author": "firetriniti", "comments": "http://delicious.com/url/e6b2c8d1a50ef79736f1eae160a3c218", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Lucullian delights - an Italian experience"}, "link": "http://lucullian.blogspot.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e6b2c8d1a50ef79736f1eae160a3c218", "id": "http://delicious.com/url/e6b2c8d1a50ef79736f1eae160a3c218#firetriniti", "tags": [{"term": "food", "scheme": "http://delicious.com/firetriniti/", "label": null}, {"term": "blog", "scheme": "http://delicious.com/firetriniti/", "label": null}, {"term": "cooking", "scheme": "http://delicious.com/firetriniti/", "label": null}, {"term": "italian", "scheme": "http://delicious.com/firetriniti/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 19:32:44 +0000", "links": [{"href": "http://andrewmcafee.org/2009/08/the-cloudy-future-of-corporate-it/", "type": "text/html", "rel": "alternate"}], "title": "The Cloudy Future of Corporate IT : Andrew McAfee\u2019s Blog", "author": "AThou111", "comments": "http://delicious.com/url/0a45a25616b8fe5496a333d280bb4208", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Cloudy Future of Corporate IT : Andrew McAfee\u2019s Blog"}, "link": "http://andrewmcafee.org/2009/08/the-cloudy-future-of-corporate-it/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0a45a25616b8fe5496a333d280bb4208", "id": "http://delicious.com/url/0a45a25616b8fe5496a333d280bb4208#AThou111", "tags": [{"term": "cloudcomputing", "scheme": "http://delicious.com/AThou111/", "label": null}, {"term": "cloud", "scheme": "http://delicious.com/AThou111/", "label": null}, {"term": "enterprise2.0", "scheme": "http://delicious.com/AThou111/", "label": null}, {"term": "saas", "scheme": "http://delicious.com/AThou111/", "label": null}, {"term": "technology", "scheme": "http://delicious.com/AThou111/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 18:44:21 +0000", "links": [{"href": "http://www.dlib.org/dlib/april03/staples/04staples.html", "type": "text/html", "rel": "alternate"}], "title": "The Fedora Project: An Open-source Digital Object Repository Management ...", "author": "jpuglisi", "comments": "http://delicious.com/url/40a7e91564aa5b60ade6984b9c0ee9ec", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Fedora Project: An Open-source Digital Object Repository Management ..."}, "link": "http://www.dlib.org/dlib/april03/staples/04staples.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/40a7e91564aa5b60ade6984b9c0ee9ec", "id": "http://delicious.com/url/40a7e91564aa5b60ade6984b9c0ee9ec#jpuglisi", "tags": [{"term": "fedora", "scheme": "http://delicious.com/jpuglisi/", "label": null}, {"term": "digitallibrary", "scheme": "http://delicious.com/jpuglisi/", "label": null}, {"term": "d-libmag", "scheme": "http://delicious.com/jpuglisi/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 02:12:07 +0000", "links": [{"href": "http://www.scorehero.com/", "type": "text/html", "rel": "alternate"}], "title": "Score Hero - Home", "author": "juanrs", "comments": "http://delicious.com/url/4aed119ff40c09e406073a4203a455d7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Score Hero - Home"}, "link": "http://www.scorehero.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/4aed119ff40c09e406073a4203a455d7", "id": "http://delicious.com/url/4aed119ff40c09e406073a4203a455d7#juanrs", "tags": [{"term": "guitar", "scheme": "http://delicious.com/juanrs/", "label": null}, {"term": "hero", "scheme": "http://delicious.com/juanrs/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 15:33:59 +0000", "links": [{"href": "http://naldzgraphics.net/freebies/850-super-cool-tech-brushes-for-photoshop/", "type": "text/html", "rel": "alternate"}], "title": "850+ Super Cool Tech Brushes for Photoshop | Naldz Graphics", "author": "eaymerich", "comments": "http://delicious.com/url/d073469bc40abe81db81af3a0587f9e8", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "850+ Super Cool Tech Brushes for Photoshop | Naldz Graphics"}, "link": "http://naldzgraphics.net/freebies/850-super-cool-tech-brushes-for-photoshop/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d073469bc40abe81db81af3a0587f9e8", "id": "http://delicious.com/url/d073469bc40abe81db81af3a0587f9e8#eaymerich", "tags": [{"term": "photoshop", "scheme": "http://delicious.com/eaymerich/", "label": null}, {"term": "brushes", "scheme": "http://delicious.com/eaymerich/", "label": null}, {"term": "design", "scheme": "http://delicious.com/eaymerich/", "label": null}, {"term": "free", "scheme": "http://delicious.com/eaymerich/", "label": null}, {"term": "resources", "scheme": "http://delicious.com/eaymerich/", "label": null}, {"term": "graphics", "scheme": "http://delicious.com/eaymerich/", "label": null}, {"term": "webdesign", "scheme": "http://delicious.com/eaymerich/", "label": null}, {"term": "tech", "scheme": "http://delicious.com/eaymerich/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 14:27:29 +0000", "links": [{"href": "http://www.microsoft.com/Presspass/gallery/ms-logos.mspx?FINISH=YES", "type": "text/html", "rel": "alternate"}], "title": "Microsoft PressPass - Image Gallery: Microsoft Logos:", "author": "brunokenj", "comments": "http://delicious.com/url/c666ac39ef072d4eae96a7f062b3b93d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Microsoft PressPass - Image Gallery: Microsoft Logos:"}, "link": "http://www.microsoft.com/Presspass/gallery/ms-logos.mspx?FINISH=YES", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c666ac39ef072d4eae96a7f062b3b93d", "id": "http://delicious.com/url/c666ac39ef072d4eae96a7f062b3b93d#brunokenj", "tags": [{"term": "microsoft", "scheme": "http://delicious.com/brunokenj/", "label": null}, {"term": "logos", "scheme": "http://delicious.com/brunokenj/", "label": null}, {"term": "images", "scheme": "http://delicious.com/brunokenj/", "label": null}, {"term": "logo", "scheme": "http://delicious.com/brunokenj/", "label": null}, {"term": "design", "scheme": "http://delicious.com/brunokenj/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 13:00:00 +0000", "links": [{"href": "http://www.pristineclassical.com/index2.html", "type": "text/html", "rel": "alternate"}], "title": "Pristine Classical - The Greatest Recordings - The Finest Sound - Home Page", "author": "ngoldberg", "comments": "http://delicious.com/url/f2a1632abe0284bdaa9ac3b825a97af7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Pristine Classical - The Greatest Recordings - The Finest Sound - Home Page"}, "link": "http://www.pristineclassical.com/index2.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f2a1632abe0284bdaa9ac3b825a97af7", "id": "http://delicious.com/url/f2a1632abe0284bdaa9ac3b825a97af7#ngoldberg", "tags": [{"term": "music", "scheme": "http://delicious.com/ngoldberg/", "label": null}, {"term": "shopping", "scheme": "http://delicious.com/ngoldberg/", "label": null}, {"term": "flac", "scheme": "http://delicious.com/ngoldberg/", "label": null}, {"term": "classical", "scheme": "http://delicious.com/ngoldberg/", "label": null}, {"term": "jazz", "scheme": "http://delicious.com/ngoldberg/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 18:29:55 +0000", "links": [{"href": "http://www.infoworld.com/d/open-source/best-open-source-software-awards-2009-628?page=0,3", "type": "text/html", "rel": "alternate"}], "title": "Best of Open Source Software Awards 2009 | Open Source - InfoWorld", "author": "mbayia", "comments": "http://delicious.com/url/d3ff3247bf35f421cc080a0f6d4d8165", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Best of Open Source Software Awards 2009 | Open Source - InfoWorld"}, "link": "http://www.infoworld.com/d/open-source/best-open-source-software-awards-2009-628?page=0,3", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d3ff3247bf35f421cc080a0f6d4d8165", "id": "http://delicious.com/url/d3ff3247bf35f421cc080a0f6d4d8165#mbayia", "tags": [{"term": "Opensource", "scheme": "http://delicious.com/mbayia/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 23:20:44 +0000", "links": [{"href": "http://www.steampunklab.com/", "type": "text/html", "rel": "alternate"}], "title": "Steampunk Lab - Powering Steampunk Innovation", "author": "LaughingRhoda", "comments": "http://delicious.com/url/824005be2016dcf642b4b67b19377aea", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Steampunk Lab - Powering Steampunk Innovation"}, "link": "http://www.steampunklab.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/824005be2016dcf642b4b67b19377aea", "id": "http://delicious.com/url/824005be2016dcf642b4b67b19377aea#LaughingRhoda", "tags": [{"term": "steampunk", "scheme": "http://delicious.com/LaughingRhoda/", "label": null}, {"term": "dyi", "scheme": "http://delicious.com/LaughingRhoda/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 13:09:23 +0000", "links": [{"href": "http://www.ibm.com/developerworks/data/library/techarticle/dm-0801sauter/", "type": "text/html", "rel": "alternate"}], "title": "The information perspective of SOA design, Part 1: Introduction to the information perspective of a Service Oriented Architecture", "author": "bspaulding", "comments": "http://delicious.com/url/4d194dfd510d774df4507790e0b62f02", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The information perspective of SOA design, Part 1: Introduction to the information perspective of a Service Oriented Architecture"}, "link": "http://www.ibm.com/developerworks/data/library/techarticle/dm-0801sauter/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/4d194dfd510d774df4507790e0b62f02", "id": "http://delicious.com/url/4d194dfd510d774df4507790e0b62f02#bspaulding", "tags": [{"term": "soa", "scheme": "http://delicious.com/bspaulding/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 03:14:46 +0000", "links": [{"href": "http://d.hatena.ne.jp/mollifier/20090814/p1", "type": "text/html", "rel": "alternate"}], "title": "Git \u3060\u308d\u3046\u3068 Mercurial \u3060\u308d\u3046\u3068\u3001\u30d6\u30e9\u30f3\u30c1\u540d\u3092zsh\u306e\u30d7\u30ed\u30f3\u30d7\u30c8\u306b\u30b9\u30de\u30fc\u30c8\u306b\u8868\u793a\u3059\u308b\u65b9\u6cd5 - ess sup", "author": "yoggy", "comments": "http://delicious.com/url/2d1c17ad966f2a3b9a23e5de91d16b40", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Git \u3060\u308d\u3046\u3068 Mercurial \u3060\u308d\u3046\u3068\u3001\u30d6\u30e9\u30f3\u30c1\u540d\u3092zsh\u306e\u30d7\u30ed\u30f3\u30d7\u30c8\u306b\u30b9\u30de\u30fc\u30c8\u306b\u8868\u793a\u3059\u308b\u65b9\u6cd5 - ess sup"}, "link": "http://d.hatena.ne.jp/mollifier/20090814/p1", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2d1c17ad966f2a3b9a23e5de91d16b40", "id": "http://delicious.com/url/2d1c17ad966f2a3b9a23e5de91d16b40#yoggy", "tags": [{"term": "subversion", "scheme": "http://delicious.com/yoggy/", "label": null}, {"term": "git", "scheme": "http://delicious.com/yoggy/", "label": null}, {"term": "zsh", "scheme": "http://delicious.com/yoggy/", "label": null}, {"term": "tips", "scheme": "http://delicious.com/yoggy/", "label": null}, {"term": "configuration", "scheme": "http://delicious.com/yoggy/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 15:48:20 +0000", "links": [{"href": "https://spreadsheets.google.com/ccc?key=0ApUZq_fYzKcRdEVtMldyc3hnYS1XZnhYcXpZcHE5QkE&hl=en", "type": "text/html", "rel": "alternate"}], "title": "Censo de bibliotecas con cuenta en Twitter", "author": "GeekTecaBiblio", "comments": "http://delicious.com/url/239b7bb0e4ab280cea61bede251e46be", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Censo de bibliotecas con cuenta en Twitter"}, "link": "https://spreadsheets.google.com/ccc?key=0ApUZq_fYzKcRdEVtMldyc3hnYS1XZnhYcXpZcHE5QkE&hl=en", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/239b7bb0e4ab280cea61bede251e46be", "id": "http://delicious.com/url/239b7bb0e4ab280cea61bede251e46be#GeekTecaBiblio", "tags": [{"term": "Bibliotecas", "scheme": "http://delicious.com/GeekTecaBiblio/", "label": null}, {"term": "Biblioteca2.0", "scheme": "http://delicious.com/GeekTecaBiblio/", "label": null}, {"term": "Twitter", "scheme": "http://delicious.com/GeekTecaBiblio/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 23:27:11 +0000", "links": [{"href": "http://stevie-sedai.livejournal.com/699.html", "type": "text/html", "rel": "alternate"}], "title": "stevie_sedai - Jared The Barbarian", "author": "Space.Clown", "comments": "http://delicious.com/url/19e5680d71d7f078fffb0045a26ef288", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "stevie_sedai - Jared The Barbarian"}, "link": "http://stevie-sedai.livejournal.com/699.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/19e5680d71d7f078fffb0045a26ef288", "id": "http://delicious.com/url/19e5680d71d7f078fffb0045a26ef288#Space.Clown", "tags": [{"term": "j2fic", "scheme": "http://delicious.com/Space.Clown/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 06:25:50 +0000", "links": [{"href": "http://www.hulu.com/", "type": "text/html", "rel": "alternate"}], "title": "Hulu - Watch your favorites. Anytime. For free.", "author": "lorellewong", "comments": "http://delicious.com/url/19585f3671ae8e2beb6a962f62fa1841", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Hulu - Watch your favorites. Anytime. For free."}, "link": "http://www.hulu.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/19585f3671ae8e2beb6a962f62fa1841", "id": "http://delicious.com/url/19585f3671ae8e2beb6a962f62fa1841#lorellewong", "tags": [{"term": "entertainment", "scheme": "http://delicious.com/lorellewong/", "label": null}, {"term": "streaming_tv", "scheme": "http://delicious.com/lorellewong/", "label": null}, {"term": "online", "scheme": "http://delicious.com/lorellewong/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 14:27:56 +0000", "links": [{"href": "http://artofhacking.com/etc/passwd.htm", "type": "text/html", "rel": "alternate"}], "title": "3678 Default Passwords from AOH", "author": "dgolda", "comments": "http://delicious.com/url/77b29ae6a356a93b9f1311e8874552de", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "3678 Default Passwords from AOH"}, "link": "http://artofhacking.com/etc/passwd.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/77b29ae6a356a93b9f1311e8874552de", "id": "http://delicious.com/url/77b29ae6a356a93b9f1311e8874552de#dgolda", "tags": [{"term": "security", "scheme": "http://delicious.com/dgolda/", "label": null}, {"term": "passwords", "scheme": "http://delicious.com/dgolda/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 19:01:08 +0000", "links": [{"href": "http://meyerweb.com/eric/tools/s5/", "type": "text/html", "rel": "alternate"}], "title": "S5: A Simple Standards-Based Slide Show System", "author": "arecki", "comments": "http://delicious.com/url/79ab6c018852ae137a4f7390a29fbaa9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "S5: A Simple Standards-Based Slide Show System"}, "link": "http://meyerweb.com/eric/tools/s5/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/79ab6c018852ae137a4f7390a29fbaa9", "id": "http://delicious.com/url/79ab6c018852ae137a4f7390a29fbaa9#arecki", "tags": [{"term": "webdesign", "scheme": "http://delicious.com/arecki/", "label": null}, {"term": "js", "scheme": "http://delicious.com/arecki/", "label": null}, {"term": "HTML", "scheme": "http://delicious.com/arecki/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 18:56:30 +0000", "links": [{"href": "http://www.libervis.com/wiki/index.php?title=Table_of_Equivalent_Software", "type": "text/html", "rel": "alternate"}], "title": "Table of Equivalent Software - Libervis Wiki", "author": "metalgalore", "comments": "http://delicious.com/url/baea1a8a8dc4204bb8353484dc5fd818", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Table of Equivalent Software - Libervis Wiki"}, "link": "http://www.libervis.com/wiki/index.php?title=Table_of_Equivalent_Software", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/baea1a8a8dc4204bb8353484dc5fd818", "id": "http://delicious.com/url/baea1a8a8dc4204bb8353484dc5fd818#metalgalore", "tags": [{"term": "linux", "scheme": "http://delicious.com/metalgalore/", "label": null}, {"term": "windows", "scheme": "http://delicious.com/metalgalore/", "label": null}, {"term": "programs", "scheme": "http://delicious.com/metalgalore/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 07:59:35 +0000", "links": [{"href": "http://www.detroitrenaissance.com/", "type": "text/html", "rel": "alternate"}], "title": "Detroit Renaissance", "author": "mxnomad", "comments": "http://delicious.com/url/be668db06ed2dea3a968c74279ee906e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Detroit Renaissance"}, "link": "http://www.detroitrenaissance.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/be668db06ed2dea3a968c74279ee906e", "id": "http://delicious.com/url/be668db06ed2dea3a968c74279ee906e#mxnomad", "tags": [{"term": "detroit", "scheme": "http://delicious.com/mxnomad/", "label": null}, {"term": "development", "scheme": "http://delicious.com/mxnomad/", "label": null}, {"term": "urban", "scheme": "http://delicious.com/mxnomad/", "label": null}, {"term": "business", "scheme": "http://delicious.com/mxnomad/", "label": null}, {"term": "resources", "scheme": "http://delicious.com/mxnomad/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 16:13:25 +0000", "links": [{"href": "http://dustbowl.wordpress.com/", "type": "text/html", "rel": "alternate"}], "title": "Dustbowl", "author": "ninnisse", "comments": "http://delicious.com/url/353269ddc6d326175d242a30cac8a9ef", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Dustbowl"}, "link": "http://dustbowl.wordpress.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/353269ddc6d326175d242a30cac8a9ef", "id": "http://delicious.com/url/353269ddc6d326175d242a30cac8a9ef#ninnisse", "tags": [{"term": "futurbillet", "scheme": "http://delicious.com/ninnisse/", "label": null}, {"term": "aware", "scheme": "http://delicious.com/ninnisse/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 22:29:54 +0000", "links": [{"href": "http://designora.com/tools/photshop-brushes-floral/", "type": "text/html", "rel": "alternate"}], "title": "100+ Floral Brushes for Photoshop | Designora: Flash Design, Graphic Design, Photography, and More", "author": "Eifonso", "comments": "http://delicious.com/url/a08e4d1ea60846a67347bb6f07be2874", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "100+ Floral Brushes for Photoshop | Designora: Flash Design, Graphic Design, Photography, and More"}, "link": "http://designora.com/tools/photshop-brushes-floral/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a08e4d1ea60846a67347bb6f07be2874", "id": "http://delicious.com/url/a08e4d1ea60846a67347bb6f07be2874#Eifonso", "tags": [{"term": "brushes", "scheme": "http://delicious.com/Eifonso/", "label": null}, {"term": "photoshop", "scheme": "http://delicious.com/Eifonso/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 13:49:56 +0000", "links": [{"href": "http://www.bpb.de/", "type": "text/html", "rel": "alternate"}], "title": "Bundeszentrale f\u00fcr politische Bildung", "author": "patrick.lengauer", "comments": "http://delicious.com/url/d58412ffa44904d75f49b79d25d8532d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Bundeszentrale f\u00fcr politische Bildung"}, "link": "http://www.bpb.de/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d58412ffa44904d75f49b79d25d8532d", "id": "http://delicious.com/url/d58412ffa44904d75f49b79d25d8532d#patrick.lengauer", "tags": [{"term": "bildung", "scheme": "http://delicious.com/patrick.lengauer/", "label": null}, {"term": "politik", "scheme": "http://delicious.com/patrick.lengauer/", "label": null}, {"term": "portal", "scheme": "http://delicious.com/patrick.lengauer/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 14:30:47 +0000", "links": [{"href": "http://designreviver.com/tips/a-collection-of-wordpress-tutorials-tips-and-themes/", "type": "text/html", "rel": "alternate"}], "title": "A Collection of Wordpress Tutorials, Tips and Themes | Design Reviver", "author": "mccarrd4", "comments": "http://delicious.com/url/5554037781330ada702fcdb8c9d09b7d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "A Collection of Wordpress Tutorials, Tips and Themes | Design Reviver"}, "link": "http://designreviver.com/tips/a-collection-of-wordpress-tutorials-tips-and-themes/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5554037781330ada702fcdb8c9d09b7d", "id": "http://delicious.com/url/5554037781330ada702fcdb8c9d09b7d#mccarrd4"}, +{"updated": "Mon, 07 Sep 2009 09:56:20 +0000", "links": [{"href": "http://d.hatena.ne.jp/rubikitch/20080627/1214504190", "type": "text/html", "rel": "alternate"}], "title": "Ruby\u3067web\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u306a\u3089httpclient\u304c\u624b\u8efd - (rubikitch loves (Emacs Ruby CUI))", "author": "ytesaki", "comments": "http://delicious.com/url/279ab27c36a66eccf7a945c8cff70edd", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Ruby\u3067web\u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u306a\u3089httpclient\u304c\u624b\u8efd - (rubikitch loves (Emacs Ruby CUI))"}, "link": "http://d.hatena.ne.jp/rubikitch/20080627/1214504190", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/279ab27c36a66eccf7a945c8cff70edd", "id": "http://delicious.com/url/279ab27c36a66eccf7a945c8cff70edd#ytesaki", "tags": [{"term": "httpclient", "scheme": "http://delicious.com/ytesaki/", "label": null}, {"term": "ruby", "scheme": "http://delicious.com/ytesaki/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 05:29:57 +0000", "links": [{"href": "https://www.clarityaccounting.com/", "type": "text/html", "rel": "alternate"}], "title": "Clarity Accounting", "author": "jadent", "comments": "http://delicious.com/url/3e55f9983739f79ed9b5c4148f627f47", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Clarity Accounting"}, "link": "https://www.clarityaccounting.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3e55f9983739f79ed9b5c4148f627f47", "id": "http://delicious.com/url/3e55f9983739f79ed9b5c4148f627f47#jadent", "tags": [{"term": "Software", "scheme": "http://delicious.com/jadent/", "label": null}, {"term": "OnDemand", "scheme": "http://delicious.com/jadent/", "label": null}, {"term": "Accounting", "scheme": "http://delicious.com/jadent/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 18:45:13 +0000", "links": [{"href": "http://www.playlist.com/", "type": "text/html", "rel": "alternate"}], "title": "Create & Share Music Playlists, Search & Listen to Favorite Music ...", "author": "Disc0xBabii", "comments": "http://delicious.com/url/fef0ffbd2476663ecdf7c65bc9a09117", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Create & Share Music Playlists, Search & Listen to Favorite Music ..."}, "link": "http://www.playlist.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/fef0ffbd2476663ecdf7c65bc9a09117", "id": "http://delicious.com/url/fef0ffbd2476663ecdf7c65bc9a09117#Disc0xBabii"}, +{"updated": "Mon, 07 Sep 2009 20:36:36 +0000", "links": [{"href": "http://editkid.com/upload_to_ftp/", "type": "text/html", "rel": "alternate"}], "title": "Upload to FTP - an Automator action for OS X", "author": "TimTim74", "comments": "http://delicious.com/url/40762c795d7cab27c95bb2a2dbbaefe0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Upload to FTP - an Automator action for OS X"}, "link": "http://editkid.com/upload_to_ftp/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/40762c795d7cab27c95bb2a2dbbaefe0", "id": "http://delicious.com/url/40762c795d7cab27c95bb2a2dbbaefe0#TimTim74", "tags": [{"term": "mac", "scheme": "http://delicious.com/TimTim74/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 22:53:43 +0000", "links": [{"href": "http://www.dmlcompetition.net/", "type": "text/html", "rel": "alternate"}], "title": "Digital Media and Learning Competition", "author": "Irene_buck", "comments": "http://delicious.com/url/f41077ff004839461ed5bef48a493023", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Digital Media and Learning Competition"}, "link": "http://www.dmlcompetition.net/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f41077ff004839461ed5bef48a493023", "id": "http://delicious.com/url/f41077ff004839461ed5bef48a493023#Irene_buck", "tags": [{"term": "Deiginal", "scheme": "http://delicious.com/Irene_buck/", "label": null}, {"term": "media", "scheme": "http://delicious.com/Irene_buck/", "label": null}, {"term": "grants", "scheme": "http://delicious.com/Irene_buck/", "label": null}, {"term": "competition", "scheme": "http://delicious.com/Irene_buck/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 16:38:40 +0000", "links": [{"href": "http://www.ncbi.nlm.nih.gov/pubmed/", "type": "text/html", "rel": "alternate"}], "title": "PubMed Home", "author": "sskaye", "comments": "http://delicious.com/url/63d884d7178774934c284656b64fa9cb", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "PubMed Home"}, "link": "http://www.ncbi.nlm.nih.gov/pubmed/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/63d884d7178774934c284656b64fa9cb", "id": "http://delicious.com/url/63d884d7178774934c284656b64fa9cb#sskaye", "tags": [{"term": "medical", "scheme": "http://delicious.com/sskaye/", "label": null}, {"term": "library", "scheme": "http://delicious.com/sskaye/", "label": null}, {"term": "research", "scheme": "http://delicious.com/sskaye/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 23:48:14 +0000", "links": [{"href": "http://www.miller-mccune.com/business_economics/computer-error-1390?article_page=1", "type": "text/html", "rel": "alternate"}], "title": "Business & Economics Articles | Cheap, Effective Alternatives to One Laptop per Child | Miller-McCune Online Magazine", "author": "anthonylv", "comments": "http://delicious.com/url/cf7c496914a3fa23919eeadc67b80234", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Business & Economics Articles | Cheap, Effective Alternatives to One Laptop per Child | Miller-McCune Online Magazine"}, "link": "http://www.miller-mccune.com/business_economics/computer-error-1390?article_page=1", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/cf7c496914a3fa23919eeadc67b80234", "id": "http://delicious.com/url/cf7c496914a3fa23919eeadc67b80234#anthonylv", "tags": [{"term": "development", "scheme": "http://delicious.com/anthonylv/", "label": null}, {"term": "education", "scheme": "http://delicious.com/anthonylv/", "label": null}, {"term": "computers", "scheme": "http://delicious.com/anthonylv/", "label": null}, {"term": "olpc", "scheme": "http://delicious.com/anthonylv/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 23:58:53 +0000", "links": [{"href": "http://www.cherylsigmon.com/handouts.asp", "type": "text/html", "rel": "alternate"}], "title": "Cheryl Sigmon's Website", "author": "kfoyle", "comments": "http://delicious.com/url/ef120fc29139c9bfe1c110a821a93295", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Cheryl Sigmon's Website"}, "link": "http://www.cherylsigmon.com/handouts.asp", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ef120fc29139c9bfe1c110a821a93295", "id": "http://delicious.com/url/ef120fc29139c9bfe1c110a821a93295#kfoyle", "tags": [{"term": "4blocks", "scheme": "http://delicious.com/kfoyle/", "label": null}, {"term": "education", "scheme": "http://delicious.com/kfoyle/", "label": null}, {"term": "language", "scheme": "http://delicious.com/kfoyle/", "label": null}, {"term": "literacy", "scheme": "http://delicious.com/kfoyle/", "label": null}, {"term": "printables", "scheme": "http://delicious.com/kfoyle/", "label": null}, {"term": "reading", "scheme": "http://delicious.com/kfoyle/", "label": null}, {"term": "resources", "scheme": "http://delicious.com/kfoyle/", "label": null}, {"term": "school", "scheme": "http://delicious.com/kfoyle/", "label": null}, {"term": "worksheets", "scheme": "http://delicious.com/kfoyle/", "label": null}, {"term": "writing", "scheme": "http://delicious.com/kfoyle/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 19:46:19 +0000", "links": [{"href": "http://phobos.xtec.net/jcardena/", "type": "text/html", "rel": "alternate"}], "title": "jcardena", "author": "arwenshions", "comments": "http://delicious.com/url/20496f6949ed71477f9543c578669a3f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "jcardena"}, "link": "http://phobos.xtec.net/jcardena/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/20496f6949ed71477f9543c578669a3f", "id": "http://delicious.com/url/20496f6949ed71477f9543c578669a3f#arwenshions", "tags": [{"term": "tic", "scheme": "http://delicious.com/arwenshions/", "label": null}, {"term": "escola", "scheme": "http://delicious.com/arwenshions/", "label": null}, {"term": "educaci\u00f3", "scheme": "http://delicious.com/arwenshions/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 01:45:41 +0000", "links": [{"href": "http://www.ideaxidea.com/archives/2009/09/bash_prompt_customize.html", "type": "text/html", "rel": "alternate"}], "title": "bash\u306e\u30b3\u30de\u30f3\u30c9\u30d7\u30ed\u30f3\u30d7\u30c8\u3092\u7d20\u6575\u306b\u30ab\u30b9\u30bf\u30de\u30a4\u30ba\u3059\u308b8\u3064\u306e\u65b9\u6cd5 - IDEA*IDEA \uff5e \u767e\u5f0f\u7ba1\u7406\u4eba\u306e\u30e9\u30a4\u30d5\u30cf\u30c3\u30af\u30d6\u30ed\u30b0", "author": "J138", "comments": "http://delicious.com/url/ebbfe054f89adb2365a16b59d69d8ae0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "bash\u306e\u30b3\u30de\u30f3\u30c9\u30d7\u30ed\u30f3\u30d7\u30c8\u3092\u7d20\u6575\u306b\u30ab\u30b9\u30bf\u30de\u30a4\u30ba\u3059\u308b8\u3064\u306e\u65b9\u6cd5 - IDEA*IDEA \uff5e \u767e\u5f0f\u7ba1\u7406\u4eba\u306e\u30e9\u30a4\u30d5\u30cf\u30c3\u30af\u30d6\u30ed\u30b0"}, "link": "http://www.ideaxidea.com/archives/2009/09/bash_prompt_customize.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ebbfe054f89adb2365a16b59d69d8ae0", "id": "http://delicious.com/url/ebbfe054f89adb2365a16b59d69d8ae0#J138", "tags": [{"term": "bash", "scheme": "http://delicious.com/J138/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 09:07:29 +0000", "links": [{"href": "http://www.syberpunk.com/cgi-bin/index.pl?page=nasubi", "type": "text/html", "rel": "alternate"}], "title": "s y b e r p u n k . c o m - Nasubi", "author": "hunther", "comments": "http://delicious.com/url/c9508223c058a09f56e952ebd0791ec2", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "s y b e r p u n k . c o m - Nasubi"}, "link": "http://www.syberpunk.com/cgi-bin/index.pl?page=nasubi", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c9508223c058a09f56e952ebd0791ec2", "id": "http://delicious.com/url/c9508223c058a09f56e952ebd0791ec2#hunther", "tags": [{"term": "TV-Format", "scheme": "http://delicious.com/hunther/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 13:40:16 +0000", "links": [{"href": "http://iwebkit.net/", "type": "text/html", "rel": "alternate"}], "title": "iWebKit - Make a quality iPhone Website or Webapp", "author": "bafication", "comments": "http://delicious.com/url/fef518e895130dd2dd5878681497cf1a", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "iWebKit - Make a quality iPhone Website or Webapp"}, "link": "http://iwebkit.net/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/fef518e895130dd2dd5878681497cf1a", "id": "http://delicious.com/url/fef518e895130dd2dd5878681497cf1a#bafication", "tags": [{"term": "iPhone", "scheme": "http://delicious.com/bafication/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 17:38:05 +0000", "links": [{"href": "http://www.soccerpunter.com/", "type": "text/html", "rel": "alternate"}], "title": "SoccerPunter.com", "author": "ewand", "comments": "http://delicious.com/url/e78a341b78f749a0fb1b93eb592634c4", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "SoccerPunter.com"}, "link": "http://www.soccerpunter.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e78a341b78f749a0fb1b93eb592634c4", "id": "http://delicious.com/url/e78a341b78f749a0fb1b93eb592634c4#ewand", "tags": [{"term": "gambling", "scheme": "http://delicious.com/ewand/", "label": null}, {"term": "football", "scheme": "http://delicious.com/ewand/", "label": null}, {"term": "statistics", "scheme": "http://delicious.com/ewand/", "label": null}, {"term": "betting", "scheme": "http://delicious.com/ewand/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 10:57:11 +0000", "links": [{"href": "http://www.asomo.net/index.jsp?id=10", "type": "text/html", "rel": "alternate"}], "title": "ASOMO", "author": "grmarquez", "comments": "http://delicious.com/url/af3705769f0d49c071aef6e8c5c2a10a", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "ASOMO"}, "link": "http://www.asomo.net/index.jsp?id=10", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/af3705769f0d49c071aef6e8c5c2a10a", "id": "http://delicious.com/url/af3705769f0d49c071aef6e8c5c2a10a#grmarquez", "tags": [{"term": "servicio", "scheme": "http://delicious.com/grmarquez/", "label": null}, {"term": "an\u00e1lisis", "scheme": "http://delicious.com/grmarquez/", "label": null}, {"term": "opini\u00f3n", "scheme": "http://delicious.com/grmarquez/", "label": null}, {"term": "movilizada", "scheme": "http://delicious.com/grmarquez/", "label": null}, {"term": "monitoreo", "scheme": "http://delicious.com/grmarquez/", "label": null}, {"term": "marcas", "scheme": "http://delicious.com/grmarquez/", "label": null}, {"term": "organizaciones", "scheme": "http://delicious.com/grmarquez/", "label": null}, {"term": "acontecimientos", "scheme": "http://delicious.com/grmarquez/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 21:59:58 +0000", "links": [{"href": "http://www.ctd.northwestern.edu/", "type": "text/html", "rel": "alternate"}], "title": "Center for Talent Development :: Welcome to Center for Talent Development", "author": "slyvermont", "comments": "http://delicious.com/url/0b90a4c44a5ca11459b83adc1daf1a7a", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Center for Talent Development :: Welcome to Center for Talent Development"}, "link": "http://www.ctd.northwestern.edu/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0b90a4c44a5ca11459b83adc1daf1a7a", "id": "http://delicious.com/url/0b90a4c44a5ca11459b83adc1daf1a7a#slyvermont", "tags": [{"term": "camps", "scheme": "http://delicious.com/slyvermont/", "label": null}, {"term": "gifteded", "scheme": "http://delicious.com/slyvermont/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 10:29:29 +0000", "links": [{"href": "http://blog.shelfari.com/my_weblog/2009/09/neil.html", "type": "text/html", "rel": "alternate"}], "title": "Shelfari: Neil Gaiman's Bookshelves", "author": "Krendalin", "comments": "http://delicious.com/url/784c34e9394565b8643421f61503e15c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Shelfari: Neil Gaiman's Bookshelves"}, "link": "http://blog.shelfari.com/my_weblog/2009/09/neil.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/784c34e9394565b8643421f61503e15c", "id": "http://delicious.com/url/784c34e9394565b8643421f61503e15c#Krendalin", "tags": [{"term": "books,", "scheme": "http://delicious.com/Krendalin/", "label": null}, {"term": "NeilGaiman", "scheme": "http://delicious.com/Krendalin/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 02:07:01 +0000", "links": [{"href": "http://www.seriouseats.com/recipes/2009/09/seriously-sick-food-for-when-youre-under-the-weather-recipes.html", "type": "text/html", "rel": "alternate"}], "title": "Seriously Sick: Food For When You're Under the Weather | Serious Eats : Recipes", "author": "kschlumpf", "comments": "http://delicious.com/url/8cd743be415172f2c72a7af687581811", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Seriously Sick: Food For When You're Under the Weather | Serious Eats : Recipes"}, "link": "http://www.seriouseats.com/recipes/2009/09/seriously-sick-food-for-when-youre-under-the-weather-recipes.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8cd743be415172f2c72a7af687581811", "id": "http://delicious.com/url/8cd743be415172f2c72a7af687581811#kschlumpf", "tags": [{"term": "recipes", "scheme": "http://delicious.com/kschlumpf/", "label": null}, {"term": "food", "scheme": "http://delicious.com/kschlumpf/", "label": null}, {"term": "recipe", "scheme": "http://delicious.com/kschlumpf/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 18:32:29 +0000", "links": [{"href": "http://firtree.org/", "type": "text/html", "rel": "alternate"}], "title": "FrontPage - Firtree", "author": "tmsh", "comments": "http://delicious.com/url/dff3bf58fe3c3226bd211dcd0d22eb4b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "FrontPage - Firtree"}, "link": "http://firtree.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/dff3bf58fe3c3226bd211dcd0d22eb4b", "id": "http://delicious.com/url/dff3bf58fe3c3226bd211dcd0d22eb4b#tmsh"}, +{"updated": "Mon, 07 Sep 2009 21:11:49 +0000", "links": [{"href": "http://bcs.bedfordstmartins.com/exercisecentral/default.asp", "type": "text/html", "rel": "alternate"}], "title": "Bedford/St. Martin's \u2014 Exercise Central", "author": "abecker1", "comments": "http://delicious.com/url/5c652e4908405f3c28e4aeacc0008f82", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Bedford/St. Martin's \u2014 Exercise Central"}, "link": "http://bcs.bedfordstmartins.com/exercisecentral/default.asp", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5c652e4908405f3c28e4aeacc0008f82", "id": "http://delicious.com/url/5c652e4908405f3c28e4aeacc0008f82#abecker1", "tags": [{"term": "comp", "scheme": "http://delicious.com/abecker1/", "label": null}, {"term": "grammar", "scheme": "http://delicious.com/abecker1/", "label": null}, {"term": "grade_9", "scheme": "http://delicious.com/abecker1/", "label": null}, {"term": "grade_10", "scheme": "http://delicious.com/abecker1/", "label": null}, {"term": "grade_11", "scheme": "http://delicious.com/abecker1/", "label": null}, {"term": "grade_12", "scheme": "http://delicious.com/abecker1/", "label": null}, {"term": "aplang", "scheme": "http://delicious.com/abecker1/", "label": null}, {"term": "honors", "scheme": "http://delicious.com/abecker1/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 10:27:25 +0000", "links": [{"href": "http://studios.thoughtworks.com/mingle-agile-project-management", "type": "text/html", "rel": "alternate"}], "title": "Mingle - Agile Project Management Tool | Adaptive ALM | Agile software development solutions | ThoughtWorks Studios", "author": "kesv", "comments": "http://delicious.com/url/eb2df6651a1abd9bbb2f241249b6d80c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Mingle - Agile Project Management Tool | Adaptive ALM | Agile software development solutions | ThoughtWorks Studios"}, "link": "http://studios.thoughtworks.com/mingle-agile-project-management", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/eb2df6651a1abd9bbb2f241249b6d80c", "id": "http://delicious.com/url/eb2df6651a1abd9bbb2f241249b6d80c#kesv", "tags": [{"term": "agile", "scheme": "http://delicious.com/kesv/", "label": null}, {"term": "tools", "scheme": "http://delicious.com/kesv/", "label": null}, {"term": "ProjectManagement", "scheme": "http://delicious.com/kesv/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 06:54:36 +0000", "links": [{"href": "http://www.quirksmode.org/js/events_order.html", "type": "text/html", "rel": "alternate"}], "title": "Javascript - Event order", "author": "allex_wang", "comments": "http://delicious.com/url/a90e5a0df79fca21cb3a03be83b360fc", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Javascript - Event order"}, "link": "http://www.quirksmode.org/js/events_order.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a90e5a0df79fca21cb3a03be83b360fc", "id": "http://delicious.com/url/a90e5a0df79fca21cb3a03be83b360fc#allex_wang", "tags": [{"term": "javascript", "scheme": "http://delicious.com/allex_wang/", "label": null}, {"term": "reference", "scheme": "http://delicious.com/allex_wang/", "label": null}, {"term": "events", "scheme": "http://delicious.com/allex_wang/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 11:04:54 +0000", "links": [{"href": "http://couchdb.apache.org/", "type": "text/html", "rel": "alternate"}], "title": "Apache CouchDB: The CouchDB Project", "author": "hannescarlmeyer", "comments": "http://delicious.com/url/a8b27a281f7bb411f05b2bce86ed6af3", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Apache CouchDB: The CouchDB Project"}, "link": "http://couchdb.apache.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a8b27a281f7bb411f05b2bce86ed6af3", "id": "http://delicious.com/url/a8b27a281f7bb411f05b2bce86ed6af3#hannescarlmeyer", "tags": [{"term": "hadoop", "scheme": "http://delicious.com/hannescarlmeyer/", "label": null}, {"term": "database", "scheme": "http://delicious.com/hannescarlmeyer/", "label": null}, {"term": "software", "scheme": "http://delicious.com/hannescarlmeyer/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 12:46:33 +0000", "links": [{"href": "http://www.google.com/coop/cse/", "type": "text/html", "rel": "alternate"}], "title": "Google Custom Search Engine - Site search and more", "author": "eculp", "comments": "http://delicious.com/url/3276cb4b3e57448721d375a9e12669a7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Google Custom Search Engine - Site search and more"}, "link": "http://www.google.com/coop/cse/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3276cb4b3e57448721d375a9e12669a7", "id": "http://delicious.com/url/3276cb4b3e57448721d375a9e12669a7#eculp"}, +{"updated": "Thu, 10 Sep 2009 02:14:33 +0000", "links": [{"href": "http://www.interactiondesign.com.au/", "type": "text/html", "rel": "alternate"}], "title": "ACID Home", "author": "amdconnor", "comments": "http://delicious.com/url/0253b164c08b0c80efaea1b85c66a088", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "ACID Home"}, "link": "http://www.interactiondesign.com.au/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0253b164c08b0c80efaea1b85c66a088", "id": "http://delicious.com/url/0253b164c08b0c80efaea1b85c66a088#amdconnor", "tags": [{"term": "interactive", "scheme": "http://delicious.com/amdconnor/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 21:08:53 +0000", "links": [{"href": "http://inspectelement.com/tutorials/how-to-design-buttons-to-help-improve-usability/", "type": "text/html", "rel": "alternate"}], "title": "How to Design Buttons to Help Improve Usability - Inspect Element", "author": "bb_mke", "comments": "http://delicious.com/url/fc9e73ff887a8d6b417bfa90d1ea919e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "How to Design Buttons to Help Improve Usability - Inspect Element"}, "link": "http://inspectelement.com/tutorials/how-to-design-buttons-to-help-improve-usability/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/fc9e73ff887a8d6b417bfa90d1ea919e", "id": "http://delicious.com/url/fc9e73ff887a8d6b417bfa90d1ea919e#bb_mke", "tags": [{"term": "webdesign", "scheme": "http://delicious.com/bb_mke/", "label": null}, {"term": "ux", "scheme": "http://delicious.com/bb_mke/", "label": null}, {"term": "usability", "scheme": "http://delicious.com/bb_mke/", "label": null}, {"term": "buttons", "scheme": "http://delicious.com/bb_mke/", "label": null}, {"term": "bestpractices", "scheme": "http://delicious.com/bb_mke/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 14:23:38 +0000", "links": [{"href": "http://yeahyeahstudio.com/", "type": "text/html", "rel": "alternate"}], "title": "Yeah Yeah Studio", "author": "plaisant", "comments": "http://delicious.com/url/dd33719b61302842d8ab89c5a003338f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Yeah Yeah Studio"}, "link": "http://yeahyeahstudio.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/dd33719b61302842d8ab89c5a003338f", "id": "http://delicious.com/url/dd33719b61302842d8ab89c5a003338f#plaisant", "tags": [{"term": "art", "scheme": "http://delicious.com/plaisant/", "label": null}, {"term": "ilustra\u00e7\u00e3o", "scheme": "http://delicious.com/plaisant/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 07:19:11 +0000", "links": [{"href": "http://www.treehugger.com/files/2008/05/growing-power-urban-aquaponics.php", "type": "text/html", "rel": "alternate"}], "title": "Growing Power: Urban Aquaponics, Vermiculture and Sustainable Agriculture : TreeHugger", "author": "thadkrgr", "comments": "http://delicious.com/url/02ba80de2daca1311ac65e6cd8239094", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Growing Power: Urban Aquaponics, Vermiculture and Sustainable Agriculture : TreeHugger"}, "link": "http://www.treehugger.com/files/2008/05/growing-power-urban-aquaponics.php", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/02ba80de2daca1311ac65e6cd8239094", "id": "http://delicious.com/url/02ba80de2daca1311ac65e6cd8239094#thadkrgr", "tags": [{"term": "farming", "scheme": "http://delicious.com/thadkrgr/", "label": null}, {"term": "urban", "scheme": "http://delicious.com/thadkrgr/", "label": null}, {"term": "aquaponics", "scheme": "http://delicious.com/thadkrgr/", "label": null}, {"term": "gardening", "scheme": "http://delicious.com/thadkrgr/", "label": null}, {"term": "sustainability", "scheme": "http://delicious.com/thadkrgr/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 20:21:08 +0000", "links": [{"href": "http://www.thinkgeek.com/", "type": "text/html", "rel": "alternate"}], "title": "ThinkGeek :: Stuff for Smart Masses", "author": "jlefeber", "comments": "http://delicious.com/url/43287c7581808ada2134228e59af4347", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "ThinkGeek :: Stuff for Smart Masses"}, "link": "http://www.thinkgeek.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/43287c7581808ada2134228e59af4347", "id": "http://delicious.com/url/43287c7581808ada2134228e59af4347#jlefeber", "tags": [{"term": "technology", "scheme": "http://delicious.com/jlefeber/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 09:07:59 +0000", "links": [{"href": "http://www.youserials.com/", "type": "text/html", "rel": "alternate"}], "title": "YouSerials.com - The KEY to Serial Numbers!", "author": "uditjain", "comments": "http://delicious.com/url/1b02ef3dee24ca44d5d4b178fd9cc341", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "YouSerials.com - The KEY to Serial Numbers!"}, "link": "http://www.youserials.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1b02ef3dee24ca44d5d4b178fd9cc341", "id": "http://delicious.com/url/1b02ef3dee24ca44d5d4b178fd9cc341#uditjain", "tags": [{"term": "serial", "scheme": "http://delicious.com/uditjain/", "label": null}, {"term": "key", "scheme": "http://delicious.com/uditjain/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 13:12:59 +0000", "links": [{"href": "http://adifference.blogspot.com/2009/05/ten-commandments.html", "type": "text/html", "rel": "alternate"}], "title": "A Difference: The Ten Commandments", "author": "schoolspodcasts", "comments": "http://delicious.com/url/0d4a38b07771c092cc2269a68dcd733b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "A Difference: The Ten Commandments"}, "link": "http://adifference.blogspot.com/2009/05/ten-commandments.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0d4a38b07771c092cc2269a68dcd733b", "id": "http://delicious.com/url/0d4a38b07771c092cc2269a68dcd733b#schoolspodcasts", "tags": [{"term": "teaching", "scheme": "http://delicious.com/schoolspodcasts/", "label": null}, {"term": "commandments", "scheme": "http://delicious.com/schoolspodcasts/", "label": null}, {"term": "rules", "scheme": "http://delicious.com/schoolspodcasts/", "label": null}, {"term": "10", "scheme": "http://delicious.com/schoolspodcasts/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 03:28:19 +0000", "links": [{"href": "http://www.programmingforums.org/thread7219.html", "type": "text/html", "rel": "alternate"}], "title": "[tutorial] Simple G++ compiler tutorial - C++", "author": "verre1", "comments": "http://delicious.com/url/d03dfb5c00d5e8cbe1fdc43354fdde64", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "[tutorial] Simple G++ compiler tutorial - C++"}, "link": "http://www.programmingforums.org/thread7219.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d03dfb5c00d5e8cbe1fdc43354fdde64", "id": "http://delicious.com/url/d03dfb5c00d5e8cbe1fdc43354fdde64#verre1"}, +{"updated": "Tue, 08 Sep 2009 08:30:38 +0000", "links": [{"href": "http://www.dealextreme.com/", "type": "text/html", "rel": "alternate"}], "title": "DealExtreme: Cool Gadgets at the Right Price - Site-Wide Free Shipping (Page 1)", "author": "avner_linder", "comments": "http://delicious.com/url/5d65c42b60292571aeac5499ce2cca41", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "DealExtreme: Cool Gadgets at the Right Price - Site-Wide Free Shipping (Page 1)"}, "link": "http://www.dealextreme.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5d65c42b60292571aeac5499ce2cca41", "id": "http://delicious.com/url/5d65c42b60292571aeac5499ce2cca41#avner_linder", "tags": [{"term": "Shopping", "scheme": "http://delicious.com/avner_linder/", "label": null}, {"term": "gadgets", "scheme": "http://delicious.com/avner_linder/", "label": null}, {"term": "shop", "scheme": "http://delicious.com/avner_linder/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 10:29:49 +0000", "links": [{"href": "http://animoto.com/", "type": "text/html", "rel": "alternate"}], "title": "animoto - the end of slideshows", "author": "BobK99", "comments": "http://delicious.com/url/6ebd210774922d50c5255278c73a3653", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "animoto - the end of slideshows"}, "link": "http://animoto.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6ebd210774922d50c5255278c73a3653", "id": "http://delicious.com/url/6ebd210774922d50c5255278c73a3653#BobK99", "tags": [{"term": "utilities", "scheme": "http://delicious.com/BobK99/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 16:01:35 +0000", "links": [{"href": "http://www.informit.com/articles/article.aspx?p=1390173", "type": "text/html", "rel": "alternate"}], "title": "InformIT: How Not To Optimize > A Load of Shift", "author": "william1104", "comments": "http://delicious.com/url/b7a08abcff402bf4a9faedd32e09fb92", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "InformIT: How Not To Optimize > A Load of Shift"}, "link": "http://www.informit.com/articles/article.aspx?p=1390173", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b7a08abcff402bf4a9faedd32e09fb92", "id": "http://delicious.com/url/b7a08abcff402bf4a9faedd32e09fb92#william1104", "tags": [{"term": "Technology", "scheme": "http://delicious.com/william1104/", "label": null}, {"term": "Optimization", "scheme": "http://delicious.com/william1104/", "label": null}, {"term": "Learn", "scheme": "http://delicious.com/william1104/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 10:36:58 +0000", "links": [{"href": "http://www.tinygrab.com/", "type": "text/html", "rel": "alternate"}], "title": "Simple. Screenshot. Sharing. - TinyGrab", "author": "mojaam", "comments": "http://delicious.com/url/025658b1dd618c60e2ef41561ea498f8", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Simple. Screenshot. Sharing. - TinyGrab"}, "link": "http://www.tinygrab.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/025658b1dd618c60e2ef41561ea498f8", "id": "http://delicious.com/url/025658b1dd618c60e2ef41561ea498f8#mojaam", "tags": [{"term": "tinygrab", "scheme": "http://delicious.com/mojaam/", "label": null}, {"term": "mac", "scheme": "http://delicious.com/mojaam/", "label": null}, {"term": "screencapture", "scheme": "http://delicious.com/mojaam/", "label": null}, {"term": "screenshots", "scheme": "http://delicious.com/mojaam/", "label": null}, {"term": "printscreen", "scheme": "http://delicious.com/mojaam/", "label": null}, {"term": "prtscr", "scheme": "http://delicious.com/mojaam/", "label": null}, {"term": "screengrab", "scheme": "http://delicious.com/mojaam/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 04:07:20 +0000", "links": [{"href": "http://strangecreature.livejournal.com/319765.html", "type": "text/html", "rel": "alternate"}], "title": "Human writes - Fic: The League of Extraordinary Bottle-Dodgers", "author": "sexymuffin194", "comments": "http://delicious.com/url/c58f5783e9e9db5b70666290af0987d6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Human writes - Fic: The League of Extraordinary Bottle-Dodgers"}, "link": "http://strangecreature.livejournal.com/319765.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c58f5783e9e9db5b70666290af0987d6", "id": "http://delicious.com/url/c58f5783e9e9db5b70666290af0987d6#sexymuffin194", "tags": [{"term": "Pete/Mikey", "scheme": "http://delicious.com/sexymuffin194/", "label": null}, {"term": "SummerOfLike", "scheme": "http://delicious.com/sexymuffin194/", "label": null}, {"term": "genderfuck", "scheme": "http://delicious.com/sexymuffin194/", "label": null}, {"term": "FallOutBoy", "scheme": "http://delicious.com/sexymuffin194/", "label": null}, {"term": "MyChemicalRomance", "scheme": "http://delicious.com/sexymuffin194/", "label": null}, {"term": "fic", "scheme": "http://delicious.com/sexymuffin194/", "label": null}, {"term": "AU", "scheme": "http://delicious.com/sexymuffin194/", "label": null}, {"term": "BleedsLikeABoy", "scheme": "http://delicious.com/sexymuffin194/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 09:28:54 +0000", "links": [{"href": "http://www.youtube.com/watch?v=7vXBY6c_oBE", "type": "text/html", "rel": "alternate"}], "title": "YouTube - Fire Alarm Jam Session", "author": "timecrawler", "comments": "http://delicious.com/url/8bacbe90e9cabc0ea67f73def07624e3", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "YouTube - Fire Alarm Jam Session"}, "link": "http://www.youtube.com/watch?v=7vXBY6c_oBE", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8bacbe90e9cabc0ea67f73def07624e3", "id": "http://delicious.com/url/8bacbe90e9cabc0ea67f73def07624e3#timecrawler", "tags": [{"term": "music", "scheme": "http://delicious.com/timecrawler/", "label": null}, {"term": "youtube", "scheme": "http://delicious.com/timecrawler/", "label": null}, {"term": "video", "scheme": "http://delicious.com/timecrawler/", "label": null}, {"term": "band", "scheme": "http://delicious.com/timecrawler/", "label": null}, {"term": "firealarm", "scheme": "http://delicious.com/timecrawler/", "label": null}, {"term": "alarm", "scheme": "http://delicious.com/timecrawler/", "label": null}, {"term": "fire", "scheme": "http://delicious.com/timecrawler/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 14:59:41 +0000", "links": [{"href": "http://www.xmarks.com/", "type": "text/html", "rel": "alternate"}], "title": "Bookmark Sync and Search | Xmarks", "author": "smass", "comments": "http://delicious.com/url/f0f4ce7435043ebf6471179dfce84249", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Bookmark Sync and Search | Xmarks"}, "link": "http://www.xmarks.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f0f4ce7435043ebf6471179dfce84249", "id": "http://delicious.com/url/f0f4ce7435043ebf6471179dfce84249#smass", "tags": [{"term": "search", "scheme": "http://delicious.com/smass/", "label": null}, {"term": "Tools", "scheme": "http://delicious.com/smass/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 17:30:37 +0000", "links": [{"href": "http://vandelaydesign.com/blog/galleries/inspiration/", "type": "text/html", "rel": "alternate"}], "title": "50 Websites (and More) for Your Design Inspiration | Vandelay Design Blog", "author": "niquefz", "comments": "http://delicious.com/url/c168e4e3431436539a26a60943ad35f1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "50 Websites (and More) for Your Design Inspiration | Vandelay Design Blog"}, "link": "http://vandelaydesign.com/blog/galleries/inspiration/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c168e4e3431436539a26a60943ad35f1", "id": "http://delicious.com/url/c168e4e3431436539a26a60943ad35f1#niquefz", "tags": [{"term": "website", "scheme": "http://delicious.com/niquefz/", "label": null}, {"term": "inspiration", "scheme": "http://delicious.com/niquefz/", "label": null}, {"term": "showcase", "scheme": "http://delicious.com/niquefz/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 05:40:47 +0000", "links": [{"href": "http://www.castorama.fr/store/html/homepage/home-page.jsp", "type": "text/html", "rel": "alternate"}], "title": "Castorama.fr - Boutique en ligne de bricolage, jardinage et d\u00e9coration - Castorama", "author": "alexshafty", "comments": "http://delicious.com/url/06fd88951945e75fb51ab5266f4c45c5", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Castorama.fr - Boutique en ligne de bricolage, jardinage et d\u00e9coration - Castorama"}, "link": "http://www.castorama.fr/store/html/homepage/home-page.jsp", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/06fd88951945e75fb51ab5266f4c45c5", "id": "http://delicious.com/url/06fd88951945e75fb51ab5266f4c45c5#alexshafty", "tags": [{"term": "bricolage", "scheme": "http://delicious.com/alexshafty/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 14:20:58 +0000", "links": [{"href": "http://sheafrotherdon.livejournal.com/65119.html", "type": "text/html", "rel": "alternate"}], "title": "Polaroids", "author": "monsieurdyn", "comments": "http://delicious.com/url/eaf11ae8a372f01bd4f708e5c7717751", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Polaroids"}, "link": "http://sheafrotherdon.livejournal.com/65119.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/eaf11ae8a372f01bd4f708e5c7717751", "id": "http://delicious.com/url/eaf11ae8a372f01bd4f708e5c7717751#monsieurdyn", "tags": [{"term": "slash", "scheme": "http://delicious.com/monsieurdyn/", "label": null}, {"term": "harrypotter", "scheme": "http://delicious.com/monsieurdyn/", "label": null}, {"term": "marauders", "scheme": "http://delicious.com/monsieurdyn/", "label": null}, {"term": "sirius", "scheme": "http://delicious.com/monsieurdyn/", "label": null}, {"term": "remus", "scheme": "http://delicious.com/monsieurdyn/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 20:54:13 +0000", "links": [{"href": "http://designdisease.com/preview/evidens-white", "type": "text/html", "rel": "alternate"}], "title": "DesignDisease - Preview Themes", "author": "damnadm", "comments": "http://delicious.com/url/d6b1988515bcbe903cbcbd84cd6b4711", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "DesignDisease - Preview Themes"}, "link": "http://designdisease.com/preview/evidens-white", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d6b1988515bcbe903cbcbd84cd6b4711", "id": "http://delicious.com/url/d6b1988515bcbe903cbcbd84cd6b4711#damnadm", "tags": [{"term": "wordpress", "scheme": "http://delicious.com/damnadm/", "label": null}, {"term": "themes", "scheme": "http://delicious.com/damnadm/", "label": null}, {"term": "free", "scheme": "http://delicious.com/damnadm/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 14:55:23 +0000", "links": [{"href": "http://delicious.com/help/bookmarklets", "type": "text/html", "rel": "alternate"}], "title": "Install Bookmarklets on Delicious", "author": "sunnysomchok", "comments": "http://delicious.com/url/e7e9bab23312c70e8373b92927facb56", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Install Bookmarklets on Delicious"}, "link": "http://delicious.com/help/bookmarklets", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e7e9bab23312c70e8373b92927facb56", "id": "http://delicious.com/url/e7e9bab23312c70e8373b92927facb56#sunnysomchok"}, +{"updated": "Thu, 10 Sep 2009 03:53:30 +0000", "links": [{"href": "http://www.ehow.com/how_2160460_custom-iphone-ringtones-free.html", "type": "text/html", "rel": "alternate"}], "title": "How to Make Custom iPhone Ringtones for Free | eHow.com", "author": "hamstersphere", "comments": "http://delicious.com/url/745b904790f8af46fba433a77413b15b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "How to Make Custom iPhone Ringtones for Free | eHow.com"}, "link": "http://www.ehow.com/how_2160460_custom-iphone-ringtones-free.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/745b904790f8af46fba433a77413b15b", "id": "http://delicious.com/url/745b904790f8af46fba433a77413b15b#hamstersphere", "tags": [{"term": "iphone", "scheme": "http://delicious.com/hamstersphere/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 10:26:45 +0000", "links": [{"href": "http://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%BC%E3%82%AB%E3%83%AB%E4%BA%8B%E4%BB%B6", "type": "text/html", "rel": "alternate"}], "title": "\u30bd\u30fc\u30ab\u30eb\u4e8b\u4ef6 - Wikipedia", "author": "hirokick", "comments": "http://delicious.com/url/f062d002b3c7d329875a46d5b7bb74b4", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u30bd\u30fc\u30ab\u30eb\u4e8b\u4ef6 - Wikipedia"}, "link": "http://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%BC%E3%82%AB%E3%83%AB%E4%BA%8B%E4%BB%B6", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f062d002b3c7d329875a46d5b7bb74b4", "id": "http://delicious.com/url/f062d002b3c7d329875a46d5b7bb74b4#hirokick"}, +{"updated": "Mon, 07 Sep 2009 17:12:03 +0000", "links": [{"href": "http://helloagainvintage.blogspot.com/", "type": "text/html", "rel": "alternate"}], "title": "HELLO AGAIN VINTAGE", "author": "lelea997", "comments": "http://delicious.com/url/e7d7cfb441a11f645cffd88a7450bd05", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "HELLO AGAIN VINTAGE"}, "link": "http://helloagainvintage.blogspot.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e7d7cfb441a11f645cffd88a7450bd05", "id": "http://delicious.com/url/e7d7cfb441a11f645cffd88a7450bd05#lelea997"}, +{"updated": "Wed, 09 Sep 2009 15:07:05 +0000", "links": [{"href": "http://www.robots.ox.ac.uk/ActiveVision/", "type": "text/html", "rel": "alternate"}], "title": "Active Vision Group, Department of Engineering Science, Oxford", "author": "laubersder", "comments": "http://delicious.com/url/86848e4bf42ccfd6067cd8c9cb9028ef", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Active Vision Group, Department of Engineering Science, Oxford"}, "link": "http://www.robots.ox.ac.uk/ActiveVision/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/86848e4bf42ccfd6067cd8c9cb9028ef", "id": "http://delicious.com/url/86848e4bf42ccfd6067cd8c9cb9028ef#laubersder", "tags": [{"term": "ai", "scheme": "http://delicious.com/laubersder/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 09:15:29 +0000", "links": [{"href": "http://suggest.name/", "type": "text/html", "rel": "alternate"}], "title": "Suggest.Name - The Web 2.0 Name Generator", "author": "LENAH82", "comments": "http://delicious.com/url/ea3277ecf3ac9edae64e23db70febdf5", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Suggest.Name - The Web 2.0 Name Generator"}, "link": "http://suggest.name/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ea3277ecf3ac9edae64e23db70febdf5", "id": "http://delicious.com/url/ea3277ecf3ac9edae64e23db70febdf5#LENAH82", "tags": [{"term": "web2.0", "scheme": "http://delicious.com/LENAH82/", "label": null}, {"term": "generator", "scheme": "http://delicious.com/LENAH82/", "label": null}, {"term": "domain", "scheme": "http://delicious.com/LENAH82/", "label": null}, {"term": "name", "scheme": "http://delicious.com/LENAH82/", "label": null}, {"term": "tools", "scheme": "http://delicious.com/LENAH82/", "label": null}, {"term": "branding", "scheme": "http://delicious.com/LENAH82/", "label": null}, {"term": "naming", "scheme": "http://delicious.com/LENAH82/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 06:32:43 +0000", "links": [{"href": "http://code.google.com/p/java-twitter/", "type": "text/html", "rel": "alternate"}], "title": "java-twitter - Project Hosting on Google Code", "author": "morxs", "comments": "http://delicious.com/url/3fbe4bf530af26b3572ebdfeca0c6fe3", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "java-twitter - Project Hosting on Google Code"}, "link": "http://code.google.com/p/java-twitter/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3fbe4bf530af26b3572ebdfeca0c6fe3", "id": "http://delicious.com/url/3fbe4bf530af26b3572ebdfeca0c6fe3#morxs", "tags": [{"term": "java", "scheme": "http://delicious.com/morxs/", "label": null}, {"term": "api", "scheme": "http://delicious.com/morxs/", "label": null}, {"term": "twitter", "scheme": "http://delicious.com/morxs/", "label": null}, {"term": "socialnetworking", "scheme": "http://delicious.com/morxs/", "label": null}, {"term": "programming", "scheme": "http://delicious.com/morxs/", "label": null}, {"term": "library", "scheme": "http://delicious.com/morxs/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 11:58:02 +0000", "links": [{"href": "https://intranet.hec.fr/", "type": "text/html", "rel": "alternate"}], "title": "HEC Paris - Intranet", "author": "henderson.gavin", "comments": "http://delicious.com/url/1d91fbd80b62221f8ec60b1cc4d475c6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "HEC Paris - Intranet"}, "link": "https://intranet.hec.fr/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1d91fbd80b62221f8ec60b1cc4d475c6", "id": "http://delicious.com/url/1d91fbd80b62221f8ec60b1cc4d475c6#henderson.gavin", "tags": [{"term": "01Freq", "scheme": "http://delicious.com/henderson.gavin/", "label": null}, {"term": "01Freq_6HEC", "scheme": "http://delicious.com/henderson.gavin/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 08:16:25 +0000", "links": [{"href": "http://www.creatiu.com/a/cool-sites/", "type": "text/html", "rel": "alternate"}], "title": "Cool Sites | CREATIU", "author": "anglv", "comments": "http://delicious.com/url/9491ce5b91ec97cbcbedabef811113fe", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Cool Sites | CREATIU"}, "link": "http://www.creatiu.com/a/cool-sites/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9491ce5b91ec97cbcbedabef811113fe", "id": "http://delicious.com/url/9491ce5b91ec97cbcbedabef811113fe#anglv", "tags": [{"term": "inspiracion-web", "scheme": "http://delicious.com/anglv/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 02:15:32 +0000", "links": [{"href": "http://forobeta.com/", "type": "text/html", "rel": "alternate"}], "title": "ForoBeta - Blogs, SEO, Dise\u00f1o: Compra, Venta e Intercambio de enlaces", "author": "jjzavaleta", "comments": "http://delicious.com/url/ec86ecc75e4c62bb248b3f4ad7f17735", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "ForoBeta - Blogs, SEO, Dise\u00f1o: Compra, Venta e Intercambio de enlaces"}, "link": "http://forobeta.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ec86ecc75e4c62bb248b3f4ad7f17735", "id": "http://delicious.com/url/ec86ecc75e4c62bb248b3f4ad7f17735#jjzavaleta", "tags": [{"term": "foro", "scheme": "http://delicious.com/jjzavaleta/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 19:38:57 +0000", "links": [{"href": "http://btemplates.com/2009/07/18/mahusay/", "type": "text/html", "rel": "alternate"}], "title": "Blogger Template Mahusay | BTemplates", "author": "josebuleo", "comments": "http://delicious.com/url/c884c3dba4c6da382101f861aa9cef74", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Blogger Template Mahusay | BTemplates"}, "link": "http://btemplates.com/2009/07/18/mahusay/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c884c3dba4c6da382101f861aa9cef74", "id": "http://delicious.com/url/c884c3dba4c6da382101f861aa9cef74#josebuleo", "tags": [{"term": "blogger", "scheme": "http://delicious.com/josebuleo/", "label": null}, {"term": "template", "scheme": "http://delicious.com/josebuleo/", "label": null}, {"term": "templates", "scheme": "http://delicious.com/josebuleo/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 20:43:51 +0000", "links": [{"href": "http://www.rubenswieringa.com/blog/eventbubbles-eventcancelable-and-eventcurrenttarget", "type": "text/html", "rel": "alternate"}], "title": "Event.bubbles, Event.cancelable, and Event.currentTarget : Ruben\u2019s blog", "author": "przygoda", "comments": "http://delicious.com/url/b47a573c81d9931e094043f9a366192b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Event.bubbles, Event.cancelable, and Event.currentTarget : Ruben\u2019s blog"}, "link": "http://www.rubenswieringa.com/blog/eventbubbles-eventcancelable-and-eventcurrenttarget", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b47a573c81d9931e094043f9a366192b", "id": "http://delicious.com/url/b47a573c81d9931e094043f9a366192b#przygoda", "tags": [{"term": "as3", "scheme": "http://delicious.com/przygoda/", "label": null}, {"term": "tutorial", "scheme": "http://delicious.com/przygoda/", "label": null}, {"term": "events", "scheme": "http://delicious.com/przygoda/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 00:17:15 +0000", "links": [{"href": "http://www.mozilla.org/unix/customizing.html", "type": "text/html", "rel": "alternate"}], "title": "Customizing Mozilla", "author": "maxzda", "comments": "http://delicious.com/url/b2bdcc42d29bad50160b8343252a953c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Customizing Mozilla"}, "link": "http://www.mozilla.org/unix/customizing.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b2bdcc42d29bad50160b8343252a953c", "id": "http://delicious.com/url/b2bdcc42d29bad50160b8343252a953c#maxzda", "tags": [{"term": "firefox", "scheme": "http://delicious.com/maxzda/", "label": null}, {"term": "css", "scheme": "http://delicious.com/maxzda/", "label": null}, {"term": "browser", "scheme": "http://delicious.com/maxzda/", "label": null}, {"term": "tips", "scheme": "http://delicious.com/maxzda/", "label": null}, {"term": "customize", "scheme": "http://delicious.com/maxzda/", "label": null}, {"term": "mozilla", "scheme": "http://delicious.com/maxzda/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 14:13:38 +0000", "links": [{"href": "http://www.tbray.org/ongoing/When/200x/2009/09/02/Ravelry", "type": "text/html", "rel": "alternate"}], "title": "ongoing \u00b7 Ravelry", "author": "aaron.g76", "comments": "http://delicious.com/url/e5ece9ef9d4c28aa8c7848d7338dad0a", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "ongoing \u00b7 Ravelry"}, "link": "http://www.tbray.org/ongoing/When/200x/2009/09/02/Ravelry", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e5ece9ef9d4c28aa8c7848d7338dad0a", "id": "http://delicious.com/url/e5ece9ef9d4c28aa8c7848d7338dad0a#aaron.g76", "tags": [{"term": "rails", "scheme": "http://delicious.com/aaron.g76/", "label": null}, {"term": "ruby", "scheme": "http://delicious.com/aaron.g76/", "label": null}, {"term": "scaling", "scheme": "http://delicious.com/aaron.g76/", "label": null}, {"term": "haproxy", "scheme": "http://delicious.com/aaron.g76/", "label": null}, {"term": "tokyocabinet", "scheme": "http://delicious.com/aaron.g76/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 02:01:17 +0000", "links": [{"href": "http://spreadsheets.google.com/ccc?key=pM_ZE5BS0SgqDabnhl8Z2aA", "type": "text/html", "rel": "alternate"}], "title": "Educators on Twitter", "author": "kgustin", "comments": "http://delicious.com/url/1dda6f79974e95022f632117419786e7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Educators on Twitter"}, "link": "http://spreadsheets.google.com/ccc?key=pM_ZE5BS0SgqDabnhl8Z2aA", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1dda6f79974e95022f632117419786e7", "id": "http://delicious.com/url/1dda6f79974e95022f632117419786e7#kgustin", "tags": [{"term": "twitter", "scheme": "http://delicious.com/kgustin/", "label": null}, {"term": "education", "scheme": "http://delicious.com/kgustin/", "label": null}, {"term": "collaboration", "scheme": "http://delicious.com/kgustin/", "label": null}, {"term": "Google", "scheme": "http://delicious.com/kgustin/", "label": null}, {"term": "web2.0", "scheme": "http://delicious.com/kgustin/", "label": null}, {"term": "googledocs", "scheme": "http://delicious.com/kgustin/", "label": null}, {"term": "teachers", "scheme": "http://delicious.com/kgustin/", "label": null}, {"term": "twitterfodder", "scheme": "http://delicious.com/kgustin/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 14:14:36 +0000", "links": [{"href": "http://m.www.yahoo.com/", "type": "text/html", "rel": "alternate"}], "title": "Yahoo!", "author": "awiedner", "comments": "http://delicious.com/url/61b668b12719db662c51b02cdc23abb0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Yahoo!"}, "link": "http://m.www.yahoo.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/61b668b12719db662c51b02cdc23abb0", "id": "http://delicious.com/url/61b668b12719db662c51b02cdc23abb0#awiedner", "tags": [{"term": "computer_-_web_related", "scheme": "http://delicious.com/awiedner/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 01:54:41 +0000", "links": [{"href": "http://www.statcounter.com/", "type": "text/html", "rel": "alternate"}], "title": "StatCounter Free invisible Web tracker, Hit counter and Web stats", "author": "alejandro.sanchez", "comments": "http://delicious.com/url/62179d3ef1eb1613091505c44e3b6af8", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "StatCounter Free invisible Web tracker, Hit counter and Web stats"}, "link": "http://www.statcounter.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/62179d3ef1eb1613091505c44e3b6af8", "id": "http://delicious.com/url/62179d3ef1eb1613091505c44e3b6af8#alejandro.sanchez", "tags": [{"term": "webmaster", "scheme": "http://delicious.com/alejandro.sanchez/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 13:57:41 +0000", "links": [{"href": "http://www.sithowyouwant.com/", "type": "text/html", "rel": "alternate"}], "title": "Sit How You Want", "author": "freegorifero", "comments": "http://delicious.com/url/6ee0a6c8ebdbda5e756afe0e16a3641e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Sit How You Want"}, "link": "http://www.sithowyouwant.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6ee0a6c8ebdbda5e756afe0e16a3641e", "id": "http://delicious.com/url/6ee0a6c8ebdbda5e756afe0e16a3641e#freegorifero", "tags": [{"term": "Furniture", "scheme": "http://delicious.com/freegorifero/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 22:17:44 +0000", "links": [{"href": "http://blogs.techrepublic.com.com/programming-and-development/?p=673", "type": "text/html", "rel": "alternate"}], "title": "Why it's impossible to become a programming expert | Programming and Development \t\t\t| TechRepublic.com", "author": "bmacauley", "comments": "http://delicious.com/url/97186fb53c86b7e4c70eb0114bfd89c5", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Why it's impossible to become a programming expert | Programming and Development \t\t\t| TechRepublic.com"}, "link": "http://blogs.techrepublic.com.com/programming-and-development/?p=673", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/97186fb53c86b7e4c70eb0114bfd89c5", "id": "http://delicious.com/url/97186fb53c86b7e4c70eb0114bfd89c5#bmacauley", "tags": [{"term": "programming", "scheme": "http://delicious.com/bmacauley/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 20:25:51 +0000", "links": [{"href": "http://googleappengine.blogspot.com/2009/09/app-engine-sdk-125-released-for-python.html", "type": "text/html", "rel": "alternate"}], "title": "Google App Engine Blog: App Engine SDK 1.2.5 released for Python and Java, now with XMPP support", "author": "delicieux_twitter", "comments": "http://delicious.com/url/413a0207cfdf727b22c4df6734b2e015", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Google App Engine Blog: App Engine SDK 1.2.5 released for Python and Java, now with XMPP support"}, "link": "http://googleappengine.blogspot.com/2009/09/app-engine-sdk-125-released-for-python.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/413a0207cfdf727b22c4df6734b2e015", "id": "http://delicious.com/url/413a0207cfdf727b22c4df6734b2e015#delicieux_twitter", "tags": [{"term": "via:twitter", "scheme": "http://delicious.com/delicieux_twitter/", "label": null}, {"term": "imported_by:gourmand", "scheme": "http://delicious.com/delicieux_twitter/", "label": null}, {"term": "from:afternoon", "scheme": "http://delicious.com/delicieux_twitter/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 01:45:46 +0000", "links": [{"href": "http://www.erights.org/", "type": "text/html", "rel": "alternate"}], "title": "Welcome to ERights.Org", "author": "SunOf27", "comments": "http://delicious.com/url/a4f31cbaee8bbf6528d5e516433a5130", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Welcome to ERights.Org"}, "link": "http://www.erights.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a4f31cbaee8bbf6528d5e516433a5130", "id": "http://delicious.com/url/a4f31cbaee8bbf6528d5e516433a5130#SunOf27"}, +{"updated": "Tue, 08 Sep 2009 14:26:21 +0000", "links": [{"href": "http://developer.cybozu.co.jp/akky/2009/08/post-3665.html", "type": "text/html", "rel": "alternate"}], "title": "\u79cb\u5143@\u30b5\u30a4\u30dc\u30a6\u30ba\u30e9\u30dc\u30fb\u30d7\u30ed\u30b0\u30e9\u30de\u30fc\u30fb\u30d6\u30ed\u30b0 : \u30ad\u30e3\u30d1\u30b7\u30c6\u30a3\u30d7\u30e9\u30f3\u30cb\u30f3\u30b0", "author": "s9727", "comments": "http://delicious.com/url/23df581968d2ab482d014f64c939229c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u79cb\u5143@\u30b5\u30a4\u30dc\u30a6\u30ba\u30e9\u30dc\u30fb\u30d7\u30ed\u30b0\u30e9\u30de\u30fc\u30fb\u30d6\u30ed\u30b0 : \u30ad\u30e3\u30d1\u30b7\u30c6\u30a3\u30d7\u30e9\u30f3\u30cb\u30f3\u30b0"}, "link": "http://developer.cybozu.co.jp/akky/2009/08/post-3665.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/23df581968d2ab482d014f64c939229c", "id": "http://delicious.com/url/23df581968d2ab482d014f64c939229c#s9727", "tags": [{"term": "book", "scheme": "http://delicious.com/s9727/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 04:00:10 +0000", "links": [{"href": "http://www.onextrapixel.com/2009/09/08/29-free-stunning-web-icons-sets-to-enhance-your-web-design/", "type": "text/html", "rel": "alternate"}], "title": "29 Free Stunning Web Icons Sets To Enhance Your Web Design", "author": "estaticdeath", "comments": "http://delicious.com/url/880c42c8e22f388353a5654d99798353", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "29 Free Stunning Web Icons Sets To Enhance Your Web Design"}, "link": "http://www.onextrapixel.com/2009/09/08/29-free-stunning-web-icons-sets-to-enhance-your-web-design/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/880c42c8e22f388353a5654d99798353", "id": "http://delicious.com/url/880c42c8e22f388353a5654d99798353#estaticdeath", "tags": [{"term": "icons", "scheme": "http://delicious.com/estaticdeath/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 00:55:42 +0000", "links": [{"href": "http://www.ala.org/ala/mgrps/divs/aasl/guidelinesandstandards/learningstandards/standards.cfm", "type": "text/html", "rel": "alternate"}], "title": "ALA | AASL Standards for the 21st-Century Learner", "author": "eheastman", "comments": "http://delicious.com/url/2098f5cbe222c551801506118affa4cd", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "ALA | AASL Standards for the 21st-Century Learner"}, "link": "http://www.ala.org/ala/mgrps/divs/aasl/guidelinesandstandards/learningstandards/standards.cfm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2098f5cbe222c551801506118affa4cd", "id": "http://delicious.com/url/2098f5cbe222c551801506118affa4cd#eheastman", "tags": [{"term": "CAREY", "scheme": "http://delicious.com/eheastman/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 20:34:37 +0000", "links": [{"href": "http://www.tubechop.com/", "type": "text/html", "rel": "alternate"}], "title": "TubeChop - Chop YouTube Videos", "author": "garymilton88", "comments": "http://delicious.com/url/5bd3c2dee3ec19c75858131bafc285c8", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "TubeChop - Chop YouTube Videos"}, "link": "http://www.tubechop.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5bd3c2dee3ec19c75858131bafc285c8", "id": "http://delicious.com/url/5bd3c2dee3ec19c75858131bafc285c8#garymilton88", "tags": [{"term": "youtube", "scheme": "http://delicious.com/garymilton88/", "label": null}, {"term": "edit", "scheme": "http://delicious.com/garymilton88/", "label": null}, {"term": "video", "scheme": "http://delicious.com/garymilton88/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 03:06:49 +0000", "links": [{"href": "http://www.autoinsurance.org/driving_test/", "type": "text/html", "rel": "alternate"}], "title": "Could You Pass a Driving Test? From Auto Insurance.org", "author": "musictakeover", "comments": "http://delicious.com/url/3a9d56fcac6c76f8c39eaf29de1ce745", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Could You Pass a Driving Test? From Auto Insurance.org"}, "link": "http://www.autoinsurance.org/driving_test/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3a9d56fcac6c76f8c39eaf29de1ce745", "id": "http://delicious.com/url/3a9d56fcac6c76f8c39eaf29de1ce745#musictakeover", "tags": [{"term": "test", "scheme": "http://delicious.com/musictakeover/", "label": null}, {"term": "driving", "scheme": "http://delicious.com/musictakeover/", "label": null}, {"term": "auto+insurance", "scheme": "http://delicious.com/musictakeover/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 21:03:13 +0000", "links": [{"href": "http://www.freephotocalendar.net/free-printable-photo-calendar.php", "type": "text/html", "rel": "alternate"}], "title": "Print Your Custom Calendar", "author": "misterhugo", "comments": "http://delicious.com/url/eb5c5f74e0f7739d115d5b823a795dd7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Print Your Custom Calendar"}, "link": "http://www.freephotocalendar.net/free-printable-photo-calendar.php", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/eb5c5f74e0f7739d115d5b823a795dd7", "id": "http://delicious.com/url/eb5c5f74e0f7739d115d5b823a795dd7#misterhugo", "tags": [{"term": "calendario", "scheme": "http://delicious.com/misterhugo/", "label": null}, {"term": "generator", "scheme": "http://delicious.com/misterhugo/", "label": null}, {"term": "calendar", "scheme": "http://delicious.com/misterhugo/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 14:43:44 +0000", "links": [{"href": "http://en.wikipedia.org/wiki/Pseudonym", "type": "text/html", "rel": "alternate"}], "title": "Pseudonym - Wikipedia, the free encyclopedia", "author": "gilbrett32", "comments": "http://delicious.com/url/6692bd741cb4bc24ad69cf07438af550", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Pseudonym - Wikipedia, the free encyclopedia"}, "link": "http://en.wikipedia.org/wiki/Pseudonym", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6692bd741cb4bc24ad69cf07438af550", "id": "http://delicious.com/url/6692bd741cb4bc24ad69cf07438af550#gilbrett32", "tags": [{"term": "vocabulary_pseudonym", "scheme": "http://delicious.com/gilbrett32/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 02:23:21 +0000", "links": [{"href": "http://thesynapse.ning.com/", "type": "text/html", "rel": "alternate"}], "title": "The Synapse", "author": "biotea", "comments": "http://delicious.com/url/685fd579db697486c7db1ec0c7e60c44", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Synapse"}, "link": "http://thesynapse.ning.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/685fd579db697486c7db1ec0c7e60c44", "id": "http://delicious.com/url/685fd579db697486c7db1ec0c7e60c44#biotea", "tags": [{"term": "research", "scheme": "http://delicious.com/biotea/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 07:59:50 +0000", "links": [{"href": "http://gizmodo.com/5357993/how-to-back-up-all-your-stuff-for-free", "type": "text/html", "rel": "alternate"}], "title": "How To: Back Up All Your Stuff, For Free - Backup - Gizmodo", "author": "georgegmacdonald", "comments": "http://delicious.com/url/856d921a102c52c2fe9f46f395f06ba1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "How To: Back Up All Your Stuff, For Free - Backup - Gizmodo"}, "link": "http://gizmodo.com/5357993/how-to-back-up-all-your-stuff-for-free", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/856d921a102c52c2fe9f46f395f06ba1", "id": "http://delicious.com/url/856d921a102c52c2fe9f46f395f06ba1#georgegmacdonald"}, +{"updated": "Wed, 09 Sep 2009 00:51:08 +0000", "links": [{"href": "http://www.literacycenter.net/lessonview_en.htm", "type": "text/html", "rel": "alternate"}], "title": "Literacy Center Education Network - Play & Learn English", "author": "mac1428", "comments": "http://delicious.com/url/2472965b00d03b688d7195e7849f98f4", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Literacy Center Education Network - Play & Learn English"}, "link": "http://www.literacycenter.net/lessonview_en.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2472965b00d03b688d7195e7849f98f4", "id": "http://delicious.com/url/2472965b00d03b688d7195e7849f98f4#mac1428", "tags": [{"term": "kindergarten", "scheme": "http://delicious.com/mac1428/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 03:10:01 +0000", "links": [{"href": "http://www.bbc.co.uk/languages/", "type": "text/html", "rel": "alternate"}], "title": "BBC - Languages - Homepage", "author": "fea_mcbane", "comments": "http://delicious.com/url/3aef06538e297126040684bc71810fca", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "BBC - Languages - Homepage"}, "link": "http://www.bbc.co.uk/languages/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3aef06538e297126040684bc71810fca", "id": "http://delicious.com/url/3aef06538e297126040684bc71810fca#fea_mcbane", "tags": [{"term": "webnews", "scheme": "http://delicious.com/fea_mcbane/", "label": null}, {"term": "learning2.0", "scheme": "http://delicious.com/fea_mcbane/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 15:23:13 +0000", "links": [{"href": "http://www.learnsomethingeveryday.co.uk/", "type": "text/html", "rel": "alternate"}], "title": "LEARN SOMETHING EVERYDAY", "author": "chess.hoyle", "comments": "http://delicious.com/url/9a04e70337d85c16604dbab2cea2243d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "LEARN SOMETHING EVERYDAY"}, "link": "http://www.learnsomethingeveryday.co.uk/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9a04e70337d85c16604dbab2cea2243d", "id": "http://delicious.com/url/9a04e70337d85c16604dbab2cea2243d#chess.hoyle"}, +{"updated": "Tue, 08 Sep 2009 19:42:45 +0000", "links": [{"href": "http://www.ixedit.com/", "type": "text/html", "rel": "alternate"}], "title": "IxEdit", "author": "somenice", "comments": "http://delicious.com/url/f7007a9383c0ae9cd7cccd45f6590833", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "IxEdit"}, "link": "http://www.ixedit.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f7007a9383c0ae9cd7cccd45f6590833", "id": "http://delicious.com/url/f7007a9383c0ae9cd7cccd45f6590833#somenice", "tags": [{"term": "jquery", "scheme": "http://delicious.com/somenice/", "label": null}, {"term": "ui", "scheme": "http://delicious.com/somenice/", "label": null}, {"term": "design", "scheme": "http://delicious.com/somenice/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 05:10:37 +0000", "links": [{"href": "http://www.cato-unbound.org/archives/august-2007/", "type": "text/html", "rel": "alternate"}], "title": "Cato Unbound \u00bb August 2007: Who Needs Government? Pirates, Collapsed States, and the Possibility of Anarchy", "author": "benjaminista", "comments": "http://delicious.com/url/c252deab8030e92ddecb03b048bf3dcc", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Cato Unbound \u00bb August 2007: Who Needs Government? Pirates, Collapsed States, and the Possibility of Anarchy"}, "link": "http://www.cato-unbound.org/archives/august-2007/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c252deab8030e92ddecb03b048bf3dcc", "id": "http://delicious.com/url/c252deab8030e92ddecb03b048bf3dcc#benjaminista", "tags": [{"term": "anarchy", "scheme": "http://delicious.com/benjaminista/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 10:09:21 +0000", "links": [{"href": "http://www.spyfilms.com/blog/", "type": "text/html", "rel": "alternate"}], "title": "spy films: the blog", "author": "lafrontierascomparsa", "comments": "http://delicious.com/url/e2c431e9527546c7de143420ca94ed39", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "spy films: the blog"}, "link": "http://www.spyfilms.com/blog/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e2c431e9527546c7de143420ca94ed39", "id": "http://delicious.com/url/e2c431e9527546c7de143420ca94ed39#lafrontierascomparsa", "tags": [{"term": "motion", "scheme": "http://delicious.com/lafrontierascomparsa/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 18:29:10 +0000", "links": [{"href": "http://www.python.jp/doc/nightly/tut/", "type": "text/html", "rel": "alternate"}], "title": "Python \u30c1\u30e5\u30fc\u30c8\u30ea\u30a2\u30eb", "author": "shookonez", "comments": "http://delicious.com/url/3f925dbb38baaa49665cafeed5564c58", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Python \u30c1\u30e5\u30fc\u30c8\u30ea\u30a2\u30eb"}, "link": "http://www.python.jp/doc/nightly/tut/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3f925dbb38baaa49665cafeed5564c58", "id": "http://delicious.com/url/3f925dbb38baaa49665cafeed5564c58#shookonez", "tags": [{"term": "programming", "scheme": "http://delicious.com/shookonez/", "label": null}, {"term": "tutorial", "scheme": "http://delicious.com/shookonez/", "label": null}, {"term": "learning", "scheme": "http://delicious.com/shookonez/", "label": null}, {"term": "python", "scheme": "http://delicious.com/shookonez/", "label": null}, {"term": "Reference", "scheme": "http://delicious.com/shookonez/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 18:32:20 +0000", "links": [{"href": "http://channelsurfing.net/", "type": "text/html", "rel": "alternate"}], "title": "Watch Online tv sports news and entertainment for free at channelsurfing", "author": "jonessw74", "comments": "http://delicious.com/url/d12cc4842f907e08c6ffd0b33fc5f0c0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Watch Online tv sports news and entertainment for free at channelsurfing"}, "link": "http://channelsurfing.net/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d12cc4842f907e08c6ffd0b33fc5f0c0", "id": "http://delicious.com/url/d12cc4842f907e08c6ffd0b33fc5f0c0#jonessw74", "tags": [{"term": "tv", "scheme": "http://delicious.com/jonessw74/", "label": null}, {"term": "sports", "scheme": "http://delicious.com/jonessw74/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 11:28:30 +0000", "links": [{"href": "http://www.ryojiikeda.com/", "type": "text/html", "rel": "alternate"}], "title": "the official ryoji ikeda web site", "author": "nailbookmarks", "comments": "http://delicious.com/url/b7d4008656aa3d4fa52c458c44ba2960", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "the official ryoji ikeda web site"}, "link": "http://www.ryojiikeda.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b7d4008656aa3d4fa52c458c44ba2960", "id": "http://delicious.com/url/b7d4008656aa3d4fa52c458c44ba2960#nailbookmarks", "tags": [{"term": "musica", "scheme": "http://delicious.com/nailbookmarks/", "label": null}, {"term": "digitale", "scheme": "http://delicious.com/nailbookmarks/", "label": null}, {"term": "elettronica", "scheme": "http://delicious.com/nailbookmarks/", "label": null}, {"term": "videoarte", "scheme": "http://delicious.com/nailbookmarks/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 20:49:25 +0000", "links": [{"href": "http://www.lebensmittel-tabelle.de/", "type": "text/html", "rel": "alternate"}], "title": "Lebensmittel-Tabelle: Energie, Fett, Eiwei\u00df, Kohlenhydrate, Broteinheiten (BE)", "author": "pfh3006", "comments": "http://delicious.com/url/277015e4fa6496c5abf1b4383eba367d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Lebensmittel-Tabelle: Energie, Fett, Eiwei\u00df, Kohlenhydrate, Broteinheiten (BE)"}, "link": "http://www.lebensmittel-tabelle.de/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/277015e4fa6496c5abf1b4383eba367d", "id": "http://delicious.com/url/277015e4fa6496c5abf1b4383eba367d#pfh3006", "tags": [{"term": "fett", "scheme": "http://delicious.com/pfh3006/", "label": null}, {"term": "kohlenhydrat", "scheme": "http://delicious.com/pfh3006/", "label": null}, {"term": "eiweiss", "scheme": "http://delicious.com/pfh3006/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 19:14:24 +0000", "links": [{"href": "http://www.blurb.com/", "type": "text/html", "rel": "alternate"}], "title": "Make your own photo book with Blurb", "author": "phlaff", "comments": "http://delicious.com/url/165c0c2a57438753a9eb3c59d35c65f1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Make your own photo book with Blurb"}, "link": "http://www.blurb.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/165c0c2a57438753a9eb3c59d35c65f1", "id": "http://delicious.com/url/165c0c2a57438753a9eb3c59d35c65f1#phlaff", "tags": [{"term": "Livre-photo", "scheme": "http://delicious.com/phlaff/", "label": null}, {"term": "livre", "scheme": "http://delicious.com/phlaff/", "label": null}, {"term": "photo", "scheme": "http://delicious.com/phlaff/", "label": null}, {"term": "book-photo", "scheme": "http://delicious.com/phlaff/", "label": null}, {"term": "book", "scheme": "http://delicious.com/phlaff/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 15:01:27 +0000", "links": [{"href": "http://ow.ly/oBfG", "type": "text/html", "rel": "alternate"}], "title": "20 Stunning Examples of Pure HDR Photographs | Design Reviver", "author": "gattisw", "comments": "http://delicious.com/url/2786b415a3f6a2f3f406d13319adf9c4", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "20 Stunning Examples of Pure HDR Photographs | Design Reviver"}, "link": "http://ow.ly/oBfG", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2786b415a3f6a2f3f406d13319adf9c4", "id": "http://delicious.com/url/2786b415a3f6a2f3f406d13319adf9c4#gattisw", "tags": [{"term": "hdr", "scheme": "http://delicious.com/gattisw/", "label": null}, {"term": "photography", "scheme": "http://delicious.com/gattisw/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 17:10:10 +0000", "links": [{"href": "http://www.nytimes.com/2009/09/03/garden/03recycle.html?_r=1", "type": "text/html", "rel": "alternate"}], "title": "One Man\u2019s Trash ... - NYTimes.com", "author": "energypositive", "comments": "http://delicious.com/url/decd5743a12aaf4fac3c564c5009207c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "One Man\u2019s Trash ... - NYTimes.com"}, "link": "http://www.nytimes.com/2009/09/03/garden/03recycle.html?_r=1", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/decd5743a12aaf4fac3c564c5009207c", "id": "http://delicious.com/url/decd5743a12aaf4fac3c564c5009207c#energypositive", "tags": [{"term": "recycling", "scheme": "http://delicious.com/energypositive/", "label": null}, {"term": "housing", "scheme": "http://delicious.com/energypositive/", "label": null}, {"term": "greenbuilding", "scheme": "http://delicious.com/energypositive/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 08:48:47 +0000", "links": [{"href": "http://www.hanyuedu.net/", "type": "text/html", "rel": "alternate"}], "title": "\u5bf9\u5916\u6c49\u6559\u8bba\u575b", "author": "eto", "comments": "http://delicious.com/url/d65c13029c65f0ae8e8b3ab9b034e755", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u5bf9\u5916\u6c49\u6559\u8bba\u575b"}, "link": "http://www.hanyuedu.net/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d65c13029c65f0ae8e8b3ab9b034e755", "id": "http://delicious.com/url/d65c13029c65f0ae8e8b3ab9b034e755#eto", "tags": [{"term": "\u5bf9\u5916\u6c49\u8bed\u6559\u5b66", "scheme": "http://delicious.com/eto/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 13:56:35 +0000", "links": [{"href": "http://www.nuevasync.com/", "type": "text/html", "rel": "alternate"}], "title": "NuevaSync - Push Email and Over-the-Air Synchronization for Your iPhone", "author": "mkurz", "comments": "http://delicious.com/url/c25bbf443daa7ad04051bdb12e018f65", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "NuevaSync - Push Email and Over-the-Air Synchronization for Your iPhone"}, "link": "http://www.nuevasync.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c25bbf443daa7ad04051bdb12e018f65", "id": "http://delicious.com/url/c25bbf443daa7ad04051bdb12e018f65#mkurz", "tags": [{"term": "iphone", "scheme": "http://delicious.com/mkurz/", "label": null}, {"term": "google", "scheme": "http://delicious.com/mkurz/", "label": null}, {"term": "sync", "scheme": "http://delicious.com/mkurz/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 09:34:35 +0000", "links": [{"href": "http://www.rafalsiderski.com/", "type": "text/html", "rel": "alternate"}], "title": "Rafal Siderski photography - subjective document, photo essays, portrait, documentary photography, reportage", "author": "agadobi", "comments": "http://delicious.com/url/9e2412534844436451edb93993c3336e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Rafal Siderski photography - subjective document, photo essays, portrait, documentary photography, reportage"}, "link": "http://www.rafalsiderski.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9e2412534844436451edb93993c3336e", "id": "http://delicious.com/url/9e2412534844436451edb93993c3336e#agadobi", "tags": [{"term": "photography", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "portfolio", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "inspiration", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "art", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "photographer", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "photojournalism", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "poland", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "polska", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "portrait", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "people", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "document", "scheme": "http://delicious.com/agadobi/", "label": null}, {"term": "korba", "scheme": "http://delicious.com/agadobi/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 23:44:01 +0000", "links": [{"href": "http://blogs.zdnet.com/Apple/?p=4821", "type": "text/html", "rel": "alternate"}], "title": "Syncing with Dropbox | The Apple Core | ZDNet.com", "author": "shinobi.ken", "comments": "http://delicious.com/url/9627b1eb0b6fe2bd5f533bc8ac425616", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Syncing with Dropbox | The Apple Core | ZDNet.com"}, "link": "http://blogs.zdnet.com/Apple/?p=4821", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9627b1eb0b6fe2bd5f533bc8ac425616", "id": "http://delicious.com/url/9627b1eb0b6fe2bd5f533bc8ac425616#shinobi.ken", "tags": [{"term": "Dropbox", "scheme": "http://delicious.com/shinobi.ken/", "label": null}, {"term": "iPhone", "scheme": "http://delicious.com/shinobi.ken/", "label": null}, {"term": "sync", "scheme": "http://delicious.com/shinobi.ken/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 20:46:33 +0000", "links": [{"href": "http://miksa.ils.unc.edu/courses/inls523/", "type": "text/html", "rel": "alternate"}], "title": "INLS 523 Database I, Spring 2009", "author": "boringalias", "comments": "http://delicious.com/url/96768cee84f27548550853a45d35d286", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "INLS 523 Database I, Spring 2009"}, "link": "http://miksa.ils.unc.edu/courses/inls523/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/96768cee84f27548550853a45d35d286", "id": "http://delicious.com/url/96768cee84f27548550853a45d35d286#boringalias", "tags": [{"term": "Database_Systems", "scheme": "http://delicious.com/boringalias/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 00:45:24 +0000", "links": [{"href": "http://brainz.org/15-coolest-cases-biomimicry", "type": "text/html", "rel": "alternate"}], "title": "The 15 Coolest Cases of Biomimicry", "author": "ibbertelsen", "comments": "http://delicious.com/url/b36e2354d5ec9d7b05a3bbe1e0102184", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The 15 Coolest Cases of Biomimicry"}, "link": "http://brainz.org/15-coolest-cases-biomimicry", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b36e2354d5ec9d7b05a3bbe1e0102184", "id": "http://delicious.com/url/b36e2354d5ec9d7b05a3bbe1e0102184#ibbertelsen", "tags": [{"term": "Biomimicry", "scheme": "http://delicious.com/ibbertelsen/", "label": null}, {"term": "technology", "scheme": "http://delicious.com/ibbertelsen/", "label": null}, {"term": "design", "scheme": "http://delicious.com/ibbertelsen/", "label": null}, {"term": "biology", "scheme": "http://delicious.com/ibbertelsen/", "label": null}, {"term": "engineering", "scheme": "http://delicious.com/ibbertelsen/", "label": null}, {"term": "tecintense", "scheme": "http://delicious.com/ibbertelsen/", "label": null}, {"term": "meft3102", "scheme": "http://delicious.com/ibbertelsen/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 18:50:28 +0000", "links": [{"href": "http://www.monopolycitystreets.com/", "type": "text/html", "rel": "alternate"}], "title": "Monopoly City Streets", "author": "escobar5", "comments": "http://delicious.com/url/d5aa9993567eaeacb94746c34264c160", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Monopoly City Streets"}, "link": "http://www.monopolycitystreets.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d5aa9993567eaeacb94746c34264c160", "id": "http://delicious.com/url/d5aa9993567eaeacb94746c34264c160#escobar5", "tags": [{"term": "games", "scheme": "http://delicious.com/escobar5/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 14:09:44 +0000", "links": [{"href": "http://www.guardian.co.uk/business/2009/mar/02/mobile-phone-internet-developing-world", "type": "text/html", "rel": "alternate"}], "title": "Developing countries drive explosion in global mobile phone use", "author": "dajan", "comments": "http://delicious.com/url/651445deed8bd69044de9325619a081d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Developing countries drive explosion in global mobile phone use"}, "link": "http://www.guardian.co.uk/business/2009/mar/02/mobile-phone-internet-developing-world", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/651445deed8bd69044de9325619a081d", "id": "http://delicious.com/url/651445deed8bd69044de9325619a081d#dajan", "tags": [{"term": "research", "scheme": "http://delicious.com/dajan/", "label": null}, {"term": "technology", "scheme": "http://delicious.com/dajan/", "label": null}, {"term": "h800_block3_2009", "scheme": "http://delicious.com/dajan/", "label": null}, {"term": "internet", "scheme": "http://delicious.com/dajan/", "label": null}, {"term": "mobile", "scheme": "http://delicious.com/dajan/", "label": null}, {"term": "development", "scheme": "http://delicious.com/dajan/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 20:40:16 +0000", "links": [{"href": "http://www.zinepal.com/", "type": "text/html", "rel": "alternate"}], "title": "Create your own printable magazines and eBooks | zinepal.com", "author": "Gertu", "comments": "http://delicious.com/url/d3d6c7140f9fd94a4447a821e7c50252", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Create your own printable magazines and eBooks | zinepal.com"}, "link": "http://www.zinepal.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d3d6c7140f9fd94a4447a821e7c50252", "id": "http://delicious.com/url/d3d6c7140f9fd94a4447a821e7c50252#Gertu", "tags": [{"term": "tools", "scheme": "http://delicious.com/Gertu/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 15:34:11 +0000", "links": [{"href": "https://www.micds.org/podium/default.aspx?rc=1", "type": "text/html", "rel": "alternate"}], "title": "My Portal", "author": "agaioni", "comments": "http://delicious.com/url/ffb2e778dffa0a0115c77e3c2a910823", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "My Portal"}, "link": "https://www.micds.org/podium/default.aspx?rc=1", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ffb2e778dffa0a0115c77e3c2a910823", "id": "http://delicious.com/url/ffb2e778dffa0a0115c77e3c2a910823#agaioni", "tags": [{"term": "School", "scheme": "http://delicious.com/agaioni/", "label": null}, {"term": "Portal", "scheme": "http://delicious.com/agaioni/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 16:39:29 +0000", "links": [{"href": "http://www.thesession.nl/", "type": "text/html", "rel": "alternate"}], "title": "The Session - Amsterdam", "author": "stevensteven88", "comments": "http://delicious.com/url/385cbeaff642cb9b17df74aef1f6a8e3", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Session - Amsterdam"}, "link": "http://www.thesession.nl/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/385cbeaff642cb9b17df74aef1f6a8e3", "id": "http://delicious.com/url/385cbeaff642cb9b17df74aef1f6a8e3#stevensteven88", "tags": [{"term": "magazines", "scheme": "http://delicious.com/stevensteven88/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 19:09:05 +0000", "links": [{"href": "http://www.allforgood.org/", "type": "text/html", "rel": "alternate"}], "title": "All for Good", "author": "crpurgas", "comments": "http://delicious.com/url/37520d2347fdcd5a74004c0b5b4fe98e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "All for Good"}, "link": "http://www.allforgood.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/37520d2347fdcd5a74004c0b5b4fe98e", "id": "http://delicious.com/url/37520d2347fdcd5a74004c0b5b4fe98e#crpurgas", "tags": [{"term": "volunteer", "scheme": "http://delicious.com/crpurgas/", "label": null}, {"term": "nonprofit", "scheme": "http://delicious.com/crpurgas/", "label": null}, {"term": "web2.0", "scheme": "http://delicious.com/crpurgas/", "label": null}, {"term": "com498", "scheme": "http://delicious.com/crpurgas/", "label": null}, {"term": "mie310", "scheme": "http://delicious.com/crpurgas/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 20:45:35 +0000", "links": [{"href": "http://library.creativecow.net/articles/ross_shane/p2_workflow/video-tutorial.php", "type": "text/html", "rel": "alternate"}], "title": "Importing P2 into FCP : Apple Final Cut Pro Video Tutorial", "author": "djquixotic", "comments": "http://delicious.com/url/89de005414391c43a6e505024380e85e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Importing P2 into FCP : Apple Final Cut Pro Video Tutorial"}, "link": "http://library.creativecow.net/articles/ross_shane/p2_workflow/video-tutorial.php", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/89de005414391c43a6e505024380e85e", "id": "http://delicious.com/url/89de005414391c43a6e505024380e85e#djquixotic"}, +{"updated": "Mon, 14 Sep 2009 03:00:07 +0000", "links": [{"href": "http://www.boingboing.net/2009/09/09/conservative-califor.html", "type": "text/html", "rel": "alternate"}], "title": "Conservative California legislator gives pornographic account of his multiple affairs (including a lobbyist) into open mic - Boing Boing", "author": "claarn", "comments": "http://delicious.com/url/26ad91bb939269884addad2e4ebdc44e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Conservative California legislator gives pornographic account of his multiple affairs (including a lobbyist) into open mic - Boing Boing"}, "link": "http://www.boingboing.net/2009/09/09/conservative-califor.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/26ad91bb939269884addad2e4ebdc44e", "id": "http://delicious.com/url/26ad91bb939269884addad2e4ebdc44e#claarn", "tags": [{"term": "tOdO_hOme", "scheme": "http://delicious.com/claarn/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 21:40:34 +0000", "links": [{"href": "http://oedb.org/blogs/ilibrarian/2008/a-quick-guide-to-screencasting-for-libraries/", "type": "text/html", "rel": "alternate"}], "title": "iLibrarian \u00bb A Quick Guide to Screencasting for Libraries", "author": "schroede", "comments": "http://delicious.com/url/e36ed015df8c7c0bfef5f8d1b763232d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "iLibrarian \u00bb A Quick Guide to Screencasting for Libraries"}, "link": "http://oedb.org/blogs/ilibrarian/2008/a-quick-guide-to-screencasting-for-libraries/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e36ed015df8c7c0bfef5f8d1b763232d", "id": "http://delicious.com/url/e36ed015df8c7c0bfef5f8d1b763232d#schroede", "tags": [{"term": "screencasting", "scheme": "http://delicious.com/schroede/", "label": null}, {"term": "tutorials", "scheme": "http://delicious.com/schroede/", "label": null}, {"term": "Web2.0", "scheme": "http://delicious.com/schroede/", "label": null}, {"term": "screencasts", "scheme": "http://delicious.com/schroede/", "label": null}, {"term": "video", "scheme": "http://delicious.com/schroede/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 14:49:53 +0000", "links": [{"href": "http://www.businessdelo.ru/", "type": "text/html", "rel": "alternate"}], "title": "\u0421\u0432\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441, \u0441\u0432\u043e\u0435 \u0434\u0435\u043b\u043e, \u043a\u0430\u043a \u043d\u0430\u0447\u0430\u0442\u044c \u0441\u0432\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441, \u043a\u0430\u043a \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0441\u0432\u043e\u0435 \u0434\u0435\u043b\u043e, \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430", "author": "tsypa", "comments": "http://delicious.com/url/810faf8c273999647f0f8b145e56dfdf", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u0421\u0432\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441, \u0441\u0432\u043e\u0435 \u0434\u0435\u043b\u043e, \u043a\u0430\u043a \u043d\u0430\u0447\u0430\u0442\u044c \u0441\u0432\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441, \u043a\u0430\u043a \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0441\u0432\u043e\u0435 \u0434\u0435\u043b\u043e, \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430"}, "link": "http://www.businessdelo.ru/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/810faf8c273999647f0f8b145e56dfdf", "id": "http://delicious.com/url/810faf8c273999647f0f8b145e56dfdf#tsypa", "tags": [{"term": "startup", "scheme": "http://delicious.com/tsypa/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 09:22:27 +0000", "links": [{"href": "http://filezilla-project.org/", "type": "text/html", "rel": "alternate"}], "title": "FileZilla - The free FTP solution", "author": "mrclose", "comments": "http://delicious.com/url/ba2332b0c6c6a0058f1dd349af2746fd", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "FileZilla - The free FTP solution"}, "link": "http://filezilla-project.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ba2332b0c6c6a0058f1dd349af2746fd", "id": "http://delicious.com/url/ba2332b0c6c6a0058f1dd349af2746fd#mrclose", "tags": [{"term": "tools", "scheme": "http://delicious.com/mrclose/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 20:44:04 +0000", "links": [{"href": "http://www.ambrosiasw.com/", "type": "text/html", "rel": "alternate"}], "title": "Home | Ambrosia Software, Inc.", "author": "donkenoyer", "comments": "http://delicious.com/url/b566cc8d2ade63fd35fa32e3ccdd6e70", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Home | Ambrosia Software, Inc."}, "link": "http://www.ambrosiasw.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b566cc8d2ade63fd35fa32e3ccdd6e70", "id": "http://delicious.com/url/b566cc8d2ade63fd35fa32e3ccdd6e70#donkenoyer", "tags": [{"term": "software", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "mac", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "games", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "osx", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "imported", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "apple", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "macintosh", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "audio", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "software_developers", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "softwaredevelopers", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "safari_export", "scheme": "http://delicious.com/donkenoyer/", "label": null}, {"term": "screencapture", "scheme": "http://delicious.com/donkenoyer/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 19:58:52 +0000", "links": [{"href": "http://tweetvalue.com/", "type": "text/html", "rel": "alternate"}], "title": "TweetValue | How much is your Twitter profile worth?", "author": "medicman", "comments": "http://delicious.com/url/17be32c8783a875389fded03e3e69c1e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "TweetValue | How much is your Twitter profile worth?"}, "link": "http://tweetvalue.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/17be32c8783a875389fded03e3e69c1e", "id": "http://delicious.com/url/17be32c8783a875389fded03e3e69c1e#medicman", "tags": [{"term": "twitter", "scheme": "http://delicious.com/medicman/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 13:51:58 +0000", "links": [{"href": "http://www.citadel.org/doku.php", "type": "text/html", "rel": "alternate"}], "title": "Email and Groupware - easy to install, easy to use. - Citadel.org", "author": "lucciano", "comments": "http://delicious.com/url/a40de66171cde56fa512d2ce57f74ca9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Email and Groupware - easy to install, easy to use. - Citadel.org"}, "link": "http://www.citadel.org/doku.php", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a40de66171cde56fa512d2ce57f74ca9", "id": "http://delicious.com/url/a40de66171cde56fa512d2ce57f74ca9#lucciano", "tags": [{"term": "groupware", "scheme": "http://delicious.com/lucciano/", "label": null}, {"term": "email", "scheme": "http://delicious.com/lucciano/", "label": null}, {"term": "opensource", "scheme": "http://delicious.com/lucciano/", "label": null}, {"term": "calendar", "scheme": "http://delicious.com/lucciano/", "label": null}, {"term": "software", "scheme": "http://delicious.com/lucciano/", "label": null}, {"term": "collaboration", "scheme": "http://delicious.com/lucciano/", "label": null}, {"term": "linux", "scheme": "http://delicious.com/lucciano/", "label": null}, {"term": "web2.0", "scheme": "http://delicious.com/lucciano/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 14:34:39 +0000", "links": [{"href": "http://www.segel.biz/", "type": "text/html", "rel": "alternate"}], "title": "Segeln Home - Segeln - Yachtcharter - Segelcharter", "author": "mfranz63", "comments": "http://delicious.com/url/a590ffeccf2f239550e73b82fe0a6a38", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Segeln Home - Segeln - Yachtcharter - Segelcharter"}, "link": "http://www.segel.biz/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a590ffeccf2f239550e73b82fe0a6a38", "id": "http://delicious.com/url/a590ffeccf2f239550e73b82fe0a6a38#mfranz63", "tags": [{"term": "charter,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "yacht,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "segelyacht,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "segeln,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "yachtcharter,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "segelcharter,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "charterkauf,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "kaufcharter,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "mittelmeer,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "weltweit,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "charteryacht,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "segelurlaub,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "charterurlaub,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "urlaubscharter,", "scheme": "http://delicious.com/mfranz63/", "label": null}, {"term": "segelspass", "scheme": "http://delicious.com/mfranz63/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 17:00:12 +0000", "links": [{"href": "http://www.imdb.com/title/tt0387514/", "type": "text/html", "rel": "alternate"}], "title": "http://www.imdb.com/title/tt0387514/", "author": "__yuuko", "comments": "http://delicious.com/url/8da906a9785287cdb01ce877039a8839", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "http://www.imdb.com/title/tt0387514/"}, "link": "http://www.imdb.com/title/tt0387514/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8da906a9785287cdb01ce877039a8839", "id": "http://delicious.com/url/8da906a9785287cdb01ce877039a8839#__yuuko", "tags": [{"term": "liar_faggot", "scheme": "http://delicious.com/__yuuko/", "label": null}, {"term": "irc", "scheme": "http://delicious.com/__yuuko/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 18:13:06 +0000", "links": [{"href": "http://portal.acs.org/portal/acs/corg/content", "type": "text/html", "rel": "alternate"}], "title": "American Chemical Society - The world's largest scientific society.", "author": "ssikes1", "comments": "http://delicious.com/url/8416822ee6d468e76c03a6f29135ca87", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "American Chemical Society - The world's largest scientific society."}, "link": "http://portal.acs.org/portal/acs/corg/content", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8416822ee6d468e76c03a6f29135ca87", "id": "http://delicious.com/url/8416822ee6d468e76c03a6f29135ca87#ssikes1", "tags": [{"term": "education", "scheme": "http://delicious.com/ssikes1/", "label": null}, {"term": "resources", "scheme": "http://delicious.com/ssikes1/", "label": null}, {"term": "science", "scheme": "http://delicious.com/ssikes1/", "label": null}, {"term": "chemistry", "scheme": "http://delicious.com/ssikes1/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 21:54:55 +0000", "links": [{"href": "http://www.massage-zen-therapie.com/radio-zen/", "type": "text/html", "rel": "alternate"}], "title": "Radio zen - musique zen de relaxation et de m\u00e9ditation", "author": "neosolo", "comments": "http://delicious.com/url/473a529b56fd0f331acf59a81c15ba67", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Radio zen - musique zen de relaxation et de m\u00e9ditation"}, "link": "http://www.massage-zen-therapie.com/radio-zen/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/473a529b56fd0f331acf59a81c15ba67", "id": "http://delicious.com/url/473a529b56fd0f331acf59a81c15ba67#neosolo", "tags": [{"term": "massage", "scheme": "http://delicious.com/neosolo/", "label": null}, {"term": "music", "scheme": "http://delicious.com/neosolo/", "label": null}, {"term": "zen", "scheme": "http://delicious.com/neosolo/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 13:38:53 +0000", "links": [{"href": "http://hamusoku.com/archives/140466.html", "type": "text/html", "rel": "alternate"}], "title": "8\u6708\u751f\u307e\u308c\u7d42\u4e86\u306e\u304a\u77e5\u3089\u305b:\u30cf\u30e0\u30b9\u30bf\u30fc\u901f\u5831\u3000\uff12\u308d\u3050", "author": "kandelaar", "comments": "http://delicious.com/url/5505ccb5ea93b2b0b95cb21baad0fba9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "8\u6708\u751f\u307e\u308c\u7d42\u4e86\u306e\u304a\u77e5\u3089\u305b:\u30cf\u30e0\u30b9\u30bf\u30fc\u901f\u5831\u3000\uff12\u308d\u3050"}, "link": "http://hamusoku.com/archives/140466.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5505ccb5ea93b2b0b95cb21baad0fba9", "id": "http://delicious.com/url/5505ccb5ea93b2b0b95cb21baad0fba9#kandelaar", "tags": [{"term": "2ch", "scheme": "http://delicious.com/kandelaar/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 06:39:44 +0000", "links": [{"href": "http://schneestern.livejournal.com/157027.html", "type": "text/html", "rel": "alternate"}], "title": "schneestern - Master Post - Static (In The Silence Of Unspoken Words)", "author": "tortugax", "comments": "http://delicious.com/url/ad732686f58a2225f9095b923e51013d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "schneestern - Master Post - Static (In The Silence Of Unspoken Words)"}, "link": "http://schneestern.livejournal.com/157027.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ad732686f58a2225f9095b923e51013d", "id": "http://delicious.com/url/ad732686f58a2225f9095b923e51013d#tortugax", "tags": [{"term": "gerard/mikey/pete", "scheme": "http://delicious.com/tortugax/", "label": null}, {"term": "gerard/mikey", "scheme": "http://delicious.com/tortugax/", "label": null}, {"term": "fob", "scheme": "http://delicious.com/tortugax/", "label": null}, {"term": "mcr", "scheme": "http://delicious.com/tortugax/", "label": null}, {"term": "bandom", "scheme": "http://delicious.com/tortugax/", "label": null}, {"term": "fic", "scheme": "http://delicious.com/tortugax/", "label": null}, {"term": "nc-17", "scheme": "http://delicious.com/tortugax/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 23:38:22 +0000", "links": [{"href": "http://www.crunchbase.com/company/techstars", "type": "text/html", "rel": "alternate"}], "title": "TechStars Company Profile", "author": "segueporaqui", "comments": "http://delicious.com/url/819e70633e2f3767050cde12c3aac979", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "TechStars Company Profile"}, "link": "http://www.crunchbase.com/company/techstars", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/819e70633e2f3767050cde12c3aac979", "id": "http://delicious.com/url/819e70633e2f3767050cde12c3aac979#segueporaqui"}, +{"updated": "Tue, 08 Sep 2009 09:33:18 +0000", "links": [{"href": "http://code.google.com/p/paintweb/", "type": "text/html", "rel": "alternate"}], "title": "paintweb - Project Hosting on Google Code", "author": "cybernetics", "comments": "http://delicious.com/url/2dfbfb3befd5a066ce1c83a6423ce504", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "paintweb - Project Hosting on Google Code"}, "link": "http://code.google.com/p/paintweb/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2dfbfb3befd5a066ce1c83a6423ce504", "id": "http://delicious.com/url/2dfbfb3befd5a066ce1c83a6423ce504#cybernetics", "tags": [{"term": "canvas", "scheme": "http://delicious.com/cybernetics/", "label": null}, {"term": "javascript", "scheme": "http://delicious.com/cybernetics/", "label": null}, {"term": "tools", "scheme": "http://delicious.com/cybernetics/", "label": null}, {"term": "web", "scheme": "http://delicious.com/cybernetics/", "label": null}, {"term": "free", "scheme": "http://delicious.com/cybernetics/", "label": null}, {"term": "paint", "scheme": "http://delicious.com/cybernetics/", "label": null}, {"term": "art", "scheme": "http://delicious.com/cybernetics/", "label": null}, {"term": "download", "scheme": "http://delicious.com/cybernetics/", "label": null}, {"term": "moodle", "scheme": "http://delicious.com/cybernetics/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 09:56:17 +0000", "links": [{"href": "http://alexandraintheforest.com/", "type": "text/html", "rel": "alternate"}], "title": "Alexandra Valenti", "author": "Andros_20", "comments": "http://delicious.com/url/1b77fc96632417677caf515949bb32ea", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Alexandra Valenti"}, "link": "http://alexandraintheforest.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1b77fc96632417677caf515949bb32ea", "id": "http://delicious.com/url/1b77fc96632417677caf515949bb32ea#Andros_20", "tags": [{"term": "inspiration", "scheme": "http://delicious.com/Andros_20/", "label": null}, {"term": "fotografia", "scheme": "http://delicious.com/Andros_20/", "label": null}, {"term": "retro", "scheme": "http://delicious.com/Andros_20/", "label": null}, {"term": "70's", "scheme": "http://delicious.com/Andros_20/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 00:53:44 +0000", "links": [{"href": "http://health.yahoo.com/featured/34/what-doctors-wish-you-d-do", "type": "text/html", "rel": "alternate"}], "title": "What Doctors Wish You'd Do on Yahoo! Health", "author": "ladyivy", "comments": "http://delicious.com/url/21641413efcd072852974a87c987c3e4", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "What Doctors Wish You'd Do on Yahoo! Health"}, "link": "http://health.yahoo.com/featured/34/what-doctors-wish-you-d-do", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/21641413efcd072852974a87c987c3e4", "id": "http://delicious.com/url/21641413efcd072852974a87c987c3e4#ladyivy", "tags": [{"term": "article", "scheme": "http://delicious.com/ladyivy/", "label": null}, {"term": "health", "scheme": "http://delicious.com/ladyivy/", "label": null}, {"term": "advice", "scheme": "http://delicious.com/ladyivy/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 22:51:44 +0000", "links": [{"href": "https://www.mfs.com/wps/portal/!ut/p/c0/04_SB8K8xLLM9MSSzPy8xBz9wNC8zLLUomIg088lNS2xNKcEJKJfkO6oCAD9cw6V/", "type": "text/html", "rel": "alternate"}], "title": "Welcome to MFS Investment Management", "author": "imarek", "comments": "http://delicious.com/url/f7367c5a7320d82e923806944375609d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Welcome to MFS Investment Management"}, "link": "https://www.mfs.com/wps/portal/!ut/p/c0/04_SB8K8xLLM9MSSzPy8xBz9wNC8zLLUomIg088lNS2xNKcEJKJfkO6oCAD9cw6V/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f7367c5a7320d82e923806944375609d", "id": "http://delicious.com/url/f7367c5a7320d82e923806944375609d#imarek", "tags": [{"term": "Orbis", "scheme": "http://delicious.com/imarek/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 01:20:04 +0000", "links": [{"href": "http://brucefwebster.com/2009/09/07/hr-3200-from-a-systems-design-perspective-part-i/", "type": "text/html", "rel": "alternate"}], "title": "HR 3200 from a systems design perspective (Part I) : Bruce F. Webster", "author": "Neuromancer0", "comments": "http://delicious.com/url/e22881d89af215312906387b3d542043", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "HR 3200 from a systems design perspective (Part I) : Bruce F. Webster"}, "link": "http://brucefwebster.com/2009/09/07/hr-3200-from-a-systems-design-perspective-part-i/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e22881d89af215312906387b3d542043", "id": "http://delicious.com/url/e22881d89af215312906387b3d542043#Neuromancer0", "tags": [{"term": "software", "scheme": "http://delicious.com/Neuromancer0/", "label": null}, {"term": "design", "scheme": "http://delicious.com/Neuromancer0/", "label": null}, {"term": "politics", "scheme": "http://delicious.com/Neuromancer0/", "label": null}, {"term": "law", "scheme": "http://delicious.com/Neuromancer0/", "label": null}, {"term": "legislation", "scheme": "http://delicious.com/Neuromancer0/", "label": null}, {"term": "legal", "scheme": "http://delicious.com/Neuromancer0/", "label": null}, {"term": "it", "scheme": "http://delicious.com/Neuromancer0/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 11:57:09 +0000", "links": [{"href": "http://mashable.com/2009/04/02/social-media-charity-events/", "type": "text/html", "rel": "alternate"}], "title": "5 Events That Have Used Social Media for a Good Cause", "author": "lklop", "comments": "http://delicious.com/url/031235af7d0005b95d9dd0e4da40d178", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "5 Events That Have Used Social Media for a Good Cause"}, "link": "http://mashable.com/2009/04/02/social-media-charity-events/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/031235af7d0005b95d9dd0e4da40d178", "id": "http://delicious.com/url/031235af7d0005b95d9dd0e4da40d178#lklop", "tags": [{"term": "socialmedia", "scheme": "http://delicious.com/lklop/", "label": null}, {"term": "social", "scheme": "http://delicious.com/lklop/", "label": null}, {"term": "marketing", "scheme": "http://delicious.com/lklop/", "label": null}, {"term": "socialnetworking", "scheme": "http://delicious.com/lklop/", "label": null}, {"term": "media", "scheme": "http://delicious.com/lklop/", "label": null}, {"term": "community", "scheme": "http://delicious.com/lklop/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 19:23:27 +0000", "links": [{"href": "http://www.theatlantic.com/doc/200909/health-care", "type": "text/html", "rel": "alternate"}], "title": "How American Health Care Killed My Father - The Atlantic(September 2009)", "author": "jordan.cheek", "comments": "http://delicious.com/url/34dde9c4d7937fcafd5d1510cf522afa", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "How American Health Care Killed My Father - The Atlantic(September 2009)"}, "link": "http://www.theatlantic.com/doc/200909/health-care", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/34dde9c4d7937fcafd5d1510cf522afa", "id": "http://delicious.com/url/34dde9c4d7937fcafd5d1510cf522afa#jordan.cheek", "tags": [{"term": "health", "scheme": "http://delicious.com/jordan.cheek/", "label": null}, {"term": "politics", "scheme": "http://delicious.com/jordan.cheek/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 15:23:28 +0000", "links": [{"href": "http://www.esquire.com/features/bill-clinton-interview-1009", "type": "text/html", "rel": "alternate"}], "title": "Bill Clinton Esquire Interview - President Clinton and Health Care - Esquire", "author": "jwisneski", "comments": "http://delicious.com/url/78d9edcd19edff11de270920459d00ab", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Bill Clinton Esquire Interview - President Clinton and Health Care - Esquire"}, "link": "http://www.esquire.com/features/bill-clinton-interview-1009", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/78d9edcd19edff11de270920459d00ab", "id": "http://delicious.com/url/78d9edcd19edff11de270920459d00ab#jwisneski", "tags": [{"term": "healthcare", "scheme": "http://delicious.com/jwisneski/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 20:32:55 +0000", "links": [{"href": "http://www.instantshift.com/2009/08/09/77-excellent-photoshop-tutorials-for-designing-posters/", "type": "text/html", "rel": "alternate"}], "title": "77 Excellent Photoshop Tutorials For Designing Posters | Tutorials | instantShift", "author": "stewmatt1", "comments": "http://delicious.com/url/217fc10f073e587b82dea7b499b6f6ee", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "77 Excellent Photoshop Tutorials For Designing Posters | Tutorials | instantShift"}, "link": "http://www.instantshift.com/2009/08/09/77-excellent-photoshop-tutorials-for-designing-posters/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/217fc10f073e587b82dea7b499b6f6ee", "id": "http://delicious.com/url/217fc10f073e587b82dea7b499b6f6ee#stewmatt1", "tags": [{"term": "photoshop", "scheme": "http://delicious.com/stewmatt1/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 04:19:22 +0000", "links": [{"href": "http://puredata.info/", "type": "text/html", "rel": "alternate"}], "title": "Pure Data \u2014 PD Community Site", "author": "rocha", "comments": "http://delicious.com/url/039852da815cf2e577b880a5d7760fff", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Pure Data \u2014 PD Community Site"}, "link": "http://puredata.info/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/039852da815cf2e577b880a5d7760fff", "id": "http://delicious.com/url/039852da815cf2e577b880a5d7760fff#rocha", "tags": [{"term": "processing", "scheme": "http://delicious.com/rocha/", "label": null}, {"term": "audio", "scheme": "http://delicious.com/rocha/", "label": null}, {"term": "puredata", "scheme": "http://delicious.com/rocha/", "label": null}, {"term": "software", "scheme": "http://delicious.com/rocha/", "label": null}, {"term": "video", "scheme": "http://delicious.com/rocha/", "label": null}, {"term": "sound", "scheme": "http://delicious.com/rocha/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 06:23:59 +0000", "links": [{"href": "http://delanach.livejournal.com/10606.html", "type": "text/html", "rel": "alternate"}], "title": "Title: I Want You to Want Me", "author": "bellebelle2", "comments": "http://delicious.com/url/a037f3004ee25c98d0e25034399e647d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Title: I Want You to Want Me"}, "link": "http://delanach.livejournal.com/10606.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a037f3004ee25c98d0e25034399e647d", "id": "http://delicious.com/url/a037f3004ee25c98d0e25034399e647d#bellebelle2", "tags": [{"term": "sam/dean", "scheme": "http://delicious.com/bellebelle2/", "label": null}, {"term": "nc-17", "scheme": "http://delicious.com/bellebelle2/", "label": null}, {"term": "pre-series", "scheme": "http://delicious.com/bellebelle2/", "label": null}, {"term": "supernatural", "scheme": "http://delicious.com/bellebelle2/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 20:40:55 +0000", "links": [{"href": "http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=900", "type": "text/html", "rel": "alternate"}], "title": "VMware Self-Service- Moving Virtual Disks to or from ESX Server", "author": "dnishida", "comments": "http://delicious.com/url/2dd1fac7e465870981e3220c01a65dc1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "VMware Self-Service- Moving Virtual Disks to or from ESX Server"}, "link": "http://kb.vmware.com/selfservice/microsites/search.do?language=en_US&cmd=displayKC&externalId=900", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2dd1fac7e465870981e3220c01a65dc1", "id": "http://delicious.com/url/2dd1fac7e465870981e3220c01a65dc1#dnishida", "tags": [{"term": "VMware", "scheme": "http://delicious.com/dnishida/", "label": null}, {"term": "ESX", "scheme": "http://delicious.com/dnishida/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 18:38:51 +0000", "links": [{"href": "http://www.chrisbrogan.com/5-things-small-business-owners-should-do-today-online/", "type": "text/html", "rel": "alternate"}], "title": "5 Things Small Business Owners Should Do Today Online", "author": "draines", "comments": "http://delicious.com/url/50cf91690f728c61c714a6879345f29d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "5 Things Small Business Owners Should Do Today Online"}, "link": "http://www.chrisbrogan.com/5-things-small-business-owners-should-do-today-online/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/50cf91690f728c61c714a6879345f29d", "id": "http://delicious.com/url/50cf91690f728c61c714a6879345f29d#draines"}, +{"updated": "Sat, 12 Sep 2009 18:01:45 +0000", "links": [{"href": "http://support.wordpress.com/prevent-content-theft/", "type": "text/html", "rel": "alternate"}], "title": "Prevent Content Theft \u00ab Support \u00ab WordPress.com", "author": "rudy.flores", "comments": "http://delicious.com/url/7d715761168423a191684c2d8cd26ef0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Prevent Content Theft \u00ab Support \u00ab WordPress.com"}, "link": "http://support.wordpress.com/prevent-content-theft/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7d715761168423a191684c2d8cd26ef0", "id": "http://delicious.com/url/7d715761168423a191684c2d8cd26ef0#rudy.flores", "tags": [{"term": "Copyright", "scheme": "http://delicious.com/rudy.flores/", "label": null}, {"term": "songwriting", "scheme": "http://delicious.com/rudy.flores/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 17:01:43 +0000", "links": [{"href": "http://www.jingproject.com/download/default.asp", "type": "text/html", "rel": "alternate"}], "title": "Download Jing | Add visuals to your online conversations", "author": "mariabarros", "comments": "http://delicious.com/url/3283aac750fd10148e436db1a15bba61", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Download Jing | Add visuals to your online conversations"}, "link": "http://www.jingproject.com/download/default.asp", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3283aac750fd10148e436db1a15bba61", "id": "http://delicious.com/url/3283aac750fd10148e436db1a15bba61#mariabarros", "tags": [{"term": "ximbal", "scheme": "http://delicious.com/mariabarros/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 20:44:05 +0000", "links": [{"href": "http://habrahabr.ru/blogs/hardware/68139/", "type": "text/html", "rel": "alternate"}], "title": "\u041e\u0431\u0437\u043e\u0440 \u043a\u043e\u0440\u043f\u0443\u0441\u043e\u0432 \u0444\u043e\u0440\u043c-\u0444\u0430\u043a\u0442\u043e\u0440\u0430 mini-ITX \u0434\u043b\u044f HTPC (\u0447\u0430\u0441\u0442\u044c 1) / \u0416\u0435\u043b\u0435\u0437\u043e / \u0425\u0430\u0431\u0440\u0430\u0445\u0430\u0431\u0440", "author": "navymaker", "comments": "http://delicious.com/url/b4c7e19d43061950a979165116ab28d0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u041e\u0431\u0437\u043e\u0440 \u043a\u043e\u0440\u043f\u0443\u0441\u043e\u0432 \u0444\u043e\u0440\u043c-\u0444\u0430\u043a\u0442\u043e\u0440\u0430 mini-ITX \u0434\u043b\u044f HTPC (\u0447\u0430\u0441\u0442\u044c 1) / \u0416\u0435\u043b\u0435\u0437\u043e / \u0425\u0430\u0431\u0440\u0430\u0445\u0430\u0431\u0440"}, "link": "http://habrahabr.ru/blogs/hardware/68139/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b4c7e19d43061950a979165116ab28d0", "id": "http://delicious.com/url/b4c7e19d43061950a979165116ab28d0#navymaker", "tags": [{"term": "mini-itx", "scheme": "http://delicious.com/navymaker/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 01:34:27 +0000", "links": [{"href": "http://webtech.kennesaw.edu/jcheek3/mother_goose.htm", "type": "text/html", "rel": "alternate"}], "title": "Mother Goose", "author": "fafcummings", "comments": "http://delicious.com/url/6a93e17957ace01ae54650182ca8491b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Mother Goose"}, "link": "http://webtech.kennesaw.edu/jcheek3/mother_goose.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6a93e17957ace01ae54650182ca8491b", "id": "http://delicious.com/url/6a93e17957ace01ae54650182ca8491b#fafcummings", "tags": [{"term": "rhyme", "scheme": "http://delicious.com/fafcummings/", "label": null}, {"term": "reading", "scheme": "http://delicious.com/fafcummings/", "label": null}, {"term": "education", "scheme": "http://delicious.com/fafcummings/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 17:15:03 +0000", "links": [{"href": "http://technet.microsoft.com/en-us/library/bb727008.aspx", "type": "text/html", "rel": "alternate"}], "title": "File and Folder Permissions", "author": "redwings3030", "comments": "http://delicious.com/url/1cbad5ad2a33042b2b0208f4bc7d735d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "File and Folder Permissions"}, "link": "http://technet.microsoft.com/en-us/library/bb727008.aspx", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1cbad5ad2a33042b2b0208f4bc7d735d", "id": "http://delicious.com/url/1cbad5ad2a33042b2b0208f4bc7d735d#redwings3030"}, +{"updated": "Wed, 09 Sep 2009 11:57:37 +0000", "links": [{"href": "http://www.vmukti.com/", "type": "text/html", "rel": "alternate"}], "title": "Video Streaming Software, Web Video Conference Software, Audio/Video ...", "author": "kamyar1979", "comments": "http://delicious.com/url/d142b2e36de181c2b8a67c3d0c19f16e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Video Streaming Software, Web Video Conference Software, Audio/Video ..."}, "link": "http://www.vmukti.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d142b2e36de181c2b8a67c3d0c19f16e", "id": "http://delicious.com/url/d142b2e36de181c2b8a67c3d0c19f16e#kamyar1979", "tags": [{"term": "Video", "scheme": "http://delicious.com/kamyar1979/", "label": null}, {"term": "Conference", "scheme": "http://delicious.com/kamyar1979/", "label": null}, {"term": "Online", "scheme": "http://delicious.com/kamyar1979/", "label": null}, {"term": "Voice", "scheme": "http://delicious.com/kamyar1979/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 21:57:54 +0000", "links": [{"href": "http://www.uhull.com.br/06/18/30-search-engines-de-rapidshare-megaupload-e-mediafire/", "type": "text/html", "rel": "alternate"}], "title": "30 Search Engines de Rapidshare, Megaupload e Mediafire \u2013 Uhull S.A.", "author": "Fallkirk", "comments": "http://delicious.com/url/9e7bb0d17ebd0f248dfec561dec15262", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "30 Search Engines de Rapidshare, Megaupload e Mediafire \u2013 Uhull S.A."}, "link": "http://www.uhull.com.br/06/18/30-search-engines-de-rapidshare-megaupload-e-mediafire/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9e7bb0d17ebd0f248dfec561dec15262", "id": "http://delicious.com/url/9e7bb0d17ebd0f248dfec561dec15262#Fallkirk", "tags": [{"term": "Buscador", "scheme": "http://delicious.com/Fallkirk/", "label": null}, {"term": "Download", "scheme": "http://delicious.com/Fallkirk/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 16:02:36 +0000", "links": [{"href": "http://dslrblog.com/", "type": "text/html", "rel": "alternate"}], "title": "DSLRBLOG - Photography Business Blog - Starting and Running a Business as a Professional Photographer", "author": "maurotrb", "comments": "http://delicious.com/url/47c608dcf0bc5faaa49d68e6e796ca74", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "DSLRBLOG - Photography Business Blog - Starting and Running a Business as a Professional Photographer"}, "link": "http://dslrblog.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/47c608dcf0bc5faaa49d68e6e796ca74", "id": "http://delicious.com/url/47c608dcf0bc5faaa49d68e6e796ca74#maurotrb", "tags": [{"term": "photography", "scheme": "http://delicious.com/maurotrb/", "label": null}, {"term": "business", "scheme": "http://delicious.com/maurotrb/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 18:56:07 +0000", "links": [{"href": "http://www.androidsnippets.org/", "type": "text/html", "rel": "alternate"}], "title": "Android Snippets: Welcome", "author": "brent.edwards", "comments": "http://delicious.com/url/84a1d7dd7bd97040e9f60e26336aeac9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Android Snippets: Welcome"}, "link": "http://www.androidsnippets.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/84a1d7dd7bd97040e9f60e26336aeac9", "id": "http://delicious.com/url/84a1d7dd7bd97040e9f60e26336aeac9#brent.edwards", "tags": [{"term": "android", "scheme": "http://delicious.com/brent.edwards/", "label": null}, {"term": "programming", "scheme": "http://delicious.com/brent.edwards/", "label": null}, {"term": "snippets", "scheme": "http://delicious.com/brent.edwards/", "label": null}, {"term": "mobile", "scheme": "http://delicious.com/brent.edwards/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 06:51:45 +0000", "links": [{"href": "http://vepece.tumblr.com/", "type": "text/html", "rel": "alternate"}], "title": "V\u00e9p\u00e9c\u00e9", "author": "parappa", "comments": "http://delicious.com/url/187ec05f55aae32acdaaa2f74f63857f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "V\u00e9p\u00e9c\u00e9"}, "link": "http://vepece.tumblr.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/187ec05f55aae32acdaaa2f74f63857f", "id": "http://delicious.com/url/187ec05f55aae32acdaaa2f74f63857f#parappa", "tags": [{"term": "vpc", "scheme": "http://delicious.com/parappa/", "label": null}, {"term": "fun", "scheme": "http://delicious.com/parappa/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 11:02:23 +0000", "links": [{"href": "http://www.techcrunch.com/2009/09/08/the-top-20-vc-bloggers-september-2009/", "type": "text/html", "rel": "alternate"}], "title": "The Top 20 VC Bloggers (September 2009)", "author": "deep_kannan", "comments": "http://delicious.com/url/6e68046d64b1ec787f7e3478daa97a55", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Top 20 VC Bloggers (September 2009)"}, "link": "http://www.techcrunch.com/2009/09/08/the-top-20-vc-bloggers-september-2009/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6e68046d64b1ec787f7e3478daa97a55", "id": "http://delicious.com/url/6e68046d64b1ec787f7e3478daa97a55#deep_kannan", "tags": [{"term": "VC", "scheme": "http://delicious.com/deep_kannan/", "label": null}, {"term": "blog", "scheme": "http://delicious.com/deep_kannan/", "label": null}, {"term": "startup", "scheme": "http://delicious.com/deep_kannan/", "label": null}, {"term": "entrepreneurship", "scheme": "http://delicious.com/deep_kannan/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 04:07:24 +0000", "links": [{"href": "http://cocos2d.org/", "type": "text/html", "rel": "alternate"}], "title": "cocos2d", "author": "karanjude", "comments": "http://delicious.com/url/9c66d332593a9baa612a5f24629002ee", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "cocos2d"}, "link": "http://cocos2d.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9c66d332593a9baa612a5f24629002ee", "id": "http://delicious.com/url/9c66d332593a9baa612a5f24629002ee#karanjude", "tags": [{"term": "python", "scheme": "http://delicious.com/karanjude/", "label": null}, {"term": "programming", "scheme": "http://delicious.com/karanjude/", "label": null}, {"term": "game", "scheme": "http://delicious.com/karanjude/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 09:51:29 +0000", "links": [{"href": "http://www.ascendercorp.com/pr/2009-09-08/", "type": "text/html", "rel": "alternate"}], "title": "Enhancements to Georgia & Verdana Typeface Families Announced", "author": "guyweb", "comments": "http://delicious.com/url/33c21ad73090f7befa60fd2bd778a4c7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Enhancements to Georgia & Verdana Typeface Families Announced"}, "link": "http://www.ascendercorp.com/pr/2009-09-08/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/33c21ad73090f7befa60fd2bd778a4c7", "id": "http://delicious.com/url/33c21ad73090f7befa60fd2bd778a4c7#guyweb", "tags": [{"term": "webdesign", "scheme": "http://delicious.com/guyweb/", "label": null}, {"term": "typography", "scheme": "http://delicious.com/guyweb/", "label": null}, {"term": "verdana", "scheme": "http://delicious.com/guyweb/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 06:01:06 +0000", "links": [{"href": "http://ja.wikipedia.org/wiki/%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%8B%E3%83%A1%E3%82%A4%E3%83%88", "type": "text/html", "rel": "alternate"}], "title": "\u30b9\u30ad\u30e3\u30cb\u30e1\u30a4\u30c8 - Wikipedia", "author": "taniguchiko", "comments": "http://delicious.com/url/13c3a4511be14ac1c85bbeb20a0f8995", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u30b9\u30ad\u30e3\u30cb\u30e1\u30a4\u30c8 - Wikipedia"}, "link": "http://ja.wikipedia.org/wiki/%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%8B%E3%83%A1%E3%82%A4%E3%83%88", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/13c3a4511be14ac1c85bbeb20a0f8995", "id": "http://delicious.com/url/13c3a4511be14ac1c85bbeb20a0f8995#taniguchiko", "tags": [{"term": "video", "scheme": "http://delicious.com/taniguchiko/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 21:45:31 +0000", "links": [{"href": "http://www.cooks.com/rec/view/0,1655,138191-244198,00.html", "type": "text/html", "rel": "alternate"}], "title": "Cooks.com - Recipe - Coney Island Chili Dog Sauce", "author": "deniseshoup", "comments": "http://delicious.com/url/3bfc70642e70a38691bd2c3c8ac4b660", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Cooks.com - Recipe - Coney Island Chili Dog Sauce"}, "link": "http://www.cooks.com/rec/view/0,1655,138191-244198,00.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3bfc70642e70a38691bd2c3c8ac4b660", "id": "http://delicious.com/url/3bfc70642e70a38691bd2c3c8ac4b660#deniseshoup", "tags": [{"term": "to_cook", "scheme": "http://delicious.com/deniseshoup/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 09:51:26 +0000", "links": [{"href": "http://stevenjambot.wordpress.com/2009/09/05/revolutions-du-journalisme-qui-suivre-sur-twitter/", "type": "text/html", "rel": "alternate"}], "title": "R/\u00e9volutions du journalisme : qui suivre sur twitter \u00ab Steven Jambot", "author": "raphaelle_ridarch", "comments": "http://delicious.com/url/eee2ea470fcfdaf0f97bba4a550712c7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "R/\u00e9volutions du journalisme : qui suivre sur twitter \u00ab Steven Jambot"}, "link": "http://stevenjambot.wordpress.com/2009/09/05/revolutions-du-journalisme-qui-suivre-sur-twitter/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/eee2ea470fcfdaf0f97bba4a550712c7", "id": "http://delicious.com/url/eee2ea470fcfdaf0f97bba4a550712c7#raphaelle_ridarch", "tags": [{"term": "journaliste", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}, {"term": "twitter", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}, {"term": "liste", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}, {"term": "information", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}, {"term": "presse", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}, {"term": "culture", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}, {"term": "internet", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}, {"term": "medias", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}, {"term": "blogs", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}, {"term": "webwriting", "scheme": "http://delicious.com/raphaelle_ridarch/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 16:44:18 +0000", "links": [{"href": "http://www.museumoftheamericancocktail.org/", "type": "text/html", "rel": "alternate"}], "title": "The Museum of the American Cocktail", "author": "sirvelvet", "comments": "http://delicious.com/url/a28e47d6e8d2c08b024a82ec99d835e0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Museum of the American Cocktail"}, "link": "http://www.museumoftheamericancocktail.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a28e47d6e8d2c08b024a82ec99d835e0", "id": "http://delicious.com/url/a28e47d6e8d2c08b024a82ec99d835e0#sirvelvet", "tags": [{"term": "NewOrleans:Museums", "scheme": "http://delicious.com/sirvelvet/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 12:49:43 +0000", "links": [{"href": "http://m.www.yahoo.com/", "type": "text/html", "rel": "alternate"}], "title": "Yahoo!", "author": "hermonaloa", "comments": "http://delicious.com/url/61b668b12719db662c51b02cdc23abb0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Yahoo!"}, "link": "http://m.www.yahoo.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/61b668b12719db662c51b02cdc23abb0", "id": "http://delicious.com/url/61b668b12719db662c51b02cdc23abb0#hermonaloa", "tags": [{"term": "personel", "scheme": "http://delicious.com/hermonaloa/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 23:14:48 +0000", "links": [{"href": "http://blogs.laweekly.com/style_council/tech/what-would-9-11-be-like-in-the/", "type": "text/html", "rel": "alternate"}], "title": "What Would 9-11 Be Like in the Age of Social Media? - Los Angeles Art - Style Council", "author": "krimsen", "comments": "http://delicious.com/url/0b7cdfae2353727006420d626fee2cda", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "What Would 9-11 Be Like in the Age of Social Media? - Los Angeles Art - Style Council"}, "link": "http://blogs.laweekly.com/style_council/tech/what-would-9-11-be-like-in-the/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0b7cdfae2353727006420d626fee2cda", "id": "http://delicious.com/url/0b7cdfae2353727006420d626fee2cda#krimsen", "tags": [{"term": "history-computer", "scheme": "http://delicious.com/krimsen/", "label": null}, {"term": "history", "scheme": "http://delicious.com/krimsen/", "label": null}, {"term": "social-networking", "scheme": "http://delicious.com/krimsen/", "label": null}, {"term": "culture", "scheme": "http://delicious.com/krimsen/", "label": null}, {"term": "society", "scheme": "http://delicious.com/krimsen/", "label": null}, {"term": "technology", "scheme": "http://delicious.com/krimsen/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 14:58:59 +0000", "links": [{"href": "https://meapplicationdevelopers.dev.java.net/phoneme_ui_labs.html", "type": "text/html", "rel": "alternate"}], "title": "meapplicationdevelopers: phoneME UI Labs", "author": "nano_unanue", "comments": "http://delicious.com/url/fecf85e950c28f9dc83be16fc56535a0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "meapplicationdevelopers: phoneME UI Labs"}, "link": "https://meapplicationdevelopers.dev.java.net/phoneme_ui_labs.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/fecf85e950c28f9dc83be16fc56535a0", "id": "http://delicious.com/url/fecf85e950c28f9dc83be16fc56535a0#nano_unanue", "tags": [{"term": "java", "scheme": "http://delicious.com/nano_unanue/", "label": null}, {"term": "jme", "scheme": "http://delicious.com/nano_unanue/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 09:46:09 +0000", "links": [{"href": "http://www.apartmenttherapy.com/dc/outdoor/setting-up-home-5-hardtokill-houseplants-092385", "type": "text/html", "rel": "alternate"}], "title": "Apartment Therapy DC | Setting Up Home: 5 Hard-to-Kill Houseplants", "author": "rafabayona", "comments": "http://delicious.com/url/924c0feeb4a5e0f400f6e7a6ae71dde8", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Apartment Therapy DC | Setting Up Home: 5 Hard-to-Kill Houseplants"}, "link": "http://www.apartmenttherapy.com/dc/outdoor/setting-up-home-5-hardtokill-houseplants-092385", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/924c0feeb4a5e0f400f6e7a6ae71dde8", "id": "http://delicious.com/url/924c0feeb4a5e0f400f6e7a6ae71dde8#rafabayona"}, +{"updated": "Sun, 06 Sep 2009 14:32:15 +0000", "links": [{"href": "http://www.tradeboss.com/", "type": "text/html", "rel": "alternate"}], "title": "B2B Market Place, Trade Leads", "author": "solomonsoftware", "comments": "http://delicious.com/url/0402cdecd67cf0356028cb0b27c04230", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "B2B Market Place, Trade Leads"}, "link": "http://www.tradeboss.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0402cdecd67cf0356028cb0b27c04230", "id": "http://delicious.com/url/0402cdecd67cf0356028cb0b27c04230#solomonsoftware", "tags": [{"term": "business", "scheme": "http://delicious.com/solomonsoftware/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 22:17:19 +0000", "links": [{"href": "http://www.ecosmartworld.com/", "type": "text/html", "rel": "alternate"}], "title": "ecosmartworld.com", "author": "scottarnold", "comments": "http://delicious.com/url/f4f0a53c063524653bee61d1d1aea15d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "ecosmartworld.com"}, "link": "http://www.ecosmartworld.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f4f0a53c063524653bee61d1d1aea15d", "id": "http://delicious.com/url/f4f0a53c063524653bee61d1d1aea15d#scottarnold", "tags": [{"term": "art", "scheme": "http://delicious.com/scottarnold/", "label": null}, {"term": "shopping", "scheme": "http://delicious.com/scottarnold/", "label": null}, {"term": "environment", "scheme": "http://delicious.com/scottarnold/", "label": null}, {"term": "eco", "scheme": "http://delicious.com/scottarnold/", "label": null}, {"term": "green", "scheme": "http://delicious.com/scottarnold/", "label": null}, {"term": "products", "scheme": "http://delicious.com/scottarnold/", "label": null}, {"term": "sustainability", "scheme": "http://delicious.com/scottarnold/", "label": null}, {"term": "sustainable", "scheme": "http://delicious.com/scottarnold/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 15:47:19 +0000", "links": [{"href": "http://www.guardian.co.uk/media/pda/2009/sep/08/internet-manifesto-future-journalism", "type": "text/html", "rel": "alternate"}], "title": "The 'Internet Manifesto' bucks a trend and gets mainstream media attention", "author": "beanheartbatman", "comments": "http://delicious.com/url/aa0718c8a491543fc79a1a01ab1fb3e9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The 'Internet Manifesto' bucks a trend and gets mainstream media attention"}, "link": "http://www.guardian.co.uk/media/pda/2009/sep/08/internet-manifesto-future-journalism", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/aa0718c8a491543fc79a1a01ab1fb3e9", "id": "http://delicious.com/url/aa0718c8a491543fc79a1a01ab1fb3e9#beanheartbatman", "tags": [{"term": "journalism", "scheme": "http://delicious.com/beanheartbatman/", "label": null}, {"term": "internet", "scheme": "http://delicious.com/beanheartbatman/", "label": null}, {"term": "media", "scheme": "http://delicious.com/beanheartbatman/", "label": null}, {"term": "trends", "scheme": "http://delicious.com/beanheartbatman/", "label": null}, {"term": "guardian", "scheme": "http://delicious.com/beanheartbatman/", "label": null}, {"term": "web", "scheme": "http://delicious.com/beanheartbatman/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 08:41:06 +0000", "links": [{"href": "http://www.rosettastone.co.jp/", "type": "text/html", "rel": "alternate"}], "title": "\u30ed\u30bc\u30c3\u30bf\u30b9\u30c8\u30fc\u30f3 | \u82f1\u4f1a\u8a71\u30fb\u82f1\u8a9e\u3092\u306f\u3058\u308131\u8a00\u8a9e\u306e\u5b66\u7fd2\u6559\u6750", "author": "nobnegy", "comments": "http://delicious.com/url/b724766c53b2a62b72a69951f5d9247c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u30ed\u30bc\u30c3\u30bf\u30b9\u30c8\u30fc\u30f3 | \u82f1\u4f1a\u8a71\u30fb\u82f1\u8a9e\u3092\u306f\u3058\u308131\u8a00\u8a9e\u306e\u5b66\u7fd2\u6559\u6750"}, "link": "http://www.rosettastone.co.jp/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b724766c53b2a62b72a69951f5d9247c", "id": "http://delicious.com/url/b724766c53b2a62b72a69951f5d9247c#nobnegy", "tags": [{"term": "English", "scheme": "http://delicious.com/nobnegy/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 23:48:45 +0000", "links": [{"href": "http://www.gimmesomeoven.com/chewy-ginger-molasses-cookies/", "type": "text/html", "rel": "alternate"}], "title": "gimme some oven \u00bb Blog Archive \u00bb chewy ginger molasses cookies", "author": "swansdepot", "comments": "http://delicious.com/url/7724768a5f5b90581cd66d779b2c0765", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "gimme some oven \u00bb Blog Archive \u00bb chewy ginger molasses cookies"}, "link": "http://www.gimmesomeoven.com/chewy-ginger-molasses-cookies/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7724768a5f5b90581cd66d779b2c0765", "id": "http://delicious.com/url/7724768a5f5b90581cd66d779b2c0765#swansdepot", "tags": [{"term": "ginger", "scheme": "http://delicious.com/swansdepot/", "label": null}, {"term": "cookies", "scheme": "http://delicious.com/swansdepot/", "label": null}, {"term": "baking", "scheme": "http://delicious.com/swansdepot/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 14:21:23 +0000", "links": [{"href": "http://www.paymo.biz/free-time-tracking.html", "type": "text/html", "rel": "alternate"}], "title": "Free Time Tracking with Paymo Free - Paymo.biz", "author": "ambicaprakash", "comments": "http://delicious.com/url/bf3990f9179071b24b8094e8e8d3c9e3", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Free Time Tracking with Paymo Free - Paymo.biz"}, "link": "http://www.paymo.biz/free-time-tracking.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/bf3990f9179071b24b8094e8e8d3c9e3", "id": "http://delicious.com/url/bf3990f9179071b24b8094e8e8d3c9e3#ambicaprakash", "tags": [{"term": "time", "scheme": "http://delicious.com/ambicaprakash/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 23:56:42 +0000", "links": [{"href": "http://www.viget.com/extend/conditionally-customize-your-urls-in-rails/", "type": "text/html", "rel": "alternate"}], "title": "Conditionally Customize Your URLs in Rails | Viget Extend", "author": "fauxstor", "comments": "http://delicious.com/url/bf80b615e559114f2726ebdf1cef0619", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Conditionally Customize Your URLs in Rails | Viget Extend"}, "link": "http://www.viget.com/extend/conditionally-customize-your-urls-in-rails/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/bf80b615e559114f2726ebdf1cef0619", "id": "http://delicious.com/url/bf80b615e559114f2726ebdf1cef0619#fauxstor", "tags": [{"term": "rails", "scheme": "http://delicious.com/fauxstor/", "label": null}, {"term": "ruby", "scheme": "http://delicious.com/fauxstor/", "label": null}, {"term": "rubyonrails", "scheme": "http://delicious.com/fauxstor/", "label": null}, {"term": "routes", "scheme": "http://delicious.com/fauxstor/", "label": null}, {"term": "urls", "scheme": "http://delicious.com/fauxstor/", "label": null}, {"term": "url", "scheme": "http://delicious.com/fauxstor/", "label": null}, {"term": "uri", "scheme": "http://delicious.com/fauxstor/", "label": null}, {"term": "usernames", "scheme": "http://delicious.com/fauxstor/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 14:41:00 +0000", "links": [{"href": "http://theresaneil.wordpress.com/2009/01/17/designing-web-interfaces-12-screen-patterns/", "type": "text/html", "rel": "alternate"}], "title": "12 Standard Screen Patterns \u00ab Theresa Neil", "author": "nano_unanue", "comments": "http://delicious.com/url/8a0daa6af379f817ef7103a12481e1f1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "12 Standard Screen Patterns \u00ab Theresa Neil"}, "link": "http://theresaneil.wordpress.com/2009/01/17/designing-web-interfaces-12-screen-patterns/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8a0daa6af379f817ef7103a12481e1f1", "id": "http://delicious.com/url/8a0daa6af379f817ef7103a12481e1f1#nano_unanue", "tags": [{"term": "webdesign", "scheme": "http://delicious.com/nano_unanue/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 13:14:28 +0000", "links": [{"href": "http://iuscommunity.org/wp/", "type": "text/html", "rel": "alternate"}], "title": "IUS Community Project", "author": "mtman97", "comments": "http://delicious.com/url/f5808b2f0a3c4296c82c1991bbf721ab", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "IUS Community Project"}, "link": "http://iuscommunity.org/wp/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f5808b2f0a3c4296c82c1991bbf721ab", "id": "http://delicious.com/url/f5808b2f0a3c4296c82c1991bbf721ab#mtman97", "tags": [{"term": "centos", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "repository", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "server", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "install", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "rackspace", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "linux", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "redhat", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "software", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "rpms", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "rpm", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "yum", "scheme": "http://delicious.com/mtman97/", "label": null}, {"term": "rhel", "scheme": "http://delicious.com/mtman97/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 10:44:54 +0000", "links": [{"href": "http://www.oudebouwmaterialen.nl/", "type": "text/html", "rel": "alternate"}], "title": "OUDE BOUWMATERIALEN Eemnes", "author": "kopo", "comments": "http://delicious.com/url/523e8ac8cdd98e47e64a6180b8f6338f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "OUDE BOUWMATERIALEN Eemnes"}, "link": "http://www.oudebouwmaterialen.nl/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/523e8ac8cdd98e47e64a6180b8f6338f", "id": "http://delicious.com/url/523e8ac8cdd98e47e64a6180b8f6338f#kopo", "tags": [{"term": "vloer", "scheme": "http://delicious.com/kopo/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 15:26:46 +0000", "links": [{"href": "http://www.afterthedeadline.com/", "type": "text/html", "rel": "alternate"}], "title": "After the Deadline - Check Spelling, Style, and Grammar in WordPress and TinyMCE", "author": "math89", "comments": "http://delicious.com/url/efe2f37a6fd742cfbe6dbebc0da52dbc", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "After the Deadline - Check Spelling, Style, and Grammar in WordPress and TinyMCE"}, "link": "http://www.afterthedeadline.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/efe2f37a6fd742cfbe6dbebc0da52dbc", "id": "http://delicious.com/url/efe2f37a6fd742cfbe6dbebc0da52dbc#math89", "tags": [{"term": "orthographe", "scheme": "http://delicious.com/math89/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 16:52:50 +0000", "links": [{"href": "http://favstar.fm/", "type": "text/html", "rel": "alternate"}], "title": "Recent tweets with 100 favs or more", "author": "dannywahlquist", "comments": "http://delicious.com/url/de58f9e6f20883cd4acaaaaaeeca79fa", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Recent tweets with 100 favs or more"}, "link": "http://favstar.fm/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/de58f9e6f20883cd4acaaaaaeeca79fa", "id": "http://delicious.com/url/de58f9e6f20883cd4acaaaaaeeca79fa#dannywahlquist", "tags": [{"term": "favorite", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "tweets", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "web", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "tools", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "search", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "social", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "community", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "aggregator", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "internet", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "tool", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "bookmarks", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "Statistics", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "twitter", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "Analytics", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "ranking", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "socialmedia", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "twittertools", "scheme": "http://delicious.com/dannywahlquist/", "label": null}, {"term": "webservice", "scheme": "http://delicious.com/dannywahlquist/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 14:34:40 +0000", "links": [{"href": "http://www.jwz.org/", "type": "text/html", "rel": "alternate"}], "title": "jwz.org", "author": "JoeFromWI", "comments": "http://delicious.com/url/27535ecb798e0511711ff5142f604158", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "jwz.org"}, "link": "http://www.jwz.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/27535ecb798e0511711ff5142f604158", "id": "http://delicious.com/url/27535ecb798e0511711ff5142f604158#JoeFromWI", "tags": [{"term": "weird", "scheme": "http://delicious.com/JoeFromWI/", "label": null}, {"term": "bizarre", "scheme": "http://delicious.com/JoeFromWI/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 00:19:30 +0000", "links": [{"href": "http://www.thefedoralounge.com/", "type": "text/html", "rel": "alternate"}], "title": "The Fedora Lounge - Powered by vBulletin", "author": "georgeandre", "comments": "http://delicious.com/url/86a9d50138b328364cd41d30f682888f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Fedora Lounge - Powered by vBulletin"}, "link": "http://www.thefedoralounge.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/86a9d50138b328364cd41d30f682888f", "id": "http://delicious.com/url/86a9d50138b328364cd41d30f682888f#georgeandre", "tags": [{"term": "fashion", "scheme": "http://delicious.com/georgeandre/", "label": null}, {"term": "vintage", "scheme": "http://delicious.com/georgeandre/", "label": null}, {"term": "retro", "scheme": "http://delicious.com/georgeandre/", "label": null}, {"term": "clothing", "scheme": "http://delicious.com/georgeandre/", "label": null}, {"term": "forum", "scheme": "http://delicious.com/georgeandre/", "label": null}, {"term": "style", "scheme": "http://delicious.com/georgeandre/", "label": null}, {"term": "clothes", "scheme": "http://delicious.com/georgeandre/", "label": null}, {"term": "men", "scheme": "http://delicious.com/georgeandre/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 20:30:21 +0000", "links": [{"href": "http://code.google.com/p/ocrfeeder/", "type": "text/html", "rel": "alternate"}], "title": "ocrfeeder - Project Hosting on Google Code", "author": "kir.fesenko", "comments": "http://delicious.com/url/15c0cefac0dac0fcc01dbcf0eeb6191c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "ocrfeeder - Project Hosting on Google Code"}, "link": "http://code.google.com/p/ocrfeeder/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/15c0cefac0dac0fcc01dbcf0eeb6191c", "id": "http://delicious.com/url/15c0cefac0dac0fcc01dbcf0eeb6191c#kir.fesenko", "tags": [{"term": "ocrfeeder", "scheme": "http://delicious.com/kir.fesenko/", "label": null}, {"term": "ocr--zoning", "scheme": "http://delicious.com/kir.fesenko/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 02:03:29 +0000", "links": [{"href": "http://www.todaysbigthing.com/2009/09/08", "type": "text/html", "rel": "alternate"}], "title": "Puppy Can't Roll Back Over is Today's BIG Thing - SEP 08, 2009", "author": "rugner", "comments": "http://delicious.com/url/fff60a6c4573233263e0c19fd5baef8b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Puppy Can't Roll Back Over is Today's BIG Thing - SEP 08, 2009"}, "link": "http://www.todaysbigthing.com/2009/09/08", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/fff60a6c4573233263e0c19fd5baef8b", "id": "http://delicious.com/url/fff60a6c4573233263e0c19fd5baef8b#rugner", "tags": [{"term": "cute", "scheme": "http://delicious.com/rugner/", "label": null}, {"term": "kyla", "scheme": "http://delicious.com/rugner/", "label": null}, {"term": "animal", "scheme": "http://delicious.com/rugner/", "label": null}, {"term": "funny", "scheme": "http://delicious.com/rugner/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 08:11:19 +0000", "links": [{"href": "http://sptnk.org/", "type": "text/html", "rel": "alternate"}], "title": "Sputnik Observatory For the Study of Contemporary Culture", "author": "omehex", "comments": "http://delicious.com/url/feab3498272591a6f6093eb976bf7ad3", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Sputnik Observatory For the Study of Contemporary Culture"}, "link": "http://sptnk.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/feab3498272591a6f6093eb976bf7ad3", "id": "http://delicious.com/url/feab3498272591a6f6093eb976bf7ad3#omehex", "tags": [{"term": "inspiration", "scheme": "http://delicious.com/omehex/", "label": null}, {"term": "technology", "scheme": "http://delicious.com/omehex/", "label": null}, {"term": "video", "scheme": "http://delicious.com/omehex/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 02:36:36 +0000", "links": [{"href": "http://www.thetickletrunk.com/categories.php?id=63&name=Containers", "type": "text/html", "rel": "alternate"}], "title": "Containers - The Tickle Trunk", "author": "JoeAlbano", "comments": "http://delicious.com/url/37e83224519df99f764ca1dc7f342c1f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Containers - The Tickle Trunk"}, "link": "http://www.thetickletrunk.com/categories.php?id=63&name=Containers", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/37e83224519df99f764ca1dc7f342c1f", "id": "http://delicious.com/url/37e83224519df99f764ca1dc7f342c1f#JoeAlbano", "tags": [{"term": "HomeResources", "scheme": "http://delicious.com/JoeAlbano/", "label": null}, {"term": "Containers", "scheme": "http://delicious.com/JoeAlbano/", "label": null}, {"term": "Storage", "scheme": "http://delicious.com/JoeAlbano/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 23:49:38 +0000", "links": [{"href": "http://suicidegirls.com/members/Missy/albums/site/13576/", "type": "text/html", "rel": "alternate"}], "title": "Missy Fight Club Photos", "author": "minitoka", "comments": "http://delicious.com/url/808f438197d8379be5b996d5d2abd6f8", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Missy Fight Club Photos"}, "link": "http://suicidegirls.com/members/Missy/albums/site/13576/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/808f438197d8379be5b996d5d2abd6f8", "id": "http://delicious.com/url/808f438197d8379be5b996d5d2abd6f8#minitoka"}, +{"updated": "Mon, 07 Sep 2009 19:48:47 +0000", "links": [{"href": "http://www.freshwap.net/forums/music/", "type": "text/html", "rel": "alternate"}], "title": "Music - FreshWap Forums", "author": "apometron", "comments": "http://delicious.com/url/eba92c4bebcc774c11951791bced0c25", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Music - FreshWap Forums"}, "link": "http://www.freshwap.net/forums/music/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/eba92c4bebcc774c11951791bced0c25", "id": "http://delicious.com/url/eba92c4bebcc774c11951791bced0c25#apometron", "tags": [{"term": "rapidshare", "scheme": "http://delicious.com/apometron/", "label": null}, {"term": "fileshare", "scheme": "http://delicious.com/apometron/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 21:20:58 +0000", "links": [{"href": "http://www.heise.de/software/download/special/backup_mit_robocopy/17_6", "type": "text/html", "rel": "alternate"}], "title": "Backup mit Robocopy, Themen-Special im heise Software-Verzeichnis", "author": "markar", "comments": "http://delicious.com/url/e013d08bd9309310add067f966ba1f0d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Backup mit Robocopy, Themen-Special im heise Software-Verzeichnis"}, "link": "http://www.heise.de/software/download/special/backup_mit_robocopy/17_6", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e013d08bd9309310add067f966ba1f0d", "id": "http://delicious.com/url/e013d08bd9309310add067f966ba1f0d#markar", "tags": [{"term": "robocopy", "scheme": "http://delicious.com/markar/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 19:28:01 +0000", "links": [{"href": "http://www.customhorror.net/", "type": "text/html", "rel": "alternate"}], "title": "Custom Horror - A new monster every day | Get today's monster Team Love as a pdf or an EPS | Watching: Team Love", "author": "KiddoMono", "comments": "http://delicious.com/url/e1161a4204605c2b5d20a904a4c6319f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Custom Horror - A new monster every day | Get today's monster Team Love as a pdf or an EPS | Watching: Team Love"}, "link": "http://www.customhorror.net/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e1161a4204605c2b5d20a904a4c6319f", "id": "http://delicious.com/url/e1161a4204605c2b5d20a904a4c6319f#KiddoMono", "tags": [{"term": "illustration", "scheme": "http://delicious.com/KiddoMono/", "label": null}, {"term": "blog", "scheme": "http://delicious.com/KiddoMono/", "label": null}, {"term": "giztag", "scheme": "http://delicious.com/KiddoMono/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 11:59:59 +0000", "links": [{"href": "http://briks.si/referenca/", "type": "text/html", "rel": "alternate"}], "title": "briks - customers", "author": "fergusburns", "comments": "http://delicious.com/url/5a8b3032323298cb38250408a7d42bee", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "briks - customers"}, "link": "http://briks.si/referenca/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5a8b3032323298cb38250408a7d42bee", "id": "http://delicious.com/url/5a8b3032323298cb38250408a7d42bee#fergusburns", "tags": [{"term": "firefox", "scheme": "http://delicious.com/fergusburns/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 07:49:43 +0000", "links": [{"href": "http://wilderdom.com/games/Icebreakers.html", "type": "text/html", "rel": "alternate"}], "title": "Icebreakers, Warmups, Energerizers, & Deinhibitizers: Activities for getting groups going", "author": "ecwelch", "comments": "http://delicious.com/url/11702cc9ba6066c68541bf403b3e8ed6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Icebreakers, Warmups, Energerizers, & Deinhibitizers: Activities for getting groups going"}, "link": "http://wilderdom.com/games/Icebreakers.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/11702cc9ba6066c68541bf403b3e8ed6", "id": "http://delicious.com/url/11702cc9ba6066c68541bf403b3e8ed6#ecwelch"}, +{"updated": "Tue, 08 Sep 2009 13:18:06 +0000", "links": [{"href": "http://citationmachine.net/", "type": "text/html", "rel": "alternate"}], "title": "Son of Citation Machine", "author": "ROBBYS_THE_BOSS", "comments": "http://delicious.com/url/b5c4ffc8d32d86c033e04986f57bc326", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Son of Citation Machine"}, "link": "http://citationmachine.net/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b5c4ffc8d32d86c033e04986f57bc326", "id": "http://delicious.com/url/b5c4ffc8d32d86c033e04986f57bc326#ROBBYS_THE_BOSS", "tags": [{"term": "research", "scheme": "http://delicious.com/ROBBYS_THE_BOSS/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 23:11:53 +0000", "links": [{"href": "http://blog.livedoor.jp/dankogai/archives/51284061.html", "type": "text/html", "rel": "alternate"}], "title": "\u66f8\u8a55 - \u541b\u306e\u6210\u7e3e\u3092\u3050\u3093\u3050\u3093\u4f38\u3070\u30597\u3064\u306e\u5fc3\u306e\u3064\u304f\u308a\u65b9", "author": "tanda", "comments": "http://delicious.com/url/b851754595db31eb857eb2929af16b72", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u66f8\u8a55 - \u541b\u306e\u6210\u7e3e\u3092\u3050\u3093\u3050\u3093\u4f38\u3070\u30597\u3064\u306e\u5fc3\u306e\u3064\u304f\u308a\u65b9"}, "link": "http://blog.livedoor.jp/dankogai/archives/51284061.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b851754595db31eb857eb2929af16b72", "id": "http://delicious.com/url/b851754595db31eb857eb2929af16b72#tanda", "tags": [{"term": "archive", "scheme": "http://delicious.com/tanda/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 13:28:11 +0000", "links": [{"href": "http://www.informit.com/articles/article.aspx?p=1390173", "type": "text/html", "rel": "alternate"}], "title": "InformIT: How Not To Optimize > A Load of Shift", "author": "rslima", "comments": "http://delicious.com/url/b7a08abcff402bf4a9faedd32e09fb92", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "InformIT: How Not To Optimize > A Load of Shift"}, "link": "http://www.informit.com/articles/article.aspx?p=1390173", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b7a08abcff402bf4a9faedd32e09fb92", "id": "http://delicious.com/url/b7a08abcff402bf4a9faedd32e09fb92#rslima", "tags": [{"term": "optimization", "scheme": "http://delicious.com/rslima/", "label": null}, {"term": "article", "scheme": "http://delicious.com/rslima/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 04:25:21 +0000", "links": [{"href": "http://www.sheppardsoftware.com/Geography.htm", "type": "text/html", "rel": "alternate"}], "title": "World Maps - geography online games", "author": "Mrs.Berman", "comments": "http://delicious.com/url/2358823c07ea91f5795466013483c38b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "World Maps - geography online games"}, "link": "http://www.sheppardsoftware.com/Geography.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2358823c07ea91f5795466013483c38b", "id": "http://delicious.com/url/2358823c07ea91f5795466013483c38b#Mrs.Berman", "tags": [{"term": "geography", "scheme": "http://delicious.com/Mrs.Berman/", "label": null}, {"term": "socialstudies", "scheme": "http://delicious.com/Mrs.Berman/", "label": null}, {"term": "team5", "scheme": "http://delicious.com/Mrs.Berman/", "label": null}, {"term": "states", "scheme": "http://delicious.com/Mrs.Berman/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 06:34:33 +0000", "links": [{"href": "http://www.wikihow.com/Calculate-the-Value-of-Scrap-Gold", "type": "text/html", "rel": "alternate"}], "title": "How to Calculate the Value of Scrap Gold - wikiHow", "author": "Josesjr2", "comments": "http://delicious.com/url/3e44b7e536dc500313ee706dc6f013ba", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "How to Calculate the Value of Scrap Gold - wikiHow"}, "link": "http://www.wikihow.com/Calculate-the-Value-of-Scrap-Gold", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3e44b7e536dc500313ee706dc6f013ba", "id": "http://delicious.com/url/3e44b7e536dc500313ee706dc6f013ba#Josesjr2", "tags": [{"term": "gold", "scheme": "http://delicious.com/Josesjr2/", "label": null}, {"term": "calculator", "scheme": "http://delicious.com/Josesjr2/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 05:25:17 +0000", "links": [{"href": "http://maketecheasier.com/8-useful-and-interesting-bash-prompts/2009/09/04", "type": "text/html", "rel": "alternate"}], "title": "8 Useful and Interesting Bash Prompts \u2013 Make Tech Easier", "author": "e_liming", "comments": "http://delicious.com/url/0fb05e51057ee53d706f37dcd510abf5", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "8 Useful and Interesting Bash Prompts \u2013 Make Tech Easier"}, "link": "http://maketecheasier.com/8-useful-and-interesting-bash-prompts/2009/09/04", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0fb05e51057ee53d706f37dcd510abf5", "id": "http://delicious.com/url/0fb05e51057ee53d706f37dcd510abf5#e_liming", "tags": [{"term": "prompt", "scheme": "http://delicious.com/e_liming/", "label": null}, {"term": "terminal", "scheme": "http://delicious.com/e_liming/", "label": null}, {"term": "cli", "scheme": "http://delicious.com/e_liming/", "label": null}, {"term": "command", "scheme": "http://delicious.com/e_liming/", "label": null}, {"term": "shell", "scheme": "http://delicious.com/e_liming/", "label": null}, {"term": "color", "scheme": "http://delicious.com/e_liming/", "label": null}, {"term": "linux", "scheme": "http://delicious.com/e_liming/", "label": null}, {"term": "unix", "scheme": "http://delicious.com/e_liming/", "label": null}, {"term": "sysadmin", "scheme": "http://delicious.com/e_liming/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 02:26:26 +0000", "links": [{"href": "http://www.thekitchn.com/", "type": "text/html", "rel": "alternate"}], "title": "Apartment Therapy The Kitchn", "author": "jennykvh", "comments": "http://delicious.com/url/f071651a495441afef93a793b2ad7315", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Apartment Therapy The Kitchn"}, "link": "http://www.thekitchn.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f071651a495441afef93a793b2ad7315", "id": "http://delicious.com/url/f071651a495441afef93a793b2ad7315#jennykvh", "tags": [{"term": "blogs", "scheme": "http://delicious.com/jennykvh/", "label": null}, {"term": "home", "scheme": "http://delicious.com/jennykvh/", "label": null}, {"term": "design", "scheme": "http://delicious.com/jennykvh/", "label": null}, {"term": "recipes", "scheme": "http://delicious.com/jennykvh/", "label": null}, {"term": "food", "scheme": "http://delicious.com/jennykvh/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 18:15:27 +0000", "links": [{"href": "http://ubik.com.au/article/named/implementing_the_specification_pattern_with_linq", "type": "text/html", "rel": "alternate"}], "title": "Ubik Systems | Delivering exceptional software", "author": "vrsathya", "comments": "http://delicious.com/url/86612f1a4970afc9f76175578ad2e371", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Ubik Systems | Delivering exceptional software"}, "link": "http://ubik.com.au/article/named/implementing_the_specification_pattern_with_linq", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/86612f1a4970afc9f76175578ad2e371", "id": "http://delicious.com/url/86612f1a4970afc9f76175578ad2e371#vrsathya", "tags": [{"term": "ddd", "scheme": "http://delicious.com/vrsathya/", "label": null}, {"term": "patterns", "scheme": "http://delicious.com/vrsathya/", "label": null}, {"term": "linq", "scheme": "http://delicious.com/vrsathya/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 11:07:17 +0000", "links": [{"href": "http://www.fornobravo.com/pompeii_oven/oven_overview.html", "type": "text/html", "rel": "alternate"}], "title": "Brick Oven Overview and Basics", "author": "KingMonkey", "comments": "http://delicious.com/url/5335688e9d4db79a76d51a3f3bcb4b81", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Brick Oven Overview and Basics"}, "link": "http://www.fornobravo.com/pompeii_oven/oven_overview.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5335688e9d4db79a76d51a3f3bcb4b81", "id": "http://delicious.com/url/5335688e9d4db79a76d51a3f3bcb4b81#KingMonkey", "tags": [{"term": "pizza", "scheme": "http://delicious.com/KingMonkey/", "label": null}, {"term": "oven", "scheme": "http://delicious.com/KingMonkey/", "label": null}, {"term": "food", "scheme": "http://delicious.com/KingMonkey/", "label": null}, {"term": "home", "scheme": "http://delicious.com/KingMonkey/", "label": null}, {"term": "howto", "scheme": "http://delicious.com/KingMonkey/", "label": null}, {"term": "diy", "scheme": "http://delicious.com/KingMonkey/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 05:46:07 +0000", "links": [{"href": "http://sixrevisions.com/javascript/20-fresh-jquery-plugins-to-enhance-your-user-interface/", "type": "text/html", "rel": "alternate"}], "title": "20 Fresh jQuery Plugins to Enhance your User Interface", "author": "DKDC", "comments": "http://delicious.com/url/1799613768bebedcf65c4892ae2619de", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "20 Fresh jQuery Plugins to Enhance your User Interface"}, "link": "http://sixrevisions.com/javascript/20-fresh-jquery-plugins-to-enhance-your-user-interface/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1799613768bebedcf65c4892ae2619de", "id": "http://delicious.com/url/1799613768bebedcf65c4892ae2619de#DKDC", "tags": [{"term": "jquery", "scheme": "http://delicious.com/DKDC/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 06:40:20 +0000", "links": [{"href": "http://clearleft.com/", "type": "text/html", "rel": "alternate"}], "title": "User experience and web design consultants | Clearleft Ltd.", "author": "petah", "comments": "http://delicious.com/url/42a3dbddd3c661f6f4225821c9a67be1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "User experience and web design consultants | Clearleft Ltd."}, "link": "http://clearleft.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/42a3dbddd3c661f6f4225821c9a67be1", "id": "http://delicious.com/url/42a3dbddd3c661f6f4225821c9a67be1#petah", "tags": [{"term": "webdesign", "scheme": "http://delicious.com/petah/", "label": null}, {"term": "inspiration", "scheme": "http://delicious.com/petah/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 17:27:07 +0000", "links": [{"href": "http://www.webdesignref.com/examples/textex.htm", "type": "text/html", "rel": "alternate"}], "title": "Web Design: TCR - Design Demos: Text: Serif vs. San-Serif", "author": "klentel", "comments": "http://delicious.com/url/5780725b9388c9459d4c0f3484e2c707", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Web Design: TCR - Design Demos: Text: Serif vs. San-Serif"}, "link": "http://www.webdesignref.com/examples/textex.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5780725b9388c9459d4c0f3484e2c707", "id": "http://delicious.com/url/5780725b9388c9459d4c0f3484e2c707#klentel", "tags": [{"term": "TextFontResources", "scheme": "http://delicious.com/klentel/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 19:05:44 +0000", "links": [{"href": "http://workingwikily.net/", "type": "text/html", "rel": "alternate"}], "title": "Working Wikily", "author": "khazana", "comments": "http://delicious.com/url/c097dd924d5b67990cafc7283168e647", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Working Wikily"}, "link": "http://workingwikily.net/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c097dd924d5b67990cafc7283168e647", "id": "http://delicious.com/url/c097dd924d5b67990cafc7283168e647#khazana", "tags": [{"term": "innovation", "scheme": "http://delicious.com/khazana/", "label": null}, {"term": "web2.0", "scheme": "http://delicious.com/khazana/", "label": null}, {"term": "socialmedia", "scheme": "http://delicious.com/khazana/", "label": null}, {"term": "wiki", "scheme": "http://delicious.com/khazana/", "label": null}, {"term": "networks", "scheme": "http://delicious.com/khazana/", "label": null}, {"term": "monitorinstitute", "scheme": "http://delicious.com/khazana/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 01:13:42 +0000", "links": [{"href": "http://www.digidna.net/diskaid/", "type": "text/html", "rel": "alternate"}], "title": "DiskAid - USB Flash Drive for iPhone & iPod Touch. Transfer Files betwen your Computer and your iPhone or iPod Touch.", "author": "plm_7609", "comments": "http://delicious.com/url/7c439a0d18e35afdd3fc7e1984e0a2e2", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "DiskAid - USB Flash Drive for iPhone & iPod Touch. Transfer Files betwen your Computer and your iPhone or iPod Touch."}, "link": "http://www.digidna.net/diskaid/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7c439a0d18e35afdd3fc7e1984e0a2e2", "id": "http://delicious.com/url/7c439a0d18e35afdd3fc7e1984e0a2e2#plm_7609", "tags": [{"term": "iphone", "scheme": "http://delicious.com/plm_7609/", "label": null}, {"term": "Software", "scheme": "http://delicious.com/plm_7609/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 07:59:48 +0000", "links": [{"href": "http://makingitlovely.com/2008/10/16/faq-product-photography-part-two/", "type": "text/html", "rel": "alternate"}], "title": "Making it Lovely | Transforming the so-so. \u00bb Blog Archive \u00bb FAQ: Product Photography (Part Two)", "author": "lulamaegirl", "comments": "http://delicious.com/url/85bc923d09d4d6c4ef36b0096637a795", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Making it Lovely | Transforming the so-so. \u00bb Blog Archive \u00bb FAQ: Product Photography (Part Two)"}, "link": "http://makingitlovely.com/2008/10/16/faq-product-photography-part-two/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/85bc923d09d4d6c4ef36b0096637a795", "id": "http://delicious.com/url/85bc923d09d4d6c4ef36b0096637a795#lulamaegirl", "tags": [{"term": "etsy", "scheme": "http://delicious.com/lulamaegirl/", "label": null}, {"term": "photography", "scheme": "http://delicious.com/lulamaegirl/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 18:31:23 +0000", "links": [{"href": "http://oleproject.org/overview/", "type": "text/html", "rel": "alternate"}], "title": "The OLE Project | Overview", "author": "grasberger", "comments": "http://delicious.com/url/8f40d4d5273388d183bc7134ef23926b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The OLE Project | Overview"}, "link": "http://oleproject.org/overview/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8f40d4d5273388d183bc7134ef23926b", "id": "http://delicious.com/url/8f40d4d5273388d183bc7134ef23926b#grasberger", "tags": [{"term": "future_libraries", "scheme": "http://delicious.com/grasberger/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 06:29:09 +0000", "links": [{"href": "http://thinkdesignblog.com/wordpress-lorem-ipsum-test-post-pack.htm", "type": "text/html", "rel": "alternate"}], "title": "Think Design Blog - Wordpress - Lorem Ipsum Test Post Pack | Think Design", "author": "willy.ten", "comments": "http://delicious.com/url/67c9e7312d139003ab48fa7b303a3c6c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Think Design Blog - Wordpress - Lorem Ipsum Test Post Pack | Think Design"}, "link": "http://thinkdesignblog.com/wordpress-lorem-ipsum-test-post-pack.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/67c9e7312d139003ab48fa7b303a3c6c", "id": "http://delicious.com/url/67c9e7312d139003ab48fa7b303a3c6c#willy.ten", "tags": [{"term": "wordpress", "scheme": "http://delicious.com/willy.ten/", "label": null}, {"term": "webdev", "scheme": "http://delicious.com/willy.ten/", "label": null}, {"term": "ressources", "scheme": "http://delicious.com/willy.ten/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 21:39:05 +0000", "links": [{"href": "http://allrecipes.com/default.aspx", "type": "text/html", "rel": "alternate"}], "title": "All recipes \u2013 complete resource for recipes and cooking tips", "author": "stantoni", "comments": "http://delicious.com/url/9de825c6396bfe9ac5351fa3a524796f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "All recipes \u2013 complete resource for recipes and cooking tips"}, "link": "http://allrecipes.com/default.aspx", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9de825c6396bfe9ac5351fa3a524796f", "id": "http://delicious.com/url/9de825c6396bfe9ac5351fa3a524796f#stantoni", "tags": [{"term": "food", "scheme": "http://delicious.com/stantoni/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 08:10:48 +0000", "links": [{"href": "http://infosthetics.com/", "type": "text/html", "rel": "alternate"}], "title": "information aesthetics - Information Visualization & Visual Communication", "author": "jvdongen", "comments": "http://delicious.com/url/e7838206da90d7a0d74c965831b7d508", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "information aesthetics - Information Visualization & Visual Communication"}, "link": "http://infosthetics.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e7838206da90d7a0d74c965831b7d508", "id": "http://delicious.com/url/e7838206da90d7a0d74c965831b7d508#jvdongen", "tags": [{"term": "visualization", "scheme": "http://delicious.com/jvdongen/", "label": null}, {"term": "graphics", "scheme": "http://delicious.com/jvdongen/", "label": null}, {"term": "statistics", "scheme": "http://delicious.com/jvdongen/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 19:17:34 +0000", "links": [{"href": "http://happydays.blogs.nytimes.com/2009/09/10/views-of-a-day/", "type": "text/html", "rel": "alternate"}], "title": "Views of a Day - Happy Days Blog - NYTimes.com", "author": "tq815", "comments": "http://delicious.com/url/63b56965a1f0d172b9bea17ef44f6e57", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Views of a Day - Happy Days Blog - NYTimes.com"}, "link": "http://happydays.blogs.nytimes.com/2009/09/10/views-of-a-day/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/63b56965a1f0d172b9bea17ef44f6e57", "id": "http://delicious.com/url/63b56965a1f0d172b9bea17ef44f6e57#tq815", "tags": [{"term": "9/11", "scheme": "http://delicious.com/tq815/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 12:12:11 +0000", "links": [{"href": "http://www.siteground.com/tutorials/mediawiki/mediawiki_sections_categories.htm", "type": "text/html", "rel": "alternate"}], "title": "MediaWiki Tutorial: Mediawiki Documentation: Sections and categories in MediaWiki", "author": "ealvarezr", "comments": "http://delicious.com/url/69bcbf3bc5ffa480ed5cf66cae66ddd1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "MediaWiki Tutorial: Mediawiki Documentation: Sections and categories in MediaWiki"}, "link": "http://www.siteground.com/tutorials/mediawiki/mediawiki_sections_categories.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/69bcbf3bc5ffa480ed5cf66cae66ddd1", "id": "http://delicious.com/url/69bcbf3bc5ffa480ed5cf66cae66ddd1#ealvarezr", "tags": [{"term": "mediawiki", "scheme": "http://delicious.com/ealvarezr/", "label": null}, {"term": "wiki", "scheme": "http://delicious.com/ealvarezr/", "label": null}, {"term": "tutorial", "scheme": "http://delicious.com/ealvarezr/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 20:11:34 +0000", "links": [{"href": "http://fmylife.com/", "type": "text/html", "rel": "alternate"}], "title": "FML: Your everyday life stories", "author": "justLOG", "comments": "http://delicious.com/url/09786b21fddcb79a203ea27f99208814", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "FML: Your everyday life stories"}, "link": "http://fmylife.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/09786b21fddcb79a203ea27f99208814", "id": "http://delicious.com/url/09786b21fddcb79a203ea27f99208814#justLOG", "tags": [{"term": "fail", "scheme": "http://delicious.com/justLOG/", "label": null}, {"term": "fml", "scheme": "http://delicious.com/justLOG/", "label": null}, {"term": "eff", "scheme": "http://delicious.com/justLOG/", "label": null}, {"term": "my", "scheme": "http://delicious.com/justLOG/", "label": null}, {"term": "life", "scheme": "http://delicious.com/justLOG/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 14:00:30 +0000", "links": [{"href": "http://artsytime.com/parents-of-the-year/", "type": "text/html", "rel": "alternate"}], "title": "Parents of the year - Artsy Time", "author": "supersirj", "comments": "http://delicious.com/url/c137294c5c56475e5077e1b26578e8d1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Parents of the year - Artsy Time"}, "link": "http://artsytime.com/parents-of-the-year/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c137294c5c56475e5077e1b26578e8d1", "id": "http://delicious.com/url/c137294c5c56475e5077e1b26578e8d1#supersirj", "tags": [{"term": "funny", "scheme": "http://delicious.com/supersirj/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 02:58:44 +0000", "links": [{"href": "http://tw.rpi.edu/portal/Social_Semantic_Web:_Where_Web_2.0_Meets_Web_3.0/program", "type": "text/html", "rel": "alternate"}], "title": "Social Semantic Web: Where Web 2.0 Meets Web 3.0/program - Semantic Portal Wiki", "author": "jack.daniel681", "comments": "http://delicious.com/url/047daaeeb31e8c71dab686289d2b4dc9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Social Semantic Web: Where Web 2.0 Meets Web 3.0/program - Semantic Portal Wiki"}, "link": "http://tw.rpi.edu/portal/Social_Semantic_Web:_Where_Web_2.0_Meets_Web_3.0/program", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/047daaeeb31e8c71dab686289d2b4dc9", "id": "http://delicious.com/url/047daaeeb31e8c71dab686289d2b4dc9#jack.daniel681", "tags": [{"term": "sioc", "scheme": "http://delicious.com/jack.daniel681/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 14:24:22 +0000", "links": [{"href": "http://www.coverdude.com/", "type": "text/html", "rel": "alternate"}], "title": "CoverDude- Fake Magazine Covers,Make a Fake Magazine Cover with photo", "author": "chethong", "comments": "http://delicious.com/url/746e4eff0ab77996219bc57b0f02fac7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "CoverDude- Fake Magazine Covers,Make a Fake Magazine Cover with photo"}, "link": "http://www.coverdude.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/746e4eff0ab77996219bc57b0f02fac7", "id": "http://delicious.com/url/746e4eff0ab77996219bc57b0f02fac7#chethong", "tags": [{"term": "fun", "scheme": "http://delicious.com/chethong/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 15:28:08 +0000", "links": [{"href": "http://tv.winelibrary.com/", "type": "text/html", "rel": "alternate"}], "title": "Wine Library TV: Gary Vaynerchuk's daily wine video blog", "author": "pbaker11", "comments": "http://delicious.com/url/00e124783cc93c2bac194a750dd284f1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Wine Library TV: Gary Vaynerchuk's daily wine video blog"}, "link": "http://tv.winelibrary.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/00e124783cc93c2bac194a750dd284f1", "id": "http://delicious.com/url/00e124783cc93c2bac194a750dd284f1#pbaker11", "tags": [{"term": "wine", "scheme": "http://delicious.com/pbaker11/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 09:13:37 +0000", "links": [{"href": "http://www.kartoo.com/", "type": "text/html", "rel": "alternate"}], "title": "KartOO : The First Interface Mapping Metasearch Engine", "author": "alessiacapuzzo", "comments": "http://delicious.com/url/420519771d651dc0b8ca2a8222e6c035", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "KartOO : The First Interface Mapping Metasearch Engine"}, "link": "http://www.kartoo.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/420519771d651dc0b8ca2a8222e6c035", "id": "http://delicious.com/url/420519771d651dc0b8ca2a8222e6c035#alessiacapuzzo", "tags": [{"term": "internet", "scheme": "http://delicious.com/alessiacapuzzo/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 12:44:08 +0000", "links": [{"href": "http://developer.yahoo.com/ypatterns/", "type": "text/html", "rel": "alternate"}], "title": "Yahoo! Design Pattern Library", "author": "dcelli", "comments": "http://delicious.com/url/6574cc7db5b317262e63f5d9da32c1fb", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Yahoo! Design Pattern Library"}, "link": "http://developer.yahoo.com/ypatterns/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6574cc7db5b317262e63f5d9da32c1fb", "id": "http://delicious.com/url/6574cc7db5b317262e63f5d9da32c1fb#dcelli", "tags": [{"term": "web", "scheme": "http://delicious.com/dcelli/", "label": null}, {"term": "design", "scheme": "http://delicious.com/dcelli/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 10:35:02 +0000", "links": [{"href": "http://www.piratenpartei.de/", "type": "text/html", "rel": "alternate"}], "title": "Piratenpartei Deutschland | Piraten bei der Bundestagswahl 2009 w\u00e4hlen!", "author": "luluivre", "comments": "http://delicious.com/url/c1d2635eaf0e286d9f2f78523a926e12", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Piratenpartei Deutschland | Piraten bei der Bundestagswahl 2009 w\u00e4hlen!"}, "link": "http://www.piratenpartei.de/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c1d2635eaf0e286d9f2f78523a926e12", "id": "http://delicious.com/url/c1d2635eaf0e286d9f2f78523a926e12#luluivre", "tags": [{"term": "blog", "scheme": "http://delicious.com/luluivre/", "label": null}, {"term": "politics", "scheme": "http://delicious.com/luluivre/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 13:52:03 +0000", "enclosures": [{"length": "1", "href": "http://content.ytmnd.com/content/a/0/8/a0812774c98903448246c7e213bc1fea.gif", "type": "image/gif"}], "links": [{"href": "http://content.ytmnd.com/content/a/0/8/a0812774c98903448246c7e213bc1fea.gif", "type": "text/html", "rel": "alternate"}], "title": "the future", "author": "amila_sajith", "comments": "http://delicious.com/url/ff123d0898c17b3094fb3358b28d11b8", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "the future"}, "link": "http://content.ytmnd.com/content/a/0/8/a0812774c98903448246c7e213bc1fea.gif", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ff123d0898c17b3094fb3358b28d11b8", "id": "http://delicious.com/url/ff123d0898c17b3094fb3358b28d11b8#amila_sajith", "tags": [{"term": "astronomy", "scheme": "http://delicious.com/amila_sajith/", "label": null}, {"term": "future", "scheme": "http://delicious.com/amila_sajith/", "label": null}, {"term": "science", "scheme": "http://delicious.com/amila_sajith/", "label": null}, {"term": "earth", "scheme": "http://delicious.com/amila_sajith/", "label": null}, {"term": "interesting", "scheme": "http://delicious.com/amila_sajith/", "label": null}, {"term": "system:filetype:gif", "scheme": "http://delicious.com/amila_sajith/", "label": null}, {"term": "system:media:image", "scheme": "http://delicious.com/amila_sajith/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 19:35:27 +0000", "links": [{"href": "https://www.adpselect.com/login/", "type": "text/html", "rel": "alternate"}], "title": "ADP Screening and Selection Services", "author": "terryweaver111", "comments": "http://delicious.com/url/26d1644bab133d352abea95bf5a9ff7e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "ADP Screening and Selection Services"}, "link": "https://www.adpselect.com/login/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/26d1644bab133d352abea95bf5a9ff7e", "id": "http://delicious.com/url/26d1644bab133d352abea95bf5a9ff7e#terryweaver111", "tags": [{"term": "ADP", "scheme": "http://delicious.com/terryweaver111/", "label": null}, {"term": "new", "scheme": "http://delicious.com/terryweaver111/", "label": null}, {"term": "hire", "scheme": "http://delicious.com/terryweaver111/", "label": null}, {"term": "employee", "scheme": "http://delicious.com/terryweaver111/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 01:00:11 +0000", "links": [{"href": "http://scijinks.jpl.nasa.gov/weather/", "type": "text/html", "rel": "alternate"}], "title": "SciJinks :: Weather :: Home", "author": "mkbedell", "comments": "http://delicious.com/url/b9bf6d96b027fe6209a6ebe35d2c230e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "SciJinks :: Weather :: Home"}, "link": "http://scijinks.jpl.nasa.gov/weather/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b9bf6d96b027fe6209a6ebe35d2c230e", "id": "http://delicious.com/url/b9bf6d96b027fe6209a6ebe35d2c230e#mkbedell", "tags": [{"term": "education", "scheme": "http://delicious.com/mkbedell/", "label": null}, {"term": "science", "scheme": "http://delicious.com/mkbedell/", "label": null}, {"term": "weather", "scheme": "http://delicious.com/mkbedell/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 14:33:21 +0000", "links": [{"href": "http://designdisease.com/", "type": "text/html", "rel": "alternate"}], "title": "Design Disease \u2013 Blog Design", "author": "profilbox", "comments": "http://delicious.com/url/49dd7b517bde6f5177475f85d03f411a", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Design Disease \u2013 Blog Design"}, "link": "http://designdisease.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/49dd7b517bde6f5177475f85d03f411a", "id": "http://delicious.com/url/49dd7b517bde6f5177475f85d03f411a#profilbox", "tags": [{"term": "wordpress", "scheme": "http://delicious.com/profilbox/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 19:54:08 +0000", "links": [{"href": "http://afreshcup.com/", "type": "text/html", "rel": "alternate"}], "title": "A Fresh Cup", "author": "medburton", "comments": "http://delicious.com/url/b6fccca8e9859ae088e0669036b332eb", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "A Fresh Cup"}, "link": "http://afreshcup.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b6fccca8e9859ae088e0669036b332eb", "id": "http://delicious.com/url/b6fccca8e9859ae088e0669036b332eb#medburton", "tags": [{"term": "blog", "scheme": "http://delicious.com/medburton/", "label": null}, {"term": "rails", "scheme": "http://delicious.com/medburton/", "label": null}, {"term": "ruby", "scheme": "http://delicious.com/medburton/", "label": null}, {"term": "development", "scheme": "http://delicious.com/medburton/", "label": null}, {"term": "fresh", "scheme": "http://delicious.com/medburton/", "label": null}, {"term": "cup", "scheme": "http://delicious.com/medburton/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 21:31:34 +0000", "links": [{"href": "http://languagelog.ldc.upenn.edu/nll/?p=1701", "type": "text/html", "rel": "alternate"}], "title": "Language Log \u00bb Google Books: A Metadata Train Wreck", "author": "GlennMcLaurin", "comments": "http://delicious.com/url/9e47f804ad9e2957ab4ea02444e9b70f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Language Log \u00bb Google Books: A Metadata Train Wreck"}, "link": "http://languagelog.ldc.upenn.edu/nll/?p=1701", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9e47f804ad9e2957ab4ea02444e9b70f", "id": "http://delicious.com/url/9e47f804ad9e2957ab4ea02444e9b70f#GlennMcLaurin", "tags": [{"term": "toread", "scheme": "http://delicious.com/GlennMcLaurin/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 15:48:20 +0000", "links": [{"href": "http://en.wikipedia.org/wiki/Design_pattern_(computer_science)", "type": "text/html", "rel": "alternate"}], "title": "Design pattern (computer science) - Wikipedia, the free encyclopedia", "author": "deathofdemigod", "comments": "http://delicious.com/url/02feac49cdecf0b61c1bf1c838c5651b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Design pattern (computer science) - Wikipedia, the free encyclopedia"}, "link": "http://en.wikipedia.org/wiki/Design_pattern_(computer_science)", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/02feac49cdecf0b61c1bf1c838c5651b", "id": "http://delicious.com/url/02feac49cdecf0b61c1bf1c838c5651b#deathofdemigod", "tags": [{"term": "patterns", "scheme": "http://delicious.com/deathofdemigod/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 03:29:26 +0000", "links": [{"href": "http://www.forbes.com/2009/09/03/fiscal-responsibility-party-opinions-columnists-bruce-bartlett.html", "type": "text/html", "rel": "alternate"}], "title": "We Need A Party Of Fiscal Responsibility - Forbes.com", "author": "beutelevision", "comments": "http://delicious.com/url/519868377cb2d9cf3b205a857bdd99a9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "We Need A Party Of Fiscal Responsibility - Forbes.com"}, "link": "http://www.forbes.com/2009/09/03/fiscal-responsibility-party-opinions-columnists-bruce-bartlett.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/519868377cb2d9cf3b205a857bdd99a9", "id": "http://delicious.com/url/519868377cb2d9cf3b205a857bdd99a9#beutelevision", "tags": [{"term": "economics", "scheme": "http://delicious.com/beutelevision/", "label": null}, {"term": "conservative", "scheme": "http://delicious.com/beutelevision/", "label": null}, {"term": "politics", "scheme": "http://delicious.com/beutelevision/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 07:07:16 +0000", "links": [{"href": "http://harveyshomepage.com/Harveys_Homepage/Utilities.html", "type": "text/html", "rel": "alternate"}], "title": "Smartboard utilities", "author": "kpomeroy", "comments": "http://delicious.com/url/35adb5cbec76d9a5ac323b09e1136842", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Smartboard utilities"}, "link": "http://harveyshomepage.com/Harveys_Homepage/Utilities.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/35adb5cbec76d9a5ac323b09e1136842", "id": "http://delicious.com/url/35adb5cbec76d9a5ac323b09e1136842#kpomeroy", "tags": [{"term": "games", "scheme": "http://delicious.com/kpomeroy/", "label": null}, {"term": "interactive", "scheme": "http://delicious.com/kpomeroy/", "label": null}, {"term": "smartboard", "scheme": "http://delicious.com/kpomeroy/", "label": null}, {"term": "resources", "scheme": "http://delicious.com/kpomeroy/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 20:19:56 +0000", "links": [{"href": "http://www.savadati.com/", "type": "text/html", "rel": "alternate"}], "title": "Sa", "author": "designznest", "comments": "http://delicious.com/url/12ffe3afb65fbd1303c14314ad3f20c1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Sa"}, "link": "http://www.savadati.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/12ffe3afb65fbd1303c14314ad3f20c1", "id": "http://delicious.com/url/12ffe3afb65fbd1303c14314ad3f20c1#designznest"}, +{"updated": "Fri, 11 Sep 2009 03:23:52 +0000", "links": [{"href": "http://www.number10.gov.uk/Page20571", "type": "text/html", "rel": "alternate"}], "title": "Treatment of Alan Turing was \u201cappalling\u201d - PM | Number10.gov.uk", "author": "AvailableName", "comments": "http://delicious.com/url/1a7cad24ac9718806f2fa64f4e369de6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Treatment of Alan Turing was \u201cappalling\u201d - PM | Number10.gov.uk"}, "link": "http://www.number10.gov.uk/Page20571", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1a7cad24ac9718806f2fa64f4e369de6", "id": "http://delicious.com/url/1a7cad24ac9718806f2fa64f4e369de6#AvailableName", "tags": [{"term": "government", "scheme": "http://delicious.com/AvailableName/", "label": null}, {"term": "politics", "scheme": "http://delicious.com/AvailableName/", "label": null}, {"term": "compsci", "scheme": "http://delicious.com/AvailableName/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 05:02:31 +0000", "links": [{"href": "http://dictionary.reference.com/", "type": "text/html", "rel": "alternate"}], "title": "Dictionary.com", "author": "javieralejandro", "comments": "http://delicious.com/url/e83d26511e11b64237cf271af6fcc1ab", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Dictionary.com"}, "link": "http://dictionary.reference.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e83d26511e11b64237cf271af6fcc1ab", "id": "http://delicious.com/url/e83d26511e11b64237cf271af6fcc1ab#javieralejandro"}, +{"updated": "Thu, 10 Sep 2009 21:18:35 +0000", "links": [{"href": "http://pt.wikipedia.org/wiki/P%C3%A1gina_principal", "type": "text/html", "rel": "alternate"}], "title": "Wikip\u00e9dia, a enciclop\u00e9dia livre", "author": "erikgetzel", "comments": "http://delicious.com/url/5797fccbe6bb5a5463e434eaf43e8a7b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Wikip\u00e9dia, a enciclop\u00e9dia livre"}, "link": "http://pt.wikipedia.org/wiki/P%C3%A1gina_principal", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5797fccbe6bb5a5463e434eaf43e8a7b", "id": "http://delicious.com/url/5797fccbe6bb5a5463e434eaf43e8a7b#erikgetzel", "tags": [{"term": "pesquisa", "scheme": "http://delicious.com/erikgetzel/", "label": null}, {"term": "informa\u00e7\u00e3o", "scheme": "http://delicious.com/erikgetzel/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 15:58:52 +0000", "links": [{"href": "http://www.tcmagazine.com/", "type": "text/html", "rel": "alternate"}], "title": "TechConnect Magazine - Technology news since 1997", "author": "abbyer27", "comments": "http://delicious.com/url/06db9612fc0732931bfa8f77857c3ef9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "TechConnect Magazine - Technology news since 1997"}, "link": "http://www.tcmagazine.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/06db9612fc0732931bfa8f77857c3ef9", "id": "http://delicious.com/url/06db9612fc0732931bfa8f77857c3ef9#abbyer27", "tags": [{"term": "new", "scheme": "http://delicious.com/abbyer27/", "label": null}, {"term": "technology", "scheme": "http://delicious.com/abbyer27/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 12:36:54 +0000", "links": [{"href": "http://bulknews.typepad.com/blog/2009/09/on-module-writers-and-users.html", "type": "text/html", "rel": "alternate"}], "title": "On module writers and users - bulknews.typepad.com", "author": "noblejasper", "comments": "http://delicious.com/url/489dbb67ddcb622171545104a2b7788b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "On module writers and users - bulknews.typepad.com"}, "link": "http://bulknews.typepad.com/blog/2009/09/on-module-writers-and-users.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/489dbb67ddcb622171545104a2b7788b", "id": "http://delicious.com/url/489dbb67ddcb622171545104a2b7788b#noblejasper", "tags": [{"term": "perl", "scheme": "http://delicious.com/noblejasper/", "label": null}, {"term": "miyagawa", "scheme": "http://delicious.com/noblejasper/", "label": null}, {"term": "yusukebe", "scheme": "http://delicious.com/noblejasper/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 21:05:34 +0000", "links": [{"href": "http://www.josefrichter.com/helvetimail/", "type": "text/html", "rel": "alternate"}], "title": "Helvetimail - josef richter", "author": "hbokh", "comments": "http://delicious.com/url/6f086d30ca99fb91485da9a0b21bf440", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Helvetimail - josef richter"}, "link": "http://www.josefrichter.com/helvetimail/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6f086d30ca99fb91485da9a0b21bf440", "id": "http://delicious.com/url/6f086d30ca99fb91485da9a0b21bf440#hbokh", "tags": [{"term": "greasemonkey", "scheme": "http://delicious.com/hbokh/", "label": null}, {"term": "helvetica", "scheme": "http://delicious.com/hbokh/", "label": null}, {"term": "helvetimail", "scheme": "http://delicious.com/hbokh/", "label": null}, {"term": "gmail", "scheme": "http://delicious.com/hbokh/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 05:40:56 +0000", "links": [{"href": "http://mashable.com/2009/09/05/sell-products-online/", "type": "text/html", "rel": "alternate"}], "title": "15 Places to Make Money Creating Your Own Products", "author": "talulax", "comments": "http://delicious.com/url/eb67bd4e44fdb44e6cec100d5c73d4cb", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "15 Places to Make Money Creating Your Own Products"}, "link": "http://mashable.com/2009/09/05/sell-products-online/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/eb67bd4e44fdb44e6cec100d5c73d4cb", "id": "http://delicious.com/url/eb67bd4e44fdb44e6cec100d5c73d4cb#talulax", "tags": [{"term": "entrepreneurship", "scheme": "http://delicious.com/talulax/", "label": null}, {"term": "products", "scheme": "http://delicious.com/talulax/", "label": null}, {"term": "creative", "scheme": "http://delicious.com/talulax/", "label": null}, {"term": "ecommerce", "scheme": "http://delicious.com/talulax/", "label": null}, {"term": "tshirts", "scheme": "http://delicious.com/talulax/", "label": null}, {"term": "printondemand", "scheme": "http://delicious.com/talulax/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 15:52:17 +0000", "links": [{"href": "http://learn.genetics.utah.edu/", "type": "text/html", "rel": "alternate"}], "title": "Learn.Genetics (TM)", "author": "srierson", "comments": "http://delicious.com/url/9ab4e9d19134a8f1391296f1ccf2d2b9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Learn.Genetics (TM)"}, "link": "http://learn.genetics.utah.edu/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9ab4e9d19134a8f1391296f1ccf2d2b9", "id": "http://delicious.com/url/9ab4e9d19134a8f1391296f1ccf2d2b9#srierson", "tags": [{"term": "Biology", "scheme": "http://delicious.com/srierson/", "label": null}, {"term": "Anatomy&Physiology", "scheme": "http://delicious.com/srierson/", "label": null}, {"term": "Genetics", "scheme": "http://delicious.com/srierson/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 10:16:47 +0000", "links": [{"href": "http://www.andrewmurphie.org/", "type": "text/html", "rel": "alternate"}], "title": "Andrew Murphie", "author": "paperbacktourist", "comments": "http://delicious.com/url/9c886e8feea83407edc042c1b56ffbe3", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Andrew Murphie"}, "link": "http://www.andrewmurphie.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9c886e8feea83407edc042c1b56ffbe3", "id": "http://delicious.com/url/9c886e8feea83407edc042c1b56ffbe3#paperbacktourist", "tags": [{"term": "culture", "scheme": "http://delicious.com/paperbacktourist/", "label": null}, {"term": "ideas", "scheme": "http://delicious.com/paperbacktourist/", "label": null}, {"term": "philosophy", "scheme": "http://delicious.com/paperbacktourist/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 10:41:07 +0000", "links": [{"href": "http://ocw.mit.edu/OcwWeb/Electrical-Engineering-and-Computer-Science/6-171Fall2003/CourseHome/index.htm", "type": "text/html", "rel": "alternate"}], "title": "MIT OpenCourseWare | Electrical Engineering and Computer Science | 6.171 Software Engineering for Web Applications, Fall 2003 | Home", "author": "vrivero", "comments": "http://delicious.com/url/4d42db18aa44252088683905d9141ed9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "MIT OpenCourseWare | Electrical Engineering and Computer Science | 6.171 Software Engineering for Web Applications, Fall 2003 | Home"}, "link": "http://ocw.mit.edu/OcwWeb/Electrical-Engineering-and-Computer-Science/6-171Fall2003/CourseHome/index.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/4d42db18aa44252088683905d9141ed9", "id": "http://delicious.com/url/4d42db18aa44252088683905d9141ed9#vrivero", "tags": [{"term": "training", "scheme": "http://delicious.com/vrivero/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 09:49:20 +0000", "links": [{"href": "http://www.reddit.com/r/csbooks/", "type": "text/html", "rel": "alternate"}], "title": "Free, legal textbooks related to computer science", "author": "thunderl1", "comments": "http://delicious.com/url/7976de95d5ace21dbc2d81d9a950a65e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Free, legal textbooks related to computer science"}, "link": "http://www.reddit.com/r/csbooks/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7976de95d5ace21dbc2d81d9a950a65e", "id": "http://delicious.com/url/7976de95d5ace21dbc2d81d9a950a65e#thunderl1", "tags": [{"term": "programming", "scheme": "http://delicious.com/thunderl1/", "label": null}, {"term": "books", "scheme": "http://delicious.com/thunderl1/", "label": null}, {"term": "free", "scheme": "http://delicious.com/thunderl1/", "label": null}, {"term": "ebooks", "scheme": "http://delicious.com/thunderl1/", "label": null}, {"term": "book", "scheme": "http://delicious.com/thunderl1/", "label": null}, {"term": "learning", "scheme": "http://delicious.com/thunderl1/", "label": null}, {"term": "computer", "scheme": "http://delicious.com/thunderl1/", "label": null}, {"term": "math", "scheme": "http://delicious.com/thunderl1/", "label": null}, {"term": "computerscience", "scheme": "http://delicious.com/thunderl1/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 08:09:46 +0000", "links": [{"href": "http://www.ibm.com/developerworks/websphere/techjournal/0609_alcott/0609_alcott.html", "type": "text/html", "rel": "alternate"}], "title": "Using Spring and Hibernate with WebSphere Application Server", "author": "yduppen", "comments": "http://delicious.com/url/f02b35a0daaeb6fbe95a62e259484f07", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Using Spring and Hibernate with WebSphere Application Server"}, "link": "http://www.ibm.com/developerworks/websphere/techjournal/0609_alcott/0609_alcott.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f02b35a0daaeb6fbe95a62e259484f07", "id": "http://delicious.com/url/f02b35a0daaeb6fbe95a62e259484f07#yduppen", "tags": [{"term": "springframework", "scheme": "http://delicious.com/yduppen/", "label": null}, {"term": "hibernate", "scheme": "http://delicious.com/yduppen/", "label": null}, {"term": "websphere", "scheme": "http://delicious.com/yduppen/", "label": null}, {"term": "was", "scheme": "http://delicious.com/yduppen/", "label": null}, {"term": "transactions", "scheme": "http://delicious.com/yduppen/", "label": null}, {"term": "j2ee", "scheme": "http://delicious.com/yduppen/", "label": null}, {"term": "ibm", "scheme": "http://delicious.com/yduppen/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 14:30:42 +0000", "links": [{"href": "http://www.win2win.co.uk/forum/", "type": "text/html", "rel": "alternate"}], "title": "Win2Win UK Horse Racing Forum - Powered by vBulletin", "author": "meganinlife", "comments": "http://delicious.com/url/ffb974a027dbf1718009b88fa44b1a0c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Win2Win UK Horse Racing Forum - Powered by vBulletin"}, "link": "http://www.win2win.co.uk/forum/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ffb974a027dbf1718009b88fa44b1a0c", "id": "http://delicious.com/url/ffb974a027dbf1718009b88fa44b1a0c#meganinlife", "tags": [{"term": "horse_racing_forum", "scheme": "http://delicious.com/meganinlife/", "label": null}, {"term": "horse_racing_systems", "scheme": "http://delicious.com/meganinlife/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 19:51:19 +0000", "links": [{"href": "http://su.pr/2qVD8n", "type": "text/html", "rel": "alternate"}], "title": "World\u2019s TOP Funny, Stupid and Banned Commercials in Videos! http://su.pr/2qVD8n", "author": "emarketingcreatively", "comments": "http://delicious.com/url/a01bc85b20d9c154f0a94384166454fb", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "World\u2019s TOP Funny, Stupid and Banned Commercials in Videos! http://su.pr/2qVD8n"}, "link": "http://su.pr/2qVD8n", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a01bc85b20d9c154f0a94384166454fb", "id": "http://delicious.com/url/a01bc85b20d9c154f0a94384166454fb#emarketingcreatively", "tags": [{"term": "World\u2019s", "scheme": "http://delicious.com/emarketingcreatively/", "label": null}, {"term": "Funny,", "scheme": "http://delicious.com/emarketingcreatively/", "label": null}, {"term": "Stupid", "scheme": "http://delicious.com/emarketingcreatively/", "label": null}, {"term": "Banned", "scheme": "http://delicious.com/emarketingcreatively/", "label": null}, {"term": "Commercials", "scheme": "http://delicious.com/emarketingcreatively/", "label": null}, {"term": "Videos!", "scheme": "http://delicious.com/emarketingcreatively/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 16:37:55 +0000", "links": [{"href": "http://www.bakeshop.cz/", "type": "text/html", "rel": "alternate"}], "title": "Bakeshop - home", "author": "chebozka", "comments": "http://delicious.com/url/1597c68bfbe0b8c0e05d9e7d6d05ad76", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Bakeshop - home"}, "link": "http://www.bakeshop.cz/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1597c68bfbe0b8c0e05d9e7d6d05ad76", "id": "http://delicious.com/url/1597c68bfbe0b8c0e05d9e7d6d05ad76#chebozka", "tags": [{"term": "cz", "scheme": "http://delicious.com/chebozka/", "label": null}, {"term": "\u0435\u0434\u0430", "scheme": "http://delicious.com/chebozka/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 20:25:44 +0000", "links": [{"href": "http://www.dragonwater.com/", "type": "text/html", "rel": "alternate"}], "title": "Dragonwater Tea - pure water | fresh leaves | simply healthy", "author": "carbiebash", "comments": "http://delicious.com/url/f63ebb7cc5677620e4dff507883ac125", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Dragonwater Tea - pure water | fresh leaves | simply healthy"}, "link": "http://www.dragonwater.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f63ebb7cc5677620e4dff507883ac125", "id": "http://delicious.com/url/f63ebb7cc5677620e4dff507883ac125#carbiebash", "tags": [{"term": "tea", "scheme": "http://delicious.com/carbiebash/", "label": null}, {"term": "food", "scheme": "http://delicious.com/carbiebash/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 19:53:08 +0000", "links": [{"href": "http://men.style.com/gq/fashion/landing?id=content_4327", "type": "text/html", "rel": "alternate"}], "title": "THE 10 COMMANDMENTS OF STYLE: GQ Fashion on men.style.com", "author": "icanh", "comments": "http://delicious.com/url/7b8d5c815eaba28822c695cce31f69ee", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "THE 10 COMMANDMENTS OF STYLE: GQ Fashion on men.style.com"}, "link": "http://men.style.com/gq/fashion/landing?id=content_4327", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7b8d5c815eaba28822c695cce31f69ee", "id": "http://delicious.com/url/7b8d5c815eaba28822c695cce31f69ee#icanh"}, +{"updated": "Sun, 06 Sep 2009 21:24:21 +0000", "links": [{"href": "http://net.tutsplus.com/tutorials/html-css-techniques/20-html-forms-best-practices-for-beginners/", "type": "text/html", "rel": "alternate"}], "title": "20+ HTML Forms Best Practices for Beginners - Nettuts+", "author": "rjsmorley", "comments": "http://delicious.com/url/ce12ed413c4f8fa117d2adf3ffb49e40", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "20+ HTML Forms Best Practices for Beginners - Nettuts+"}, "link": "http://net.tutsplus.com/tutorials/html-css-techniques/20-html-forms-best-practices-for-beginners/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ce12ed413c4f8fa117d2adf3ffb49e40", "id": "http://delicious.com/url/ce12ed413c4f8fa117d2adf3ffb49e40#rjsmorley", "tags": [{"term": "forms", "scheme": "http://delicious.com/rjsmorley/", "label": null}, {"term": "css", "scheme": "http://delicious.com/rjsmorley/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 05:56:25 +0000", "links": [{"href": "http://www.freeagentcentral.com/", "type": "text/html", "rel": "alternate"}], "title": "FreeAgent --- Online Accounting Software for Freelancers and Small Businesses -", "author": "raymondnaquino", "comments": "http://delicious.com/url/48b73e9edfedaa08cb6693e95a085a8c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "FreeAgent --- Online Accounting Software for Freelancers and Small Businesses -"}, "link": "http://www.freeagentcentral.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/48b73e9edfedaa08cb6693e95a085a8c", "id": "http://delicious.com/url/48b73e9edfedaa08cb6693e95a085a8c#raymondnaquino"}, +{"updated": "Mon, 14 Sep 2009 08:36:46 +0000", "links": [{"href": "http://www.anitavanderkrol.com/", "type": "text/html", "rel": "alternate"}], "title": "Dubai and surroundings, heritage photography 1975-1980 by Anita van der Krol", "author": "idicula", "comments": "http://delicious.com/url/db86b84369c56fd8eb07f1f29047126e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Dubai and surroundings, heritage photography 1975-1980 by Anita van der Krol"}, "link": "http://www.anitavanderkrol.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/db86b84369c56fd8eb07f1f29047126e", "id": "http://delicious.com/url/db86b84369c56fd8eb07f1f29047126e#idicula", "tags": [{"term": "Photography", "scheme": "http://delicious.com/idicula/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 11:52:42 +0000", "links": [{"href": "http://www.unitednerds.org/thefallen/docs/index.php?area=Sincronia&tuto=Unison-basico", "type": "text/html", "rel": "alternate"}], "title": "Sincronia : Sincronia/R\u00e9plica de Arquivos com o Unison", "author": "fantonio", "comments": "http://delicious.com/url/70616ef8e466e9ab790dc3a2a02a98a4", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Sincronia : Sincronia/R\u00e9plica de Arquivos com o Unison"}, "link": "http://www.unitednerds.org/thefallen/docs/index.php?area=Sincronia&tuto=Unison-basico", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/70616ef8e466e9ab790dc3a2a02a98a4", "id": "http://delicious.com/url/70616ef8e466e9ab790dc3a2a02a98a4#fantonio", "tags": [{"term": "SoftwareLivre", "scheme": "http://delicious.com/fantonio/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 09:59:17 +0000", "links": [{"href": "http://fsharpnews.blogspot.com/", "type": "text/html", "rel": "alternate"}], "title": "F# News", "author": "c1sbc", "comments": "http://delicious.com/url/1babcdb8e1512b544691b57ec1ea28d2", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "F# News"}, "link": "http://fsharpnews.blogspot.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1babcdb8e1512b544691b57ec1ea28d2", "id": "http://delicious.com/url/1babcdb8e1512b544691b57ec1ea28d2#c1sbc", "tags": [{"term": "fsharp", "scheme": "http://delicious.com/c1sbc/", "label": null}, {"term": "functional", "scheme": "http://delicious.com/c1sbc/", "label": null}, {"term": "f#", "scheme": "http://delicious.com/c1sbc/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 16:36:04 +0000", "links": [{"href": "http://www.logoorange.com/logo-design-09.php", "type": "text/html", "rel": "alternate"}], "title": "Logo Design & Branding Trends 2009", "author": "diego_ital", "comments": "http://delicious.com/url/8d37659cfd9c747b23c61f40371278ae", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Logo Design & Branding Trends 2009"}, "link": "http://www.logoorange.com/logo-design-09.php", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8d37659cfd9c747b23c61f40371278ae", "id": "http://delicious.com/url/8d37659cfd9c747b23c61f40371278ae#diego_ital", "tags": [{"term": "Identity", "scheme": "http://delicious.com/diego_ital/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 16:55:34 +0000", "links": [{"href": "http://pelfusion.com/tutorials/25-excellent-collection-of-jquery-tutorials/", "type": "text/html", "rel": "alternate"}], "title": "25 Excellent jQuery Tutorials", "author": "tdjones74021", "comments": "http://delicious.com/url/457def9cea09ba2b3ca4e36809ef8e4f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "25 Excellent jQuery Tutorials"}, "link": "http://pelfusion.com/tutorials/25-excellent-collection-of-jquery-tutorials/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/457def9cea09ba2b3ca4e36809ef8e4f", "id": "http://delicious.com/url/457def9cea09ba2b3ca4e36809ef8e4f#tdjones74021", "tags": [{"term": "tutorial", "scheme": "http://delicious.com/tdjones74021/", "label": null}, {"term": "webdesign", "scheme": "http://delicious.com/tdjones74021/", "label": null}, {"term": "javascript", "scheme": "http://delicious.com/tdjones74021/", "label": null}, {"term": "ajax", "scheme": "http://delicious.com/tdjones74021/", "label": null}, {"term": "jquery", "scheme": "http://delicious.com/tdjones74021/", "label": null}, {"term": "directory", "scheme": "http://delicious.com/tdjones74021/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 22:15:15 +0000", "links": [{"href": "http://www.pdfsam.org/", "type": "text/html", "rel": "alternate"}], "title": "PDF Split and Merge", "author": "dpelkins", "comments": "http://delicious.com/url/7fdf030a1279776227f249dadb9ee92d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "PDF Split and Merge"}, "link": "http://www.pdfsam.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7fdf030a1279776227f249dadb9ee92d", "id": "http://delicious.com/url/7fdf030a1279776227f249dadb9ee92d#dpelkins", "tags": [{"term": "software", "scheme": "http://delicious.com/dpelkins/", "label": null}, {"term": "Cool_Tools", "scheme": "http://delicious.com/dpelkins/", "label": null}, {"term": "My_Toolbox", "scheme": "http://delicious.com/dpelkins/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 09:13:23 +0000", "links": [{"href": "http://www.telugufilmfun.com/", "type": "text/html", "rel": "alternate"}], "title": "Telugu Film Fun: Telugu Movie Reviews, Free Downloads, Videos, Telugu Actress Photo Galleries,", "author": "chravikiran", "comments": "http://delicious.com/url/e83ad1a6237c1fb066b592f9858b3298", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Telugu Film Fun: Telugu Movie Reviews, Free Downloads, Videos, Telugu Actress Photo Galleries,"}, "link": "http://www.telugufilmfun.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e83ad1a6237c1fb066b592f9858b3298", "id": "http://delicious.com/url/e83ad1a6237c1fb066b592f9858b3298#chravikiran", "tags": [{"term": "videos", "scheme": "http://delicious.com/chravikiran/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 12:45:04 +0000", "links": [{"href": "http://www.marketwatch.com/story//the-day-the-buck-broke-2009-09-08", "type": "text/html", "rel": "alternate"}], "title": "The day the buck broke - MarketWatch", "author": "LaneV", "comments": "http://delicious.com/url/55665a3705aacdea833fd07237c8949c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The day the buck broke - MarketWatch"}, "link": "http://www.marketwatch.com/story//the-day-the-buck-broke-2009-09-08", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/55665a3705aacdea833fd07237c8949c", "id": "http://delicious.com/url/55665a3705aacdea833fd07237c8949c#LaneV", "tags": [{"term": "financialcrisis", "scheme": "http://delicious.com/LaneV/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 13:08:03 +0000", "enclosures": [{"length": "1", "href": "http://www.justenoughplanning.org/pdf/Spitfire%20Campaign%20Guide%20final.pdf", "type": "application/pdf"}], "links": [{"href": "http://www.justenoughplanning.org/pdf/Spitfire%20Campaign%20Guide%20final.pdf", "type": "text/html", "rel": "alternate"}], "title": "Communications Plan Template", "author": "jball0317", "comments": "http://delicious.com/url/4165403554cd6d42e88759502f29a76d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Communications Plan Template"}, "link": "http://www.justenoughplanning.org/pdf/Spitfire%20Campaign%20Guide%20final.pdf", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/4165403554cd6d42e88759502f29a76d", "id": "http://delicious.com/url/4165403554cd6d42e88759502f29a76d#jball0317", "tags": [{"term": "nonprofit", "scheme": "http://delicious.com/jball0317/", "label": null}, {"term": "marketing", "scheme": "http://delicious.com/jball0317/", "label": null}, {"term": "resource", "scheme": "http://delicious.com/jball0317/", "label": null}, {"term": "system:filetype:pdf", "scheme": "http://delicious.com/jball0317/", "label": null}, {"term": "system:media:document", "scheme": "http://delicious.com/jball0317/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 17:18:32 +0000", "links": [{"href": "http://atc.ugr.es/~jesus/asignaturas/aci/", "type": "text/html", "rel": "alternate"}], "title": "Arquitectura de Computadores", "author": "venosov", "comments": "http://delicious.com/url/34c4ae19c9a66e17d10ffd15e651ecbf", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Arquitectura de Computadores"}, "link": "http://atc.ugr.es/~jesus/asignaturas/aci/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/34c4ae19c9a66e17d10ffd15e651ecbf", "id": "http://delicious.com/url/34c4ae19c9a66e17d10ffd15e651ecbf#venosov", "tags": [{"term": "facultad", "scheme": "http://delicious.com/venosov/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 23:02:59 +0000", "links": [{"href": "http://www.intac.net/build-your-own-server/", "type": "text/html", "rel": "alternate"}], "title": "Build Your Own Server |", "author": "crazyunclejoe", "comments": "http://delicious.com/url/e09cfe9af05ee3b5c30e5e07ad41e7e6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Build Your Own Server |"}, "link": "http://www.intac.net/build-your-own-server/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e09cfe9af05ee3b5c30e5e07ad41e7e6", "id": "http://delicious.com/url/e09cfe9af05ee3b5c30e5e07ad41e7e6#crazyunclejoe", "tags": [{"term": "linux", "scheme": "http://delicious.com/crazyunclejoe/", "label": null}, {"term": "tutorial", "scheme": "http://delicious.com/crazyunclejoe/", "label": null}, {"term": "hardware", "scheme": "http://delicious.com/crazyunclejoe/", "label": null}, {"term": "howto", "scheme": "http://delicious.com/crazyunclejoe/", "label": null}, {"term": "fileserver", "scheme": "http://delicious.com/crazyunclejoe/", "label": null}, {"term": "diy", "scheme": "http://delicious.com/crazyunclejoe/", "label": null}, {"term": "server", "scheme": "http://delicious.com/crazyunclejoe/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 08:09:43 +0000", "links": [{"href": "http://www.mmu.ac.uk/staff/", "type": "text/html", "rel": "alternate"}], "title": "Staff Homepage | Manchester Metropolitan University", "author": "robertoconnor76", "comments": "http://delicious.com/url/fde01ed3c625c3237024c8bf4a75fa55", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Staff Homepage | Manchester Metropolitan University"}, "link": "http://www.mmu.ac.uk/staff/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/fde01ed3c625c3237024c8bf4a75fa55", "id": "http://delicious.com/url/fde01ed3c625c3237024c8bf4a75fa55#robertoconnor76"}, +{"updated": "Tue, 08 Sep 2009 00:38:34 +0000", "links": [{"href": "http://www.techcrunch.com/2009/06/23/iphone-in-app-purchases-already-leading-to-the-dreaded-two-words-bait-and-switch/", "type": "text/html", "rel": "alternate"}], "title": "iPhone In-App Purchases Already Leading To The Dreaded Two Words: Bait And Switch", "author": "felipek", "comments": "http://delicious.com/url/dd61620d155452f9fc4a377a5dfa2863", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "iPhone In-App Purchases Already Leading To The Dreaded Two Words: Bait And Switch"}, "link": "http://www.techcrunch.com/2009/06/23/iphone-in-app-purchases-already-leading-to-the-dreaded-two-words-bait-and-switch/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/dd61620d155452f9fc4a377a5dfa2863", "id": "http://delicious.com/url/dd61620d155452f9fc4a377a5dfa2863#felipek", "tags": [{"term": "AppStore", "scheme": "http://delicious.com/felipek/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 12:28:08 +0000", "links": [{"href": "http://www.ixedit.com/", "type": "text/html", "rel": "alternate"}], "title": "IxEdit", "author": "diriccio", "comments": "http://delicious.com/url/f7007a9383c0ae9cd7cccd45f6590833", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "IxEdit"}, "link": "http://www.ixedit.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f7007a9383c0ae9cd7cccd45f6590833", "id": "http://delicious.com/url/f7007a9383c0ae9cd7cccd45f6590833#diriccio", "tags": [{"term": "javascript", "scheme": "http://delicious.com/diriccio/", "label": null}, {"term": "webdesign", "scheme": "http://delicious.com/diriccio/", "label": null}, {"term": "tools", "scheme": "http://delicious.com/diriccio/", "label": null}, {"term": "design", "scheme": "http://delicious.com/diriccio/", "label": null}, {"term": "ui", "scheme": "http://delicious.com/diriccio/", "label": null}, {"term": "interaction", "scheme": "http://delicious.com/diriccio/", "label": null}, {"term": "jquery", "scheme": "http://delicious.com/diriccio/", "label": null}, {"term": "web", "scheme": "http://delicious.com/diriccio/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 17:12:45 +0000", "links": [{"href": "http://www.readwriteweb.com/archives/top_5_web_trends_of_2009_internet_of_things.php", "type": "text/html", "rel": "alternate"}], "title": "Top 5 Web Trends of 2009: Internet of Things", "author": "abelniak", "comments": "http://delicious.com/url/75440ae4a65e63897dc642a43f3de295", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Top 5 Web Trends of 2009: Internet of Things"}, "link": "http://www.readwriteweb.com/archives/top_5_web_trends_of_2009_internet_of_things.php", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/75440ae4a65e63897dc642a43f3de295", "id": "http://delicious.com/url/75440ae4a65e63897dc642a43f3de295#abelniak", "tags": [{"term": "trends", "scheme": "http://delicious.com/abelniak/", "label": null}, {"term": "internet", "scheme": "http://delicious.com/abelniak/", "label": null}, {"term": "technology", "scheme": "http://delicious.com/abelniak/", "label": null}, {"term": "web2.0", "scheme": "http://delicious.com/abelniak/", "label": null}, {"term": "web", "scheme": "http://delicious.com/abelniak/", "label": null}, {"term": "2009", "scheme": "http://delicious.com/abelniak/", "label": null}, {"term": "business", "scheme": "http://delicious.com/abelniak/", "label": null}, {"term": "blog", "scheme": "http://delicious.com/abelniak/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 18:02:28 +0000", "links": [{"href": "http://www.thisweknow.org/", "type": "text/html", "rel": "alternate"}], "title": "This We Know: Explore U.S. Government Data About Your Community", "author": "williamcouch", "comments": "http://delicious.com/url/41ce7476f4fb5d91189bf31b5fb459d7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "This We Know: Explore U.S. Government Data About Your Community"}, "link": "http://www.thisweknow.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/41ce7476f4fb5d91189bf31b5fb459d7", "id": "http://delicious.com/url/41ce7476f4fb5d91189bf31b5fb459d7#williamcouch", "tags": [{"term": "government", "scheme": "http://delicious.com/williamcouch/", "label": null}, {"term": "data", "scheme": "http://delicious.com/williamcouch/", "label": null}, {"term": "visualization", "scheme": "http://delicious.com/williamcouch/", "label": null}, {"term": "statistics", "scheme": "http://delicious.com/williamcouch/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 12:52:00 +0000", "links": [{"href": "http://wordpress.org/extend/plugins/advanced-permalinks/", "type": "text/html", "rel": "alternate"}], "title": "Advanced Permalinks \u00ab WordPress Plugins", "author": "joaom2", "comments": "http://delicious.com/url/ba5876d6830991fd55c0f34f01edd7b6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Advanced Permalinks \u00ab WordPress Plugins"}, "link": "http://wordpress.org/extend/plugins/advanced-permalinks/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ba5876d6830991fd55c0f34f01edd7b6", "id": "http://delicious.com/url/ba5876d6830991fd55c0f34f01edd7b6#joaom2", "tags": [{"term": "blog", "scheme": "http://delicious.com/joaom2/", "label": null}, {"term": "wordpress", "scheme": "http://delicious.com/joaom2/", "label": null}, {"term": "plugin", "scheme": "http://delicious.com/joaom2/", "label": null}, {"term": "seo", "scheme": "http://delicious.com/joaom2/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 21:20:43 +0000", "links": [{"href": "http://designrfix.com/inspiration/creative-business-cards-uses-of-various-shapes-and-materials", "type": "text/html", "rel": "alternate"}], "title": "Creative Business Cards: Uses of various shapes and materials | designrfix.com", "author": "matchstickmedia", "comments": "http://delicious.com/url/a6ab0cb188f04cd0b86fbf039d077437", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Creative Business Cards: Uses of various shapes and materials | designrfix.com"}, "link": "http://designrfix.com/inspiration/creative-business-cards-uses-of-various-shapes-and-materials", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/a6ab0cb188f04cd0b86fbf039d077437", "id": "http://delicious.com/url/a6ab0cb188f04cd0b86fbf039d077437#matchstickmedia"}, +{"updated": "Mon, 07 Sep 2009 20:12:16 +0000", "links": [{"href": "http://www.hairrific.com/index.html", "type": "text/html", "rel": "alternate"}], "title": "Hairrific.com - SALON GLAM - Kathie Rothkop, Owner/Stylist", "author": "cirom", "comments": "http://delicious.com/url/1c68514e7f3832476285d7998e3cbe05", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Hairrific.com - SALON GLAM - Kathie Rothkop, Owner/Stylist"}, "link": "http://www.hairrific.com/index.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1c68514e7f3832476285d7998e3cbe05", "id": "http://delicious.com/url/1c68514e7f3832476285d7998e3cbe05#cirom", "tags": [{"term": "haj", "scheme": "http://delicious.com/cirom/", "label": null}, {"term": "history", "scheme": "http://delicious.com/cirom/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 03:44:02 +0000", "links": [{"href": "http://www.gtdagenda.com/index.php", "type": "text/html", "rel": "alternate"}], "title": "Gtdagenda.com", "author": "ppaxel", "comments": "http://delicious.com/url/8f1cdcd854ae4912f77cfaa3148b791a", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Gtdagenda.com"}, "link": "http://www.gtdagenda.com/index.php", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8f1cdcd854ae4912f77cfaa3148b791a", "id": "http://delicious.com/url/8f1cdcd854ae4912f77cfaa3148b791a#ppaxel", "tags": [{"term": "productivity", "scheme": "http://delicious.com/ppaxel/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 22:15:58 +0000", "links": [{"href": "http://37signals.com/svn/posts/1880-the-different-sketch-styles-of-the-designers-at-37signals", "type": "text/html", "rel": "alternate"}], "title": "The different sketch styles of the designers at 37signals - (37signals)", "author": "sewen", "comments": "http://delicious.com/url/fc6b87c32e319463ea1f4eabc15422f1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The different sketch styles of the designers at 37signals - (37signals)"}, "link": "http://37signals.com/svn/posts/1880-the-different-sketch-styles-of-the-designers-at-37signals", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/fc6b87c32e319463ea1f4eabc15422f1", "id": "http://delicious.com/url/fc6b87c32e319463ea1f4eabc15422f1#sewen", "tags": [{"term": "webdesign", "scheme": "http://delicious.com/sewen/", "label": null}, {"term": "wireframes", "scheme": "http://delicious.com/sewen/", "label": null}, {"term": "ux", "scheme": "http://delicious.com/sewen/", "label": null}, {"term": "ui", "scheme": "http://delicious.com/sewen/", "label": null}, {"term": "37signals", "scheme": "http://delicious.com/sewen/", "label": null}, {"term": "sketching", "scheme": "http://delicious.com/sewen/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 17:06:00 +0000", "links": [{"href": "http://blogs.archives.gov/online-public-access/", "type": "text/html", "rel": "alternate"}], "title": "NARAtions", "author": "djleenieman", "comments": "http://delicious.com/url/8413be36f41eeb437306457c99ef41bb", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "NARAtions"}, "link": "http://blogs.archives.gov/online-public-access/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8413be36f41eeb437306457c99ef41bb", "id": "http://delicious.com/url/8413be36f41eeb437306457c99ef41bb#djleenieman", "tags": [{"term": "blog", "scheme": "http://delicious.com/djleenieman/", "label": null}, {"term": "genealogy", "scheme": "http://delicious.com/djleenieman/", "label": null}, {"term": "archive", "scheme": "http://delicious.com/djleenieman/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 19:37:48 +0000", "links": [{"href": "http://www.youtube.com/watch?v=JE5kkyucts8", "type": "text/html", "rel": "alternate"}], "title": "YouTube - Danish mother seeking", "author": "maxmotor", "comments": "http://delicious.com/url/0e2cba1cfa252684f8bbc7019d20b470", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "YouTube - Danish mother seeking"}, "link": "http://www.youtube.com/watch?v=JE5kkyucts8", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0e2cba1cfa252684f8bbc7019d20b470", "id": "http://delicious.com/url/0e2cba1cfa252684f8bbc7019d20b470#maxmotor", "tags": [{"term": "mother", "scheme": "http://delicious.com/maxmotor/", "label": null}, {"term": "danish", "scheme": "http://delicious.com/maxmotor/", "label": null}, {"term": "seeking", "scheme": "http://delicious.com/maxmotor/", "label": null}, {"term": "hoax", "scheme": "http://delicious.com/maxmotor/", "label": null}, {"term": "Video", "scheme": "http://delicious.com/maxmotor/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 13:05:52 +0000", "links": [{"href": "http://therumpus.net/2009/06/the-women-of-mcsweeneysnet/", "type": "text/html", "rel": "alternate"}], "title": "The Women Of McSweeneys.net - The Rumpus.net", "author": "swhank99", "comments": "http://delicious.com/url/257ebb607cb07f87552096e7f31664d2", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Women Of McSweeneys.net - The Rumpus.net"}, "link": "http://therumpus.net/2009/06/the-women-of-mcsweeneysnet/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/257ebb607cb07f87552096e7f31664d2", "id": "http://delicious.com/url/257ebb607cb07f87552096e7f31664d2#swhank99", "tags": [{"term": "writing", "scheme": "http://delicious.com/swhank99/", "label": null}, {"term": "women", "scheme": "http://delicious.com/swhank99/", "label": null}, {"term": "humor", "scheme": "http://delicious.com/swhank99/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 11:34:03 +0000", "links": [{"href": "http://paginaspersonales.deusto.es/dipina/", "type": "text/html", "rel": "alternate"}], "title": "P\u00e1gina personal de Dr. Diego L\u00f3pez de Ipi\u00f1a", "author": "ggoggin", "comments": "http://delicious.com/url/350b47fc9f80d22e5a9316c86a834e93", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "P\u00e1gina personal de Dr. Diego L\u00f3pez de Ipi\u00f1a"}, "link": "http://paginaspersonales.deusto.es/dipina/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/350b47fc9f80d22e5a9316c86a834e93", "id": "http://delicious.com/url/350b47fc9f80d22e5a9316c86a834e93#ggoggin", "tags": [{"term": "Ipi\u00f1a", "scheme": "http://delicious.com/ggoggin/", "label": null}, {"term": "disability", "scheme": "http://delicious.com/ggoggin/", "label": null}, {"term": "Spain", "scheme": "http://delicious.com/ggoggin/", "label": null}, {"term": "mobiles", "scheme": "http://delicious.com/ggoggin/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 04:02:39 +0000", "links": [{"href": "http://www.archive.org/index.php", "type": "text/html", "rel": "alternate"}], "title": "Internet Archive", "author": "saithimran", "comments": "http://delicious.com/url/604a83f971ad6aa917f623deb1479507", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Internet Archive"}, "link": "http://www.archive.org/index.php", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/604a83f971ad6aa917f623deb1479507", "id": "http://delicious.com/url/604a83f971ad6aa917f623deb1479507#saithimran", "tags": [{"term": "Archives", "scheme": "http://delicious.com/saithimran/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 01:15:44 +0000", "links": [{"href": "http://www.fastcompany.com/node/1325728/print", "type": "text/html", "rel": "alternate"}], "title": "How Web-Savvy Edupunks Are Transforming American Higher Education", "author": "cbbugbee", "comments": "http://delicious.com/url/b5a02b7c7e0e9195b942c79829f5290b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "How Web-Savvy Edupunks Are Transforming American Higher Education"}, "link": "http://www.fastcompany.com/node/1325728/print", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b5a02b7c7e0e9195b942c79829f5290b", "id": "http://delicious.com/url/b5a02b7c7e0e9195b942c79829f5290b#cbbugbee", "tags": [{"term": "education", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "web2.0", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "academic", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "future", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "web", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "trends", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "online", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "2.0", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "university", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "opensource", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "highered", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "e-learning", "scheme": "http://delicious.com/cbbugbee/", "label": null}, {"term": "innovation", "scheme": "http://delicious.com/cbbugbee/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 08:03:01 +0000", "links": [{"href": "http://www.google.com/ta3reeb/", "type": "text/html", "rel": "alternate"}], "title": "Google ta3reeb - Arabic Keyboard using English Characters", "author": "webtizer", "comments": "http://delicious.com/url/c2ea0ca008b6a0b350b2d6a0e1f6ccd2", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Google ta3reeb - Arabic Keyboard using English Characters"}, "link": "http://www.google.com/ta3reeb/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c2ea0ca008b6a0b350b2d6a0e1f6ccd2", "id": "http://delicious.com/url/c2ea0ca008b6a0b350b2d6a0e1f6ccd2#webtizer", "tags": [{"term": "nice", "scheme": "http://delicious.com/webtizer/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 13:15:20 +0000", "links": [{"href": "http://miupa.org/usability/jobs-in-usability/", "type": "text/html", "rel": "alternate"}], "title": "Michigan Usability Professionals\u2019 Association \u00bb Jobs in Usability", "author": "si.careers", "comments": "http://delicious.com/url/6a4dafadedd46325341b5dfd8712ff04", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Michigan Usability Professionals\u2019 Association \u00bb Jobs in Usability"}, "link": "http://miupa.org/usability/jobs-in-usability/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6a4dafadedd46325341b5dfd8712ff04", "id": "http://delicious.com/url/6a4dafadedd46325341b5dfd8712ff04#si.careers", "tags": [{"term": "SI_HCI", "scheme": "http://delicious.com/si.careers/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 03:05:10 +0000", "links": [{"href": "http://www.opensourcewindows.org/", "type": "text/html", "rel": "alternate"}], "title": "Open Source Windows - Free, Open-Source software for Windows XP and Vista", "author": "marbinmf", "comments": "http://delicious.com/url/6efc42e2d40f786502778a40b929ad42", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Open Source Windows - Free, Open-Source software for Windows XP and Vista"}, "link": "http://www.opensourcewindows.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6efc42e2d40f786502778a40b929ad42", "id": "http://delicious.com/url/6efc42e2d40f786502778a40b929ad42#marbinmf", "tags": [{"term": "opensource", "scheme": "http://delicious.com/marbinmf/", "label": null}, {"term": "software", "scheme": "http://delicious.com/marbinmf/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 02:40:12 +0000", "links": [{"href": "http://en.wikipedia.org/wiki/Foundry_Networks", "type": "text/html", "rel": "alternate"}], "title": "Foundry Networks - Wikipedia, the free encyclopedia", "author": "rick7b7b", "comments": "http://delicious.com/url/f5d6c71e6a5a0307a024fc58b90cda24", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Foundry Networks - Wikipedia, the free encyclopedia"}, "link": "http://en.wikipedia.org/wiki/Foundry_Networks", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f5d6c71e6a5a0307a024fc58b90cda24", "id": "http://delicious.com/url/f5d6c71e6a5a0307a024fc58b90cda24#rick7b7b", "tags": [{"term": "ClearWireInterview", "scheme": "http://delicious.com/rick7b7b/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 16:37:22 +0000", "links": [{"href": "http://www.oldsoulnewheart.com/", "type": "text/html", "rel": "alternate"}], "title": "Old Soul New Heart \u2014 Home", "author": "kananimasterson", "comments": "http://delicious.com/url/1c286aa035764a8a924a491f43acdc0c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Old Soul New Heart \u2014 Home"}, "link": "http://www.oldsoulnewheart.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1c286aa035764a8a924a491f43acdc0c", "id": "http://delicious.com/url/1c286aa035764a8a924a491f43acdc0c#kananimasterson", "tags": [{"term": "fashion", "scheme": "http://delicious.com/kananimasterson/", "label": null}, {"term": "accessories", "scheme": "http://delicious.com/kananimasterson/", "label": null}, {"term": "headbands", "scheme": "http://delicious.com/kananimasterson/", "label": null}, {"term": "jewelry", "scheme": "http://delicious.com/kananimasterson/", "label": null}, {"term": "gifts", "scheme": "http://delicious.com/kananimasterson/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 18:44:23 +0000", "links": [{"href": "http://www.youtube.com/watch?v=SknedFYCZZA", "type": "text/html", "rel": "alternate"}], "title": "YouTube - \u30de\u30af\u30ed\u30b9\u611b\u899a\u3048\u3066\u3044\u307e\u3059\u304b\uff1f", "author": "33rpm", "comments": "http://delicious.com/url/202a1dd4966e946d012e8992ffac2666", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "YouTube - \u30de\u30af\u30ed\u30b9\u611b\u899a\u3048\u3066\u3044\u307e\u3059\u304b\uff1f"}, "link": "http://www.youtube.com/watch?v=SknedFYCZZA", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/202a1dd4966e946d012e8992ffac2666", "id": "http://delicious.com/url/202a1dd4966e946d012e8992ffac2666#33rpm", "tags": [{"term": "music", "scheme": "http://delicious.com/33rpm/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 05:03:05 +0000", "links": [{"href": "http://steviarecipes.blogspot.com/", "type": "text/html", "rel": "alternate"}], "title": "Stevia Recipes (group of recipes, including the Banana Bread recipe)", "author": "msdrpepper", "comments": "http://delicious.com/url/af0159bb0a675c5929a1ae7757e6acbf", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Stevia Recipes (group of recipes, including the Banana Bread recipe)"}, "link": "http://steviarecipes.blogspot.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/af0159bb0a675c5929a1ae7757e6acbf", "id": "http://delicious.com/url/af0159bb0a675c5929a1ae7757e6acbf#msdrpepper", "tags": [{"term": "Stevia", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "recipe", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "recipes", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "blog", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "food", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "cooking", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "baking", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "healthy", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "sweetener", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "banana", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "bread", "scheme": "http://delicious.com/msdrpepper/", "label": null}, {"term": "muffins", "scheme": "http://delicious.com/msdrpepper/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 03:23:25 +0000", "links": [{"href": "http://www.linuxfoundation.org/en/Net:NAPI", "type": "text/html", "rel": "alternate"}], "title": "Net:NAPI - The *new* driver packet processing framework", "author": "stevie.glass", "comments": "http://delicious.com/url/242b33fef971ab91a3e1a15b461304ae", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Net:NAPI - The *new* driver packet processing framework"}, "link": "http://www.linuxfoundation.org/en/Net:NAPI", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/242b33fef971ab91a3e1a15b461304ae", "id": "http://delicious.com/url/242b33fef971ab91a3e1a15b461304ae#stevie.glass", "tags": [{"term": "GNU", "scheme": "http://delicious.com/stevie.glass/", "label": null}, {"term": "linux", "scheme": "http://delicious.com/stevie.glass/", "label": null}, {"term": "kernel", "scheme": "http://delicious.com/stevie.glass/", "label": null}, {"term": "network", "scheme": "http://delicious.com/stevie.glass/", "label": null}, {"term": "driver", "scheme": "http://delicious.com/stevie.glass/", "label": null}, {"term": "API", "scheme": "http://delicious.com/stevie.glass/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 06:34:04 +0000", "links": [{"href": "http://www.planetebook.com/", "type": "text/html", "rel": "alternate"}], "title": "Free eBooks at Planet eBook - Classic Novels and Literature You're Free to Share", "author": "bort1002000", "comments": "http://delicious.com/url/6327a53b52d91b2bb8dfd91d7d6f3727", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Free eBooks at Planet eBook - Classic Novels and Literature You're Free to Share"}, "link": "http://www.planetebook.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6327a53b52d91b2bb8dfd91d7d6f3727", "id": "http://delicious.com/url/6327a53b52d91b2bb8dfd91d7d6f3727#bort1002000", "tags": [{"term": "ebooks", "scheme": "http://delicious.com/bort1002000/", "label": null}, {"term": "free", "scheme": "http://delicious.com/bort1002000/", "label": null}, {"term": "resources", "scheme": "http://delicious.com/bort1002000/", "label": null}, {"term": "download", "scheme": "http://delicious.com/bort1002000/", "label": null}, {"term": "reading", "scheme": "http://delicious.com/bort1002000/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 14:56:30 +0000", "links": [{"href": "http://www.kilim.nl/", "type": "text/html", "rel": "alternate"}], "title": "Kilim, Turks Specialiteiten", "author": "beanavarro", "comments": "http://delicious.com/url/6d2b238afc5d510c4f6d590ea0e14e89", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Kilim, Turks Specialiteiten"}, "link": "http://www.kilim.nl/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/6d2b238afc5d510c4f6d590ea0e14e89", "id": "http://delicious.com/url/6d2b238afc5d510c4f6d590ea0e14e89#beanavarro", "tags": [{"term": "Holanda", "scheme": "http://delicious.com/beanavarro/", "label": null}, {"term": "Restaurante", "scheme": "http://delicious.com/beanavarro/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 15:23:34 +0000", "links": [{"href": "http://www.sciencedaily.com/releases/2009/06/090604144324.htm", "type": "text/html", "rel": "alternate"}], "title": "High Population Density Triggers Cultural Explosions", "author": "simon.belak", "comments": "http://delicious.com/url/ae52b142a76a87cafd8ed66da5902b29", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "High Population Density Triggers Cultural Explosions"}, "link": "http://www.sciencedaily.com/releases/2009/06/090604144324.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ae52b142a76a87cafd8ed66da5902b29", "id": "http://delicious.com/url/ae52b142a76a87cafd8ed66da5902b29#simon.belak", "tags": [{"term": "evolution", "scheme": "http://delicious.com/simon.belak/", "label": null}, {"term": "culture", "scheme": "http://delicious.com/simon.belak/", "label": null}, {"term": "communication", "scheme": "http://delicious.com/simon.belak/", "label": null}, {"term": "anthropology", "scheme": "http://delicious.com/simon.belak/", "label": null}, {"term": "history", "scheme": "http://delicious.com/simon.belak/", "label": null}, {"term": "future", "scheme": "http://delicious.com/simon.belak/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 08:20:21 +0000", "links": [{"href": "http://www.iphone-me.co.il/blog/", "type": "text/html", "rel": "alternate"}], "title": "\u05d0\u05d9 \u05e4\u05d5\u05df", "author": "amiifargan3", "comments": "http://delicious.com/url/e85465b7fece23e206089341ab59d883", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u05d0\u05d9 \u05e4\u05d5\u05df"}, "link": "http://www.iphone-me.co.il/blog/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e85465b7fece23e206089341ab59d883", "id": "http://delicious.com/url/e85465b7fece23e206089341ab59d883#amiifargan3"}, +{"updated": "Mon, 07 Sep 2009 18:41:27 +0000", "links": [{"href": "http://letsgetsconed.blogspot.com/", "type": "text/html", "rel": "alternate"}], "title": "Get Sconed!", "author": "kassloom", "comments": "http://delicious.com/url/999943ad195366699c9addff4c64e05d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Get Sconed!"}, "link": "http://letsgetsconed.blogspot.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/999943ad195366699c9addff4c64e05d", "id": "http://delicious.com/url/999943ad195366699c9addff4c64e05d#kassloom", "tags": [{"term": "vegan", "scheme": "http://delicious.com/kassloom/", "label": null}, {"term": "recipes", "scheme": "http://delicious.com/kassloom/", "label": null}, {"term": "blogs", "scheme": "http://delicious.com/kassloom/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 03:26:04 +0000", "links": [{"href": "http://bestiario.org/research/remap/", "type": "text/html", "rel": "alternate"}], "title": "reMap", "author": "hjhoo", "comments": "http://delicious.com/url/b164714d84ed673642840733afeb51dd", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "reMap"}, "link": "http://bestiario.org/research/remap/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b164714d84ed673642840733afeb51dd", "id": "http://delicious.com/url/b164714d84ed673642840733afeb51dd#hjhoo", "tags": [{"term": "texture", "scheme": "http://delicious.com/hjhoo/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 02:54:22 +0000", "links": [{"href": "http://www.codeguru.com/cpp/controls/treeview/dragdrop/article.php/c707/", "type": "text/html", "rel": "alternate"}], "title": "Drag and drop \u2014 CodeGuru.com", "author": "tboyle", "comments": "http://delicious.com/url/0c984a083201a18d338be232844feb2a", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Drag and drop \u2014 CodeGuru.com"}, "link": "http://www.codeguru.com/cpp/controls/treeview/dragdrop/article.php/c707/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0c984a083201a18d338be232844feb2a", "id": "http://delicious.com/url/0c984a083201a18d338be232844feb2a#tboyle", "tags": [{"term": "mfc-dragdrop", "scheme": "http://delicious.com/tboyle/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 22:43:35 +0000", "links": [{"href": "http://www.webofentertainment.com/2009/09/19-handwritten-signs-of-lol.html", "type": "text/html", "rel": "alternate"}], "title": "Entertainment Web: 19 Handwritten Signs of LOL", "author": "polyrhythmic", "comments": "http://delicious.com/url/3858d309de41df93690554a13fabaf03", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Entertainment Web: 19 Handwritten Signs of LOL"}, "link": "http://www.webofentertainment.com/2009/09/19-handwritten-signs-of-lol.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3858d309de41df93690554a13fabaf03", "id": "http://delicious.com/url/3858d309de41df93690554a13fabaf03#polyrhythmic", "tags": [{"term": "lol", "scheme": "http://delicious.com/polyrhythmic/", "label": null}, {"term": "grafitti", "scheme": "http://delicious.com/polyrhythmic/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 17:55:45 +0000", "links": [{"href": "http://www.bartelme.at/showroom", "type": "text/html", "rel": "alternate"}], "title": "Bartelme Design | Showroom", "author": "octavecat", "comments": "http://delicious.com/url/0e6b1d73a65e7dd500ca885c58d0fe27", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Bartelme Design | Showroom"}, "link": "http://www.bartelme.at/showroom", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0e6b1d73a65e7dd500ca885c58d0fe27", "id": "http://delicious.com/url/0e6b1d73a65e7dd500ca885c58d0fe27#octavecat", "tags": [{"term": "icons", "scheme": "http://delicious.com/octavecat/", "label": null}, {"term": "design", "scheme": "http://delicious.com/octavecat/", "label": null}, {"term": "portfolio", "scheme": "http://delicious.com/octavecat/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 02:18:00 +0000", "links": [{"href": "http://www.behance.net/Gallery/Wire-Illustrations/252777", "type": "text/html", "rel": "alternate"}], "title": "Wire Illustrations on the Behance Network", "author": "philrenaud", "comments": "http://delicious.com/url/14f5f8f1508a736e826d352c02acd8a9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Wire Illustrations on the Behance Network"}, "link": "http://www.behance.net/Gallery/Wire-Illustrations/252777", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/14f5f8f1508a736e826d352c02acd8a9", "id": "http://delicious.com/url/14f5f8f1508a736e826d352c02acd8a9#philrenaud", "tags": [{"term": "illustration", "scheme": "http://delicious.com/philrenaud/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 15:31:09 +0000", "links": [{"href": "http://www.mommybloggers.com/2006/03/the_goodenough_mother.html", "type": "text/html", "rel": "alternate"}], "title": "Mommy Bloggers: The Good-Enough Mother", "author": "finnell", "comments": "http://delicious.com/url/e0a68a6463fa1e017e0ce2d3198b1444", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Mommy Bloggers: The Good-Enough Mother"}, "link": "http://www.mommybloggers.com/2006/03/the_goodenough_mother.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/e0a68a6463fa1e017e0ce2d3198b1444", "id": "http://delicious.com/url/e0a68a6463fa1e017e0ce2d3198b1444#finnell", "tags": [{"term": "parenting", "scheme": "http://delicious.com/finnell/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 07:10:29 +0000", "links": [{"href": "http://www.thesurvivalistblog.net/", "type": "text/html", "rel": "alternate"}], "title": "The Survivalist Blog | Free Info Covering All Aspects Of Survival", "author": "jeremycrews", "comments": "http://delicious.com/url/066a4b4de0690aa8ec0484e07f66fffe", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Survivalist Blog | Free Info Covering All Aspects Of Survival"}, "link": "http://www.thesurvivalistblog.net/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/066a4b4de0690aa8ec0484e07f66fffe", "id": "http://delicious.com/url/066a4b4de0690aa8ec0484e07f66fffe#jeremycrews", "tags": [{"term": "survivalist", "scheme": "http://delicious.com/jeremycrews/", "label": null}, {"term": "survival", "scheme": "http://delicious.com/jeremycrews/", "label": null}, {"term": "guns", "scheme": "http://delicious.com/jeremycrews/", "label": null}, {"term": "preparedness", "scheme": "http://delicious.com/jeremycrews/", "label": null}, {"term": "self-defense", "scheme": "http://delicious.com/jeremycrews/", "label": null}, {"term": "frugal-living", "scheme": "http://delicious.com/jeremycrews/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 16:34:06 +0000", "links": [{"href": "http://www.area1.info/2009/03/12/60-best-layout-design-tutorials/", "type": "text/html", "rel": "alternate"}], "title": "60 Layout design tutorials | AREA 1 - Graphic/web design tutorials & articles", "author": "lv.beleck", "comments": "http://delicious.com/url/d1f4e8b8d7608beacec8491e96ff7857", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "60 Layout design tutorials | AREA 1 - Graphic/web design tutorials & articles"}, "link": "http://www.area1.info/2009/03/12/60-best-layout-design-tutorials/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d1f4e8b8d7608beacec8491e96ff7857", "id": "http://delicious.com/url/d1f4e8b8d7608beacec8491e96ff7857#lv.beleck", "tags": [{"term": "mega", "scheme": "http://delicious.com/lv.beleck/", "label": null}, {"term": "process", "scheme": "http://delicious.com/lv.beleck/", "label": null}, {"term": "case", "scheme": "http://delicious.com/lv.beleck/", "label": null}, {"term": "study", "scheme": "http://delicious.com/lv.beleck/", "label": null}, {"term": "tuto", "scheme": "http://delicious.com/lv.beleck/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 00:35:22 +0000", "links": [{"href": "http://www.facebook.com/", "type": "text/html", "rel": "alternate"}], "title": "Welcome to Facebook! | Facebook", "author": "duhaas", "comments": "http://delicious.com/url/87468c07c02e370ef84d4b7e3a668589", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Welcome to Facebook! | Facebook"}, "link": "http://www.facebook.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/87468c07c02e370ef84d4b7e3a668589", "id": "http://delicious.com/url/87468c07c02e370ef84d4b7e3a668589#duhaas"}, +{"updated": "Fri, 11 Sep 2009 03:47:17 +0000", "links": [{"href": "http://picky-palate.com/2009/09/10/caramel-apple-cream-cheese-cookie-bars/", "type": "text/html", "rel": "alternate"}], "title": "Caramel Apple Cream Cheese Cookie Bars! | Picky Palate", "author": "msindependent05", "comments": "http://delicious.com/url/c04c92442cf68f5c14a36c78339a5fa5", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Caramel Apple Cream Cheese Cookie Bars! | Picky Palate"}, "link": "http://picky-palate.com/2009/09/10/caramel-apple-cream-cheese-cookie-bars/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c04c92442cf68f5c14a36c78339a5fa5", "id": "http://delicious.com/url/c04c92442cf68f5c14a36c78339a5fa5#msindependent05", "tags": [{"term": "FANTASTIC", "scheme": "http://delicious.com/msindependent05/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 21:22:00 +0000", "links": [{"href": "http://ourworld.compuserve.com/homepages/jsuebersax/icc.htm", "type": "text/html", "rel": "alternate"}], "title": "Intraclass Correlation and Variance Component Methods", "author": "lyn_howfixcredit", "comments": "http://delicious.com/url/75687c3bc36a1ce72131ad43f2f6a79b", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Intraclass Correlation and Variance Component Methods"}, "link": "http://ourworld.compuserve.com/homepages/jsuebersax/icc.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/75687c3bc36a1ce72131ad43f2f6a79b", "id": "http://delicious.com/url/75687c3bc36a1ce72131ad43f2f6a79b#lyn_howfixcredit", "tags": [{"term": "ink,", "scheme": "http://delicious.com/lyn_howfixcredit/", "label": null}, {"term": "cartridge,", "scheme": "http://delicious.com/lyn_howfixcredit/", "label": null}, {"term": "toner,", "scheme": "http://delicious.com/lyn_howfixcredit/", "label": null}, {"term": "toner", "scheme": "http://delicious.com/lyn_howfixcredit/", "label": null}, {"term": "printer", "scheme": "http://delicious.com/lyn_howfixcredit/", "label": null}, {"term": "cartridges,", "scheme": "http://delicious.com/lyn_howfixcredit/", "label": null}, {"term": "repair", "scheme": "http://delicious.com/lyn_howfixcredit/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 22:32:13 +0000", "links": [{"href": "http://www.creativepro.com/article/shooting-photographs-web", "type": "text/html", "rel": "alternate"}], "title": "http://www.creativepro.com/article/shooting-photographs-web", "author": "larrylevin", "comments": "http://delicious.com/url/7480570e6ec1079957e3707c4dfd19c1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "http://www.creativepro.com/article/shooting-photographs-web"}, "link": "http://www.creativepro.com/article/shooting-photographs-web", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7480570e6ec1079957e3707c4dfd19c1", "id": "http://delicious.com/url/7480570e6ec1079957e3707c4dfd19c1#larrylevin", "tags": [{"term": "photography", "scheme": "http://delicious.com/larrylevin/", "label": null}, {"term": "bestpractices", "scheme": "http://delicious.com/larrylevin/", "label": null}, {"term": "Photoshop", "scheme": "http://delicious.com/larrylevin/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 14:14:26 +0000", "links": [{"href": "http://news.yahoo.com/", "type": "text/html", "rel": "alternate"}], "title": "Yahoo! News", "author": "cooklibrary", "comments": "http://delicious.com/url/93fee69812de30ddb87308f016af8cff", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Yahoo! News"}, "link": "http://news.yahoo.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/93fee69812de30ddb87308f016af8cff", "id": "http://delicious.com/url/93fee69812de30ddb87308f016af8cff#cooklibrary", "tags": [{"term": "news", "scheme": "http://delicious.com/cooklibrary/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 08:17:50 +0000", "links": [{"href": "http://mashable.com/2009/03/16/design-inspiration/", "type": "text/html", "rel": "alternate"}], "title": "100 Great Resources for Design Inspiration", "author": "hvostik", "comments": "http://delicious.com/url/c790ccfd282bf25d5aa3d161d6e1a23d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "100 Great Resources for Design Inspiration"}, "link": "http://mashable.com/2009/03/16/design-inspiration/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c790ccfd282bf25d5aa3d161d6e1a23d", "id": "http://delicious.com/url/c790ccfd282bf25d5aa3d161d6e1a23d#hvostik", "tags": [{"term": "inspiration", "scheme": "http://delicious.com/hvostik/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 16:59:56 +0000", "links": [{"href": "http://kideos.com/", "type": "text/html", "rel": "alternate"}], "title": "YouTube Videos of Elmo, Disney, Barney, Teletubbies & Sesame Street | Kideos | Videos for Kids", "author": "manaid", "comments": "http://delicious.com/url/4f902ba4ed97ad8d3df5c77a10c92bb8", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "YouTube Videos of Elmo, Disney, Barney, Teletubbies & Sesame Street | Kideos | Videos for Kids"}, "link": "http://kideos.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/4f902ba4ed97ad8d3df5c77a10c92bb8", "id": "http://delicious.com/url/4f902ba4ed97ad8d3df5c77a10c92bb8#manaid", "tags": [{"term": "ingles", "scheme": "http://delicious.com/manaid/", "label": null}, {"term": "kids", "scheme": "http://delicious.com/manaid/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 08:45:37 +0000", "links": [{"href": "http://www.livecdlist.com/", "type": "text/html", "rel": "alternate"}], "title": "The LiveCD List", "author": "hanyoud", "comments": "http://delicious.com/url/89909ef2707b8a8a8692d0b76b2f75a2", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The LiveCD List"}, "link": "http://www.livecdlist.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/89909ef2707b8a8a8692d0b76b2f75a2", "id": "http://delicious.com/url/89909ef2707b8a8a8692d0b76b2f75a2#hanyoud", "tags": [{"term": "linux", "scheme": "http://delicious.com/hanyoud/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 01:55:40 +0000", "links": [{"href": "http://www.ionerucquoi.com/", "type": "text/html", "rel": "alternate"}], "title": "Ione Rucquoi | Mixed Media Artist - Conceptual Photography - Portraiture", "author": "andreasjunus", "comments": "http://delicious.com/url/8cfd37ad47b20b35c2feec19f74b73ce", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Ione Rucquoi | Mixed Media Artist - Conceptual Photography - Portraiture"}, "link": "http://www.ionerucquoi.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8cfd37ad47b20b35c2feec19f74b73ce", "id": "http://delicious.com/url/8cfd37ad47b20b35c2feec19f74b73ce#andreasjunus", "tags": [{"term": "photography", "scheme": "http://delicious.com/andreasjunus/", "label": null}, {"term": "art", "scheme": "http://delicious.com/andreasjunus/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 16:39:38 +0000", "links": [{"href": "http://www.go2web20.net/app/?a=Kideos", "type": "text/html", "rel": "alternate"}], "title": "Kideos: Video for Kids - kideos.com", "author": "kmkramer6888", "comments": "http://delicious.com/url/f56cde9128f65c2e2ddc9d3a400599d2", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Kideos: Video for Kids - kideos.com"}, "link": "http://www.go2web20.net/app/?a=Kideos", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f56cde9128f65c2e2ddc9d3a400599d2", "id": "http://delicious.com/url/f56cde9128f65c2e2ddc9d3a400599d2#kmkramer6888", "tags": [{"term": "Kid", "scheme": "http://delicious.com/kmkramer6888/", "label": null}, {"term": "Videos", "scheme": "http://delicious.com/kmkramer6888/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 18:06:49 +0000", "links": [{"href": "http://sports.yahoo.com/mlb/standings", "type": "text/html", "rel": "alternate"}], "title": "MLB - Standings - Yahoo! Sports", "author": "ShelliMayfield", "comments": "http://delicious.com/url/0a4ad92e682788fbc82590fb36574342", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "MLB - Standings - Yahoo! Sports"}, "link": "http://sports.yahoo.com/mlb/standings", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0a4ad92e682788fbc82590fb36574342", "id": "http://delicious.com/url/0a4ad92e682788fbc82590fb36574342#ShelliMayfield"}, +{"updated": "Wed, 09 Sep 2009 01:36:20 +0000", "links": [{"href": "http://www.helvetireader.com/", "type": "text/html", "rel": "alternate"}], "title": "Helvetireader", "author": "Meeaall", "comments": "http://delicious.com/url/04539d7fc149080506591c90cb20ba51", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Helvetireader"}, "link": "http://www.helvetireader.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/04539d7fc149080506591c90cb20ba51", "id": "http://delicious.com/url/04539d7fc149080506591c90cb20ba51#Meeaall"}, +{"updated": "Sat, 05 Sep 2009 12:16:28 +0000", "links": [{"href": "http://mashable.com/2009/09/05/twitter-advanced-search/", "type": "text/html", "rel": "alternate"}], "title": "HOW TO: Use Twitter\u2019s Advanced Search Features", "author": "Joanne_Maly", "comments": "http://delicious.com/url/5b03054d43d3baa9641d8d4808cfa67c", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "HOW TO: Use Twitter\u2019s Advanced Search Features"}, "link": "http://mashable.com/2009/09/05/twitter-advanced-search/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5b03054d43d3baa9641d8d4808cfa67c", "id": "http://delicious.com/url/5b03054d43d3baa9641d8d4808cfa67c#Joanne_Maly", "tags": [{"term": "twittersearch", "scheme": "http://delicious.com/Joanne_Maly/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 09:45:02 +0000", "links": [{"href": "http://poisontaster.livejournal.com/318736.html", "type": "text/html", "rel": "alternate"}], "title": "[The Breakfast Club] You Don't Bring Me Flowers", "author": "jessikast", "comments": "http://delicious.com/url/7075807b55e1c6068b0386bf53a4a3da", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "[The Breakfast Club] You Don't Bring Me Flowers"}, "link": "http://poisontaster.livejournal.com/318736.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7075807b55e1c6068b0386bf53a4a3da", "id": "http://delicious.com/url/7075807b55e1c6068b0386bf53a4a3da#jessikast", "tags": [{"term": "BreakfastClub", "scheme": "http://delicious.com/jessikast/", "label": null}, {"term": "by:Poisontaster", "scheme": "http://delicious.com/jessikast/", "label": null}, {"term": "slash", "scheme": "http://delicious.com/jessikast/", "label": null}, {"term": "Bender/Brian", "scheme": "http://delicious.com/jessikast/", "label": null}, {"term": "fic", "scheme": "http://delicious.com/jessikast/", "label": null}, {"term": "~2,000", "scheme": "http://delicious.com/jessikast/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 15:08:17 +0000", "links": [{"href": "http://constellations.labs.exalead.com/graph/", "type": "text/html", "rel": "alternate"}], "title": "scientologie - Constellations", "author": "cybunk", "comments": "http://delicious.com/url/038ac2283008371e9f60dd4aad4afd21", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "scientologie - Constellations"}, "link": "http://constellations.labs.exalead.com/graph/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/038ac2283008371e9f60dd4aad4afd21", "id": "http://delicious.com/url/038ac2283008371e9f60dd4aad4afd21#cybunk", "tags": [{"term": "cartographie", "scheme": "http://delicious.com/cybunk/", "label": null}, {"term": "searchengine", "scheme": "http://delicious.com/cybunk/", "label": null}, {"term": "internet", "scheme": "http://delicious.com/cybunk/", "label": null}, {"term": "visualisation", "scheme": "http://delicious.com/cybunk/", "label": null}, {"term": "flex", "scheme": "http://delicious.com/cybunk/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 17:43:01 +0000", "links": [{"href": "http://www.sacredlearning.org/classrooms/arabic/index.htm", "type": "text/html", "rel": "alternate"}], "title": "Fundamentals of Classical Arabic", "author": "miroque", "comments": "http://delicious.com/url/4a020bf328fd7d6012b5aa5ac85f5ac6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Fundamentals of Classical Arabic"}, "link": "http://www.sacredlearning.org/classrooms/arabic/index.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/4a020bf328fd7d6012b5aa5ac85f5ac6", "id": "http://delicious.com/url/4a020bf328fd7d6012b5aa5ac85f5ac6#miroque", "tags": [{"term": "Educational", "scheme": "http://delicious.com/miroque/", "label": null}, {"term": "Lang", "scheme": "http://delicious.com/miroque/", "label": null}, {"term": "arabic", "scheme": "http://delicious.com/miroque/", "label": null}, {"term": "Tutorials", "scheme": "http://delicious.com/miroque/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 11:27:47 +0000", "links": [{"href": "http://www.stayinvisible.com/proxy_lists.html", "type": "text/html", "rel": "alternate"}], "title": "Proxy Lists", "author": "hebz0rl", "comments": "http://delicious.com/url/76b47508e2f6281cbefd592610e3cdf1", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Proxy Lists"}, "link": "http://www.stayinvisible.com/proxy_lists.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/76b47508e2f6281cbefd592610e3cdf1", "id": "http://delicious.com/url/76b47508e2f6281cbefd592610e3cdf1#hebz0rl", "tags": [{"term": "security", "scheme": "http://delicious.com/hebz0rl/", "label": null}, {"term": "privacy", "scheme": "http://delicious.com/hebz0rl/", "label": null}, {"term": "list", "scheme": "http://delicious.com/hebz0rl/", "label": null}, {"term": "proxy", "scheme": "http://delicious.com/hebz0rl/", "label": null}, {"term": "internet", "scheme": "http://delicious.com/hebz0rl/", "label": null}, {"term": "network", "scheme": "http://delicious.com/hebz0rl/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 14:59:13 +0000", "links": [{"href": "http://www.livinghealthyreviews.com/02/cardio-twister-review/", "type": "text/html", "rel": "alternate"}], "title": "Cardio Twister Review", "author": "nataliewaterfie", "comments": "http://delicious.com/url/407e4a326c4571ecacba113cb0689336", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Cardio Twister Review"}, "link": "http://www.livinghealthyreviews.com/02/cardio-twister-review/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/407e4a326c4571ecacba113cb0689336", "id": "http://delicious.com/url/407e4a326c4571ecacba113cb0689336#nataliewaterfie", "tags": [{"term": "fitness", "scheme": "http://delicious.com/nataliewaterfie/", "label": null}, {"term": "exercise", "scheme": "http://delicious.com/nataliewaterfie/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 14:26:39 +0000", "links": [{"href": "http://www.beargrylls.com/", "type": "text/html", "rel": "alternate"}], "title": "Bear Grylls | International Speaker | Best selling Author | Everest Mountaineer | test", "author": "frandaly01", "comments": "http://delicious.com/url/b49b19cbc323cc10d9c8e2d0d0425ddf", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Bear Grylls | International Speaker | Best selling Author | Everest Mountaineer | test"}, "link": "http://www.beargrylls.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b49b19cbc323cc10d9c8e2d0d0425ddf", "id": "http://delicious.com/url/b49b19cbc323cc10d9c8e2d0d0425ddf#frandaly01", "tags": [{"term": "dissertation", "scheme": "http://delicious.com/frandaly01/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 21:50:44 +0000", "links": [{"href": "http://ceevee.com/", "type": "text/html", "rel": "alternate"}], "title": "CeeVee - quick & painless r\u00e9sum\u00e9 management", "author": "mathrandom", "comments": "http://delicious.com/url/64c1b97035ebfb953c680c115e8ec24d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "CeeVee - quick & painless r\u00e9sum\u00e9 management"}, "link": "http://ceevee.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/64c1b97035ebfb953c680c115e8ec24d", "id": "http://delicious.com/url/64c1b97035ebfb953c680c115e8ec24d#mathrandom"}, +{"updated": "Fri, 11 Sep 2009 05:11:35 +0000", "links": [{"href": "http://www.cinemek.com/", "type": "text/html", "rel": "alternate"}], "title": "Cinemek - Tools for film makers.", "author": "arhhjw", "comments": "http://delicious.com/url/8f8d11af21ea9bd9ae6ec972ed25b2f6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Cinemek - Tools for film makers."}, "link": "http://www.cinemek.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8f8d11af21ea9bd9ae6ec972ed25b2f6", "id": "http://delicious.com/url/8f8d11af21ea9bd9ae6ec972ed25b2f6#arhhjw", "tags": [{"term": "apple/mac", "scheme": "http://delicious.com/arhhjw/", "label": null}, {"term": "Movie", "scheme": "http://delicious.com/arhhjw/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 22:59:16 +0000", "links": [{"href": "http://www.instructables.com/id/EVY5I7SEFAEPYT3T8G/", "type": "text/html", "rel": "alternate"}], "title": "Another example", "author": "sammie8877", "comments": "http://delicious.com/url/2848d3f1ac38a2552236607d5d0aba20", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Another example"}, "link": "http://www.instructables.com/id/EVY5I7SEFAEPYT3T8G/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2848d3f1ac38a2552236607d5d0aba20", "id": "http://delicious.com/url/2848d3f1ac38a2552236607d5d0aba20#sammie8877", "tags": [{"term": "stumble", "scheme": "http://delicious.com/sammie8877/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 10:10:08 +0000", "links": [{"href": "http://www.slide.com/", "type": "text/html", "rel": "alternate"}], "title": "Slide - slideshows, slide shows, photo sharing, image hosting, widgets ...", "author": "saezvargas", "comments": "http://delicious.com/url/096f900e2adefc5f2c28ac3c9504396d", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Slide - slideshows, slide shows, photo sharing, image hosting, widgets ..."}, "link": "http://www.slide.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/096f900e2adefc5f2c28ac3c9504396d", "id": "http://delicious.com/url/096f900e2adefc5f2c28ac3c9504396d#saezvargas", "tags": [{"term": "visor", "scheme": "http://delicious.com/saezvargas/", "label": null}, {"term": "de", "scheme": "http://delicious.com/saezvargas/", "label": null}, {"term": "fotos,", "scheme": "http://delicious.com/saezvargas/", "label": null}, {"term": "presentaciones", "scheme": "http://delicious.com/saezvargas/", "label": null}, {"term": "diapositivas,", "scheme": "http://delicious.com/saezvargas/", "label": null}, {"term": "compartir", "scheme": "http://delicious.com/saezvargas/", "label": null}, {"term": "alojamiento", "scheme": "http://delicious.com/saezvargas/", "label": null}, {"term": "im\u00e1genes,", "scheme": "http://delicious.com/saezvargas/", "label": null}, {"term": "reproductores", "scheme": "http://delicious.com/saezvargas/", "label": null}, {"term": "...", "scheme": "http://delicious.com/saezvargas/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 09:32:41 +0000", "links": [{"href": "http://www.j5live.com/2009/09/03/introducing-kamaloka-js-amqp-javascript-bindings/", "type": "text/html", "rel": "alternate"}], "title": "J5\u2019s Blog \u00bb Introducing kamaloka-js: amqp JavaScript Bindings", "author": "lsinger", "comments": "http://delicious.com/url/c4de3dec675d03a9df261b95a4eae481", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "J5\u2019s Blog \u00bb Introducing kamaloka-js: amqp JavaScript Bindings"}, "link": "http://www.j5live.com/2009/09/03/introducing-kamaloka-js-amqp-javascript-bindings/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c4de3dec675d03a9df261b95a4eae481", "id": "http://delicious.com/url/c4de3dec675d03a9df261b95a4eae481#lsinger", "tags": [{"term": "javascript", "scheme": "http://delicious.com/lsinger/", "label": null}, {"term": "library", "scheme": "http://delicious.com/lsinger/", "label": null}, {"term": "programming", "scheme": "http://delicious.com/lsinger/", "label": null}, {"term": "framework", "scheme": "http://delicious.com/lsinger/", "label": null}, {"term": "amqp", "scheme": "http://delicious.com/lsinger/", "label": null}, {"term": "real-time", "scheme": "http://delicious.com/lsinger/", "label": null}, {"term": "messaging", "scheme": "http://delicious.com/lsinger/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 01:59:07 +0000", "links": [{"href": "http://en.wikipedia.org/wiki/Random_act_of_kindness", "type": "text/html", "rel": "alternate"}], "title": "Random act of kindness - Wikipedia, the free encyclopedia", "author": "feralape", "comments": "http://delicious.com/url/c5f891f4d749611e9d01fb54c19b3968", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Random act of kindness - Wikipedia, the free encyclopedia"}, "link": "http://en.wikipedia.org/wiki/Random_act_of_kindness", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/c5f891f4d749611e9d01fb54c19b3968", "id": "http://delicious.com/url/c5f891f4d749611e9d01fb54c19b3968#feralape"}, +{"updated": "Mon, 14 Sep 2009 08:21:47 +0000", "links": [{"href": "http://pintura.aut.org/", "type": "text/html", "rel": "alternate"}], "title": "Ciudad de la pintura - La mayor pinacoteca virtual", "author": "neconcom", "comments": "http://delicious.com/url/fa34addb33a8470cfc14a027fd36cd02", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Ciudad de la pintura - La mayor pinacoteca virtual"}, "link": "http://pintura.aut.org/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/fa34addb33a8470cfc14a027fd36cd02", "id": "http://delicious.com/url/fa34addb33a8470cfc14a027fd36cd02#neconcom", "tags": [{"term": "pintura", "scheme": "http://delicious.com/neconcom/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 05:07:43 +0000", "links": [{"href": "http://forums.dlink.com/index.php?board=155.0", "type": "text/html", "rel": "alternate"}], "title": "DLink DNS-323 Official Forum", "author": "pfferreira", "comments": "http://delicious.com/url/89f35b1d50c8da0f3ad036cddba08d01", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "DLink DNS-323 Official Forum"}, "link": "http://forums.dlink.com/index.php?board=155.0", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/89f35b1d50c8da0f3ad036cddba08d01", "id": "http://delicious.com/url/89f35b1d50c8da0f3ad036cddba08d01#pfferreira", "tags": [{"term": "dns-323", "scheme": "http://delicious.com/pfferreira/", "label": null}, {"term": "nas", "scheme": "http://delicious.com/pfferreira/", "label": null}, {"term": "networking", "scheme": "http://delicious.com/pfferreira/", "label": null}, {"term": "hardware", "scheme": "http://delicious.com/pfferreira/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 18:12:24 +0000", "links": [{"href": "https://www.chase.com/Chase.html", "type": "text/html", "rel": "alternate"}], "title": "Chase Personal Banking Investments Credit Cards Home Student Loans Auto Commercial Small Business Insurance<", "author": "brosiusad", "comments": "http://delicious.com/url/cfdd9b33289d2d05353261a340ae1845", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Chase Personal Banking Investments Credit Cards Home Student Loans Auto Commercial Small Business Insurance<"}, "link": "https://www.chase.com/Chase.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/cfdd9b33289d2d05353261a340ae1845", "id": "http://delicious.com/url/cfdd9b33289d2d05353261a340ae1845#brosiusad", "tags": [{"term": "Personal", "scheme": "http://delicious.com/brosiusad/", "label": null}, {"term": "home", "scheme": "http://delicious.com/brosiusad/", "label": null}, {"term": "finance", "scheme": "http://delicious.com/brosiusad/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 14:46:58 +0000", "links": [{"href": "http://library.duke.edu/digitalcollections/adviews/", "type": "text/html", "rel": "alternate"}], "title": "AdViews", "author": "dwilson777", "comments": "http://delicious.com/url/28159c28c1b7dab42a3098a6b7720d97", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "AdViews"}, "link": "http://library.duke.edu/digitalcollections/adviews/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/28159c28c1b7dab42a3098a6b7720d97", "id": "http://delicious.com/url/28159c28c1b7dab42a3098a6b7720d97#dwilson777", "tags": [{"term": "tv", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "video", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "history", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "television", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "media", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "culture", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "vintage", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "archives", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "advertising", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "archive", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "ads", "scheme": "http://delicious.com/dwilson777/", "label": null}, {"term": "teacherlist", "scheme": "http://delicious.com/dwilson777/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 15:16:31 +0000", "links": [{"href": "http://www.philly.com/inquirer/magazine/20090908_Classical_music_finding_intimacy_and_freedom_in_nightclub_venues.html?referrer=delicious", "type": "text/html", "rel": "alternate"}], "title": "\"Classical music finding intimacy and freedom in nightclub venues: __\" By David Patrick Stearns", "author": "sergioaugusto", "comments": "http://delicious.com/url/93752b9f82fa5198d42a9902d130e0ef", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\"Classical music finding intimacy and freedom in nightclub venues: __\" By David Patrick Stearns"}, "link": "http://www.philly.com/inquirer/magazine/20090908_Classical_music_finding_intimacy_and_freedom_in_nightclub_venues.html?referrer=delicious", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/93752b9f82fa5198d42a9902d130e0ef", "id": "http://delicious.com/url/93752b9f82fa5198d42a9902d130e0ef#sergioaugusto", "tags": [{"term": "Music", "scheme": "http://delicious.com/sergioaugusto/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 19:21:36 +0000", "links": [{"href": "http://www.imdb.com/name/nm0601619/", "type": "text/html", "rel": "alternate"}], "title": "Michael Moore (II)", "author": "aesculf", "comments": "http://delicious.com/url/73eb614a2fdfc1d58f5b982e17fefb8e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Michael Moore (II)"}, "link": "http://www.imdb.com/name/nm0601619/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/73eb614a2fdfc1d58f5b982e17fefb8e", "id": "http://delicious.com/url/73eb614a2fdfc1d58f5b982e17fefb8e#aesculf", "tags": [{"term": "commie,", "scheme": "http://delicious.com/aesculf/", "label": null}, {"term": "basterd,", "scheme": "http://delicious.com/aesculf/", "label": null}, {"term": "communist", "scheme": "http://delicious.com/aesculf/", "label": null}, {"term": "idiot", "scheme": "http://delicious.com/aesculf/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 16:17:35 +0000", "links": [{"href": "http://www.dynamicdrive.com/dynamicindex17/featuredcontentslider.htm", "type": "text/html", "rel": "alternate"}], "title": "Dynamic Drive DHTML Scripts- Featured Content Slider v2.4", "author": "GreenType", "comments": "http://delicious.com/url/7b5deb22e756723e24805223a7dbdc60", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Dynamic Drive DHTML Scripts- Featured Content Slider v2.4"}, "link": "http://www.dynamicdrive.com/dynamicindex17/featuredcontentslider.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7b5deb22e756723e24805223a7dbdc60", "id": "http://delicious.com/url/7b5deb22e756723e24805223a7dbdc60#GreenType", "tags": [{"term": "development", "scheme": "http://delicious.com/GreenType/", "label": null}, {"term": "javascript", "scheme": "http://delicious.com/GreenType/", "label": null}, {"term": "howto", "scheme": "http://delicious.com/GreenType/", "label": null}, {"term": "code", "scheme": "http://delicious.com/GreenType/", "label": null}, {"term": "script", "scheme": "http://delicious.com/GreenType/", "label": null}, {"term": "slide", "scheme": "http://delicious.com/GreenType/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 21:29:16 +0000", "links": [{"href": "http://thru-you.com/", "type": "text/html", "rel": "alternate"}], "title": "THRU YOU | Kutiman mixes YouTube", "author": "ashmueli", "comments": "http://delicious.com/url/d8337cad330d81ef882de775d8512000", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "THRU YOU | Kutiman mixes YouTube"}, "link": "http://thru-you.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d8337cad330d81ef882de775d8512000", "id": "http://delicious.com/url/d8337cad330d81ef882de775d8512000#ashmueli", "tags": [{"term": "Music", "scheme": "http://delicious.com/ashmueli/", "label": null}, {"term": "media", "scheme": "http://delicious.com/ashmueli/", "label": null}, {"term": "culture", "scheme": "http://delicious.com/ashmueli/", "label": null}, {"term": "art", "scheme": "http://delicious.com/ashmueli/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 06:15:53 +0000", "links": [{"href": "http://blog.fefe.de/?ts=b457e9d5", "type": "text/html", "rel": "alternate"}], "title": "Interwiew von Fr. Zypries mit der taz", "author": "konus77", "comments": "http://delicious.com/url/9ce5ec14ae1a8c2c26246c1a0e3b1046", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Interwiew von Fr. Zypries mit der taz"}, "link": "http://blog.fefe.de/?ts=b457e9d5", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9ce5ec14ae1a8c2c26246c1a0e3b1046", "id": "http://delicious.com/url/9ce5ec14ae1a8c2c26246c1a0e3b1046#konus77", "tags": [{"term": "gelesen", "scheme": "http://delicious.com/konus77/", "label": null}, {"term": "politik", "scheme": "http://delicious.com/konus77/", "label": null}, {"term": "wahlkampf", "scheme": "http://delicious.com/konus77/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 06:13:08 +0000", "links": [{"href": "http://vimeo.com/877053", "type": "text/html", "rel": "alternate"}], "title": "A SHORT LOVE STORY IN STOP MOTION on Vimeo", "author": "philosufi", "comments": "http://delicious.com/url/df5c483cf98f9f1e4f7cdefb13ebcdff", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "A SHORT LOVE STORY IN STOP MOTION on Vimeo"}, "link": "http://vimeo.com/877053", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/df5c483cf98f9f1e4f7cdefb13ebcdff", "id": "http://delicious.com/url/df5c483cf98f9f1e4f7cdefb13ebcdff#philosufi"}, +{"updated": "Sat, 12 Sep 2009 22:11:03 +0000", "links": [{"href": "http://smittenkitchen.com/2009/09/cheesecake-swirled-brownies/", "type": "text/html", "rel": "alternate"}], "title": "cheesecake-marbled brownies | smitten kitchen", "author": "Kuechenpsychologin", "comments": "http://delicious.com/url/bb62f3e53ed57076268b8ba37227714f", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "cheesecake-marbled brownies | smitten kitchen"}, "link": "http://smittenkitchen.com/2009/09/cheesecake-swirled-brownies/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/bb62f3e53ed57076268b8ba37227714f", "id": "http://delicious.com/url/bb62f3e53ed57076268b8ba37227714f#Kuechenpsychologin", "tags": [{"term": "Dessert", "scheme": "http://delicious.com/Kuechenpsychologin/", "label": null}, {"term": "Cheesecake", "scheme": "http://delicious.com/Kuechenpsychologin/", "label": null}, {"term": "Brownie", "scheme": "http://delicious.com/Kuechenpsychologin/", "label": null}, {"term": "Schokolade", "scheme": "http://delicious.com/Kuechenpsychologin/", "label": null}, {"term": "Ei", "scheme": "http://delicious.com/Kuechenpsychologin/", "label": null}, {"term": "Frischk\u00e4se", "scheme": "http://delicious.com/Kuechenpsychologin/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 17:43:09 +0000", "links": [{"href": "http://madrid.loquo.com/cs/comunidad/intercambio-de-idiomas/103", "type": "text/html", "rel": "alternate"}], "title": "intercambio de idiomas Madrid | anuncios clasificados gratis | Loquo Madrid", "author": "pearx_art", "comments": "http://delicious.com/url/9e2754b293c3b41da695418403e4c66e", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "intercambio de idiomas Madrid | anuncios clasificados gratis | Loquo Madrid"}, "link": "http://madrid.loquo.com/cs/comunidad/intercambio-de-idiomas/103", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9e2754b293c3b41da695418403e4c66e", "id": "http://delicious.com/url/9e2754b293c3b41da695418403e4c66e#pearx_art", "tags": [{"term": "listening", "scheme": "http://delicious.com/pearx_art/", "label": null}, {"term": "english", "scheme": "http://delicious.com/pearx_art/", "label": null}, {"term": "intercambio", "scheme": "http://delicious.com/pearx_art/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 13:43:35 +0000", "links": [{"href": "http://www.nicovideo.jp/watch/sm8167036", "type": "text/html", "rel": "alternate"}], "title": "\u3010\u30d5\u30a1\u30df\u30de\u5165\u5e97\u97f3\u3011\u30d5\u30a1\u30df\u30de\u79cb\u8449\u539f\u5e97\u306b\u5165\u3063\u305f\u3089\u30c6\u30f3\u30b7\u30e7\u30f3\u304c\u3042\u304c\u3063\u305f", "author": "satosi_m", "comments": "http://delicious.com/url/4d9c63276ee25d832acfd3f2c56466c2", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u3010\u30d5\u30a1\u30df\u30de\u5165\u5e97\u97f3\u3011\u30d5\u30a1\u30df\u30de\u79cb\u8449\u539f\u5e97\u306b\u5165\u3063\u305f\u3089\u30c6\u30f3\u30b7\u30e7\u30f3\u304c\u3042\u304c\u3063\u305f"}, "link": "http://www.nicovideo.jp/watch/sm8167036", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/4d9c63276ee25d832acfd3f2c56466c2", "id": "http://delicious.com/url/4d9c63276ee25d832acfd3f2c56466c2#satosi_m", "tags": [{"term": "\u30cb\u30b3\u30cb\u30b3\u52d5\u753b", "scheme": "http://delicious.com/satosi_m/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 17:44:12 +0000", "links": [{"href": "http://www.marthastewart.com/portal/site/mslo/menuitem.0e0eb51a2e6b5ad593598e10d373a0a0/?vgnextoid=da672e912b11f010VgnVCM1000003d370a0aRCRD&vgnextfmt=default&backto=true", "type": "text/html", "rel": "alternate"}], "title": "Felt Baby Shoes and more creative crafts projects, templates, tips, clip-art, patterns, and ideas on marthastewart.com", "author": "sikurus", "comments": "http://delicious.com/url/f406f068063791bb6f3448ca85455814", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Felt Baby Shoes and more creative crafts projects, templates, tips, clip-art, patterns, and ideas on marthastewart.com"}, "link": "http://www.marthastewart.com/portal/site/mslo/menuitem.0e0eb51a2e6b5ad593598e10d373a0a0/?vgnextoid=da672e912b11f010VgnVCM1000003d370a0aRCRD&vgnextfmt=default&backto=true", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f406f068063791bb6f3448ca85455814", "id": "http://delicious.com/url/f406f068063791bb6f3448ca85455814#sikurus", "tags": [{"term": "babycraft", "scheme": "http://delicious.com/sikurus/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 12:11:57 +0000", "links": [{"href": "http://www.berwickdawgs.com/", "type": "text/html", "rel": "alternate"}], "title": "Dawgs football", "author": "13tohl", "comments": "http://delicious.com/url/f7dbb33d7a603f1a7990de059fa23bd9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Dawgs football"}, "link": "http://www.berwickdawgs.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f7dbb33d7a603f1a7990de059fa23bd9", "id": "http://delicious.com/url/f7dbb33d7a603f1a7990de059fa23bd9#13tohl", "tags": [{"term": "berwick", "scheme": "http://delicious.com/13tohl/", "label": null}, {"term": "football", "scheme": "http://delicious.com/13tohl/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 15:06:18 +0000", "links": [{"href": "http://arstechnica.com/open-source/news/2009/09/ec-fears-oracle-will-kill-mysql-but-is-it-even-possible.ars", "type": "text/html", "rel": "alternate"}], "title": "EU fears Oracle will kill MySQL, but is that even possible? - Ars Technica", "author": "stygiansonic", "comments": "http://delicious.com/url/af3c35711df241ebcdb64c724258f2b7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "EU fears Oracle will kill MySQL, but is that even possible? - Ars Technica"}, "link": "http://arstechnica.com/open-source/news/2009/09/ec-fears-oracle-will-kill-mysql-but-is-it-even-possible.ars", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/af3c35711df241ebcdb64c724258f2b7", "id": "http://delicious.com/url/af3c35711df241ebcdb64c724258f2b7#stygiansonic", "tags": [{"term": "recent-links", "scheme": "http://delicious.com/stygiansonic/", "label": null}, {"term": "technology", "scheme": "http://delicious.com/stygiansonic/", "label": null}, {"term": "mysql", "scheme": "http://delicious.com/stygiansonic/", "label": null}, {"term": "competition", "scheme": "http://delicious.com/stygiansonic/", "label": null}, {"term": "sun", "scheme": "http://delicious.com/stygiansonic/", "label": null}, {"term": "oracle", "scheme": "http://delicious.com/stygiansonic/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 20:29:43 +0000", "links": [{"href": "http://www.instantshift.com/2009/06/29/99-amazing-widescreen-wallpapers-to-spice-up-your-desktop/", "type": "text/html", "rel": "alternate"}], "title": "99 Amazing Widescreen Wallpapers To Spice Up Your Desktop", "author": "pacifikoprinco", "comments": "http://delicious.com/url/4d1572d8f8edcbc98a7457b63a0836e3", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "99 Amazing Widescreen Wallpapers To Spice Up Your Desktop"}, "link": "http://www.instantshift.com/2009/06/29/99-amazing-widescreen-wallpapers-to-spice-up-your-desktop/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/4d1572d8f8edcbc98a7457b63a0836e3", "id": "http://delicious.com/url/4d1572d8f8edcbc98a7457b63a0836e3#pacifikoprinco", "tags": [{"term": "Wallpapers", "scheme": "http://delicious.com/pacifikoprinco/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 01:16:24 +0000", "links": [{"href": "http://wow-gem.com/gems.aspx", "type": "text/html", "rel": "alternate"}], "title": "WoW Gem Finder", "author": "rynet", "comments": "http://delicious.com/url/5716037949576e92b5d69bedf87ff8d6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "WoW Gem Finder"}, "link": "http://wow-gem.com/gems.aspx", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/5716037949576e92b5d69bedf87ff8d6", "id": "http://delicious.com/url/5716037949576e92b5d69bedf87ff8d6#rynet", "tags": [{"term": "wow", "scheme": "http://delicious.com/rynet/", "label": null}, {"term": "worldofwarcraft", "scheme": "http://delicious.com/rynet/", "label": null}, {"term": "reference", "scheme": "http://delicious.com/rynet/", "label": null}, {"term": "games", "scheme": "http://delicious.com/rynet/", "label": null}, {"term": "gaming", "scheme": "http://delicious.com/rynet/", "label": null}, {"term": "tools", "scheme": "http://delicious.com/rynet/", "label": null}, {"term": "mmo", "scheme": "http://delicious.com/rynet/", "label": null}, {"term": "mmorpg", "scheme": "http://delicious.com/rynet/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 17:40:27 +0000", "links": [{"href": "http://wave.google.com/", "type": "text/html", "rel": "alternate"}], "title": "Google Wave Preview", "author": "kyle_w", "comments": "http://delicious.com/url/b01e436cf94d2dd7d780ede9a0f39079", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Google Wave Preview"}, "link": "http://wave.google.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/b01e436cf94d2dd7d780ede9a0f39079", "id": "http://delicious.com/url/b01e436cf94d2dd7d780ede9a0f39079#kyle_w", "tags": [{"term": "google", "scheme": "http://delicious.com/kyle_w/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 13:42:00 +0000", "links": [{"href": "http://goskylar.com/2009/07/to-die-for-image-rollovers-with-jquery-parallax/", "type": "text/html", "rel": "alternate"}], "title": "Skylar Anderson | Web Design, Facebook Application Development, AJAX, User Interface | Indianapolis, IN | \u00bb To die for image rollovers with jQuery Parallax", "author": "jollino", "comments": "http://delicious.com/url/9ec9e57477098272ae9ce19a5f8a8a70", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Skylar Anderson | Web Design, Facebook Application Development, AJAX, User Interface | Indianapolis, IN | \u00bb To die for image rollovers with jQuery Parallax"}, "link": "http://goskylar.com/2009/07/to-die-for-image-rollovers-with-jquery-parallax/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/9ec9e57477098272ae9ce19a5f8a8a70", "id": "http://delicious.com/url/9ec9e57477098272ae9ce19a5f8a8a70#jollino", "tags": [{"term": "jquery", "scheme": "http://delicious.com/jollino/", "label": null}, {"term": "grafica", "scheme": "http://delicious.com/jollino/", "label": null}, {"term": "parallasse", "scheme": "http://delicious.com/jollino/", "label": null}, {"term": "tutorial", "scheme": "http://delicious.com/jollino/", "label": null}, {"term": "howto", "scheme": "http://delicious.com/jollino/", "label": null}, {"term": "javascript", "scheme": "http://delicious.com/jollino/", "label": null}, {"term": "webdesign", "scheme": "http://delicious.com/jollino/", "label": null}]}, +{"updated": "Sat, 05 Sep 2009 22:56:19 +0000", "links": [{"href": "http://news.ycombinator.com/item?id=693971", "type": "text/html", "rel": "alternate"}], "title": "Hacker News | The BBC's Glow effort has always confused me. They were using jQuery on the main...", "author": "colmbritton", "comments": "http://delicious.com/url/d1d5b7a7fe68bdb3aeb3772287354061", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Hacker News | The BBC's Glow effort has always confused me. They were using jQuery on the main..."}, "link": "http://news.ycombinator.com/item?id=693971", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d1d5b7a7fe68bdb3aeb3772287354061", "id": "http://delicious.com/url/d1d5b7a7fe68bdb3aeb3772287354061#colmbritton", "tags": [{"term": "jquery", "scheme": "http://delicious.com/colmbritton/", "label": null}, {"term": "discussion", "scheme": "http://delicious.com/colmbritton/", "label": null}, {"term": "coding", "scheme": "http://delicious.com/colmbritton/", "label": null}, {"term": "bbc", "scheme": "http://delicious.com/colmbritton/", "label": null}, {"term": "glow", "scheme": "http://delicious.com/colmbritton/", "label": null}, {"term": "development", "scheme": "http://delicious.com/colmbritton/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 11:07:58 +0000", "links": [{"href": "http://mashable.com/2009/09/05/sell-products-online/", "type": "text/html", "rel": "alternate"}], "title": "15 Places to Make Money Creating Your Own Products", "author": "angsthas3", "comments": "http://delicious.com/url/eb67bd4e44fdb44e6cec100d5c73d4cb", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "15 Places to Make Money Creating Your Own Products"}, "link": "http://mashable.com/2009/09/05/sell-products-online/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/eb67bd4e44fdb44e6cec100d5c73d4cb", "id": "http://delicious.com/url/eb67bd4e44fdb44e6cec100d5c73d4cb#angsthas3", "tags": [{"term": "shop", "scheme": "http://delicious.com/angsthas3/", "label": null}, {"term": "information", "scheme": "http://delicious.com/angsthas3/", "label": null}, {"term": "business", "scheme": "http://delicious.com/angsthas3/", "label": null}, {"term": "sehrwichtig", "scheme": "http://delicious.com/angsthas3/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 14:52:45 +0000", "links": [{"href": "http://fotobounce.com/index.php", "type": "text/html", "rel": "alternate"}], "title": "Fotobounce - Photo organizing with face recognition", "author": "maerlynne", "comments": "http://delicious.com/url/91217b7a32840125a9b4db6f0d361de9", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Fotobounce - Photo organizing with face recognition"}, "link": "http://fotobounce.com/index.php", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/91217b7a32840125a9b4db6f0d361de9", "id": "http://delicious.com/url/91217b7a32840125a9b4db6f0d361de9#maerlynne", "tags": [{"term": "free", "scheme": "http://delicious.com/maerlynne/", "label": null}, {"term": "software", "scheme": "http://delicious.com/maerlynne/", "label": null}, {"term": "photo", "scheme": "http://delicious.com/maerlynne/", "label": null}, {"term": "flickr", "scheme": "http://delicious.com/maerlynne/", "label": null}, {"term": "skip", "scheme": "http://delicious.com/maerlynne/", "label": null}]}, +{"updated": "Mon, 14 Sep 2009 04:05:50 +0000", "links": [{"href": "http://www.marthastewart.com/article/clay-flowers", "type": "text/html", "rel": "alternate"}], "title": "Clay Flowers and more creative crafts projects, templates, tips, clip-art, patterns, and ideas on marthastewart.com", "author": "jcwest47", "comments": "http://delicious.com/url/2556bfd29ebfffd2dca149d64fa1ebe5", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Clay Flowers and more creative crafts projects, templates, tips, clip-art, patterns, and ideas on marthastewart.com"}, "link": "http://www.marthastewart.com/article/clay-flowers", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/2556bfd29ebfffd2dca149d64fa1ebe5", "id": "http://delicious.com/url/2556bfd29ebfffd2dca149d64fa1ebe5#jcwest47", "tags": [{"term": "clay", "scheme": "http://delicious.com/jcwest47/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 18:07:55 +0000", "links": [{"href": "http://www.teachingliterature.org/teachingliterature/chapter1/activities.htm", "type": "text/html", "rel": "alternate"}], "title": "Teaching Literature", "author": "EducationSG", "comments": "http://delicious.com/url/85d74ace59604f9387a06ea134de9ee0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Teaching Literature"}, "link": "http://www.teachingliterature.org/teachingliterature/chapter1/activities.htm", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/85d74ace59604f9387a06ea134de9ee0", "id": "http://delicious.com/url/85d74ace59604f9387a06ea134de9ee0#EducationSG", "tags": [{"term": "teaching", "scheme": "http://delicious.com/EducationSG/", "label": null}, {"term": "literature", "scheme": "http://delicious.com/EducationSG/", "label": null}, {"term": "english", "scheme": "http://delicious.com/EducationSG/", "label": null}, {"term": "lessonplans", "scheme": "http://delicious.com/EducationSG/", "label": null}, {"term": "resources", "scheme": "http://delicious.com/EducationSG/", "label": null}, {"term": "teachingresources", "scheme": "http://delicious.com/EducationSG/", "label": null}]}, +{"updated": "Mon, 07 Sep 2009 18:58:01 +0000", "links": [{"href": "http://www.dragosroua.com/100-ways-to-improve-your-blog/", "type": "text/html", "rel": "alternate"}], "title": "100 Ways To Improve Your Blog", "author": "golomeov", "comments": "http://delicious.com/url/8fce29084a0dacb0c3915f0e15a9f3d6", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "100 Ways To Improve Your Blog"}, "link": "http://www.dragosroua.com/100-ways-to-improve-your-blog/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/8fce29084a0dacb0c3915f0e15a9f3d6", "id": "http://delicious.com/url/8fce29084a0dacb0c3915f0e15a9f3d6#golomeov"}, +{"updated": "Thu, 10 Sep 2009 18:34:25 +0000", "links": [{"href": "http://www.theoldrobots.com/index.html", "type": "text/html", "rel": "alternate"}], "title": "The Old Robot's Web Site", "author": "Andresitio", "comments": "http://delicious.com/url/d4bb52712748c57072bbee11a1bf9588", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Old Robot's Web Site"}, "link": "http://www.theoldrobots.com/index.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d4bb52712748c57072bbee11a1bf9588", "id": "http://delicious.com/url/d4bb52712748c57072bbee11a1bf9588#Andresitio", "tags": [{"term": "robots", "scheme": "http://delicious.com/Andresitio/", "label": null}, {"term": "vintage", "scheme": "http://delicious.com/Andresitio/", "label": null}, {"term": "retro", "scheme": "http://delicious.com/Andresitio/", "label": null}, {"term": "technology", "scheme": "http://delicious.com/Andresitio/", "label": null}]}, +{"updated": "Thu, 10 Sep 2009 08:45:12 +0000", "links": [{"href": "http://www.designvitality.com/blog/2007/09/photoshop-text-effect-tutorial/", "type": "text/html", "rel": "alternate"}], "title": "Photoshop Text Effects", "author": "hotpants1", "comments": "http://delicious.com/url/90c1b26e451a090452df8b947d6298cb", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Photoshop Text Effects"}, "link": "http://www.designvitality.com/blog/2007/09/photoshop-text-effect-tutorial/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/90c1b26e451a090452df8b947d6298cb", "id": "http://delicious.com/url/90c1b26e451a090452df8b947d6298cb#hotpants1", "tags": [{"term": "photoshop", "scheme": "http://delicious.com/hotpants1/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 14:30:53 +0000", "links": [{"href": "http://chompchomp.com/", "type": "text/html", "rel": "alternate"}], "title": "Grammar Bytes! Grammar Instruction with Attitude", "author": "stanleylawrence", "comments": "http://delicious.com/url/1837acc32c77784bfdc9462a83aaffbe", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "Grammar Bytes! Grammar Instruction with Attitude"}, "link": "http://chompchomp.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1837acc32c77784bfdc9462a83aaffbe", "id": "http://delicious.com/url/1837acc32c77784bfdc9462a83aaffbe#stanleylawrence", "tags": [{"term": "uenchompchomp", "scheme": "http://delicious.com/stanleylawrence/", "label": null}]}, +{"updated": "Sat, 12 Sep 2009 16:08:14 +0000", "links": [{"href": "http://mrsa.wikispaces.com/Internet+Safety+Curriculum+Resources", "type": "text/html", "rel": "alternate"}], "title": "MrsA - Internet Safety Curriculum Resources", "author": "quirkytech", "comments": "http://delicious.com/url/3363fcb177850cff2a9bacaf702a7b74", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "MrsA - Internet Safety Curriculum Resources"}, "link": "http://mrsa.wikispaces.com/Internet+Safety+Curriculum+Resources", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/3363fcb177850cff2a9bacaf702a7b74", "id": "http://delicious.com/url/3363fcb177850cff2a9bacaf702a7b74#quirkytech", "tags": [{"term": "internetsafety", "scheme": "http://delicious.com/quirkytech/", "label": null}, {"term": "web2.0", "scheme": "http://delicious.com/quirkytech/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 12:31:35 +0000", "links": [{"href": "http://www.wadoo.de/", "type": "text/html", "rel": "alternate"}], "title": "WADOO \u00b7 Home", "author": "luluivre", "comments": "http://delicious.com/url/d0b7f9114fdfd726b8339f68bf17aac7", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "WADOO \u00b7 Home"}, "link": "http://www.wadoo.de/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/d0b7f9114fdfd726b8339f68bf17aac7", "id": "http://delicious.com/url/d0b7f9114fdfd726b8339f68bf17aac7#luluivre", "tags": [{"term": "shopping", "scheme": "http://delicious.com/luluivre/", "label": null}, {"term": "presents", "scheme": "http://delicious.com/luluivre/", "label": null}]}, +{"updated": "Sun, 13 Sep 2009 23:43:43 +0000", "links": [{"href": "http://www.wb0w.com/", "type": "text/html", "rel": "alternate"}], "title": "wb0w home page", "author": "bvscumbi", "comments": "http://delicious.com/url/707efeb0a522b15141d94191a732cd89", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "wb0w home page"}, "link": "http://www.wb0w.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/707efeb0a522b15141d94191a732cd89", "id": "http://delicious.com/url/707efeb0a522b15141d94191a732cd89#bvscumbi", "tags": [{"term": "AmateurRadio", "scheme": "http://delicious.com/bvscumbi/", "label": null}, {"term": "radio", "scheme": "http://delicious.com/bvscumbi/", "label": null}, {"term": "antenna", "scheme": "http://delicious.com/bvscumbi/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 19:53:56 +0000", "links": [{"href": "http://research.hrc.utexas.edu/poedc/", "type": "text/html", "rel": "alternate"}], "title": "The Edgar Allan Poe Digital Collection", "author": "VanishedOne", "comments": "http://delicious.com/url/1f868d81205c440bd5225bca2aadfd99", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "The Edgar Allan Poe Digital Collection"}, "link": "http://research.hrc.utexas.edu/poedc/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/1f868d81205c440bd5225bca2aadfd99", "id": "http://delicious.com/url/1f868d81205c440bd5225bca2aadfd99#VanishedOne", "tags": [{"term": "literature", "scheme": "http://delicious.com/VanishedOne/", "label": null}, {"term": "EdgarAllanPoe", "scheme": "http://delicious.com/VanishedOne/", "label": null}]}, +{"updated": "Tue, 08 Sep 2009 16:25:57 +0000", "links": [{"href": "http://www.video2mp3.net/", "type": "text/html", "rel": "alternate"}], "title": "YouTube to MP3 Converter - Video2mp3", "author": "bboocc", "comments": "http://delicious.com/url/7cbfde2079bfeff0f98932aede53bed0", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "YouTube to MP3 Converter - Video2mp3"}, "link": "http://www.video2mp3.net/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/7cbfde2079bfeff0f98932aede53bed0", "id": "http://delicious.com/url/7cbfde2079bfeff0f98932aede53bed0#bboocc", "tags": [{"term": "convercao", "scheme": "http://delicious.com/bboocc/", "label": null}]}, +{"updated": "Sun, 06 Sep 2009 03:59:51 +0000", "links": [{"href": "http://www.switchersblog.com/", "type": "text/html", "rel": "alternate"}], "title": "1Password - Switchers' Blog", "author": "woggit", "comments": "http://delicious.com/url/544fd7997be38db4ced75fcec24ffa46", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "1Password - Switchers' Blog"}, "link": "http://www.switchersblog.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/544fd7997be38db4ced75fcec24ffa46", "id": "http://delicious.com/url/544fd7997be38db4ced75fcec24ffa46#woggit", "tags": [{"term": "apple", "scheme": "http://delicious.com/woggit/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 07:03:52 +0000", "links": [{"href": "http://www.youtube.com/watch?v=5nzhV0x3_qM&feature=related", "type": "text/html", "rel": "alternate"}], "title": "YouTube - Pablo Valbuena, Augmented Sculpture v. 1.2", "author": "clausmo", "comments": "http://delicious.com/url/124c428c94a98ded786126b4a918c3d8", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "YouTube - Pablo Valbuena, Augmented Sculpture v. 1.2"}, "link": "http://www.youtube.com/watch?v=5nzhV0x3_qM&feature=related", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/124c428c94a98ded786126b4a918c3d8", "id": "http://delicious.com/url/124c428c94a98ded786126b4a918c3d8#clausmo", "tags": [{"term": "projection", "scheme": "http://delicious.com/clausmo/", "label": null}, {"term": "valbuena", "scheme": "http://delicious.com/clausmo/", "label": null}, {"term": "arselectronica", "scheme": "http://delicious.com/clausmo/", "label": null}]}, +{"updated": "Wed, 09 Sep 2009 10:58:35 +0000", "links": [{"href": "http://www.baidu.com/", "type": "text/html", "rel": "alternate"}], "title": "\u767e\u5ea6\u4e00\u4e0b\uff0c\u4f60\u5c31\u77e5\u9053", "author": "falkograu", "comments": "http://delicious.com/url/f03f5717616221de41881be555473a02", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "\u767e\u5ea6\u4e00\u4e0b\uff0c\u4f60\u5c31\u77e5\u9053"}, "link": "http://www.baidu.com/", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/f03f5717616221de41881be555473a02", "id": "http://delicious.com/url/f03f5717616221de41881be555473a02#falkograu", "tags": [{"term": "search", "scheme": "http://delicious.com/falkograu/", "label": null}, {"term": "tools", "scheme": "http://delicious.com/falkograu/", "label": null}, {"term": "internet", "scheme": "http://delicious.com/falkograu/", "label": null}, {"term": "portal", "scheme": "http://delicious.com/falkograu/", "label": null}, {"term": "searchengine", "scheme": "http://delicious.com/falkograu/", "label": null}, {"term": "suchmaschine", "scheme": "http://delicious.com/falkograu/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 19:30:23 +0000", "links": [{"href": "http://online.wsj.com/article/SB10001424052970203440104574404893691325078.html", "type": "text/html", "rel": "alternate"}], "title": "President Obama Fudges Truth on Medicare - WSJ.com", "author": "GreenWizard", "comments": "http://delicious.com/url/ee488adb263077f8ffaa188fd6af9752", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "President Obama Fudges Truth on Medicare - WSJ.com"}, "link": "http://online.wsj.com/article/SB10001424052970203440104574404893691325078.html", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/ee488adb263077f8ffaa188fd6af9752", "id": "http://delicious.com/url/ee488adb263077f8ffaa188fd6af9752#GreenWizard", "tags": [{"term": "obamashow", "scheme": "http://delicious.com/GreenWizard/", "label": null}, {"term": "medicare", "scheme": "http://delicious.com/GreenWizard/", "label": null}]}, +{"updated": "Fri, 11 Sep 2009 15:53:01 +0000", "links": [{"href": "http://www.htmlgoodies.com/beyond/css/article.php/3642151", "type": "text/html", "rel": "alternate"}], "title": "CSS Layouts Without Tables - www.htmlgoodies.com", "author": "rpoor", "comments": "http://delicious.com/url/0225356192e5f6779e3b92f745d82924", "guidislink": false, "title_detail": {"base": "http://feeds.delicious.com/v2/rss/recent?min=1&count=100", "type": "text/plain", "language": null, "value": "CSS Layouts Without Tables - www.htmlgoodies.com"}, "link": "http://www.htmlgoodies.com/beyond/css/article.php/3642151", "source": {}, "wfw_commentrss": "http://feeds.delicious.com/v2/rss/url/0225356192e5f6779e3b92f745d82924", "id": "http://delicious.com/url/0225356192e5f6779e3b92f745d82924#rpoor", "tags": [{"term": "css", "scheme": "http://delicious.com/rpoor/", "label": null}, {"term": "tables", "scheme": "http://delicious.com/rpoor/", "label": null}]}] diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 7e866e3c3d..705b595180 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1277,6 +1277,7 @@ JsonEncoding JsonFormat JsonFormatType JsonHashEntry +JsonIncrementalState JsonIsPredicate JsonIterateStringValuesAction JsonKeyValue @@ -1284,15 +1285,19 @@ JsonLexContext JsonLikeRegexContext JsonManifestFileField JsonManifestParseContext +JsonManifestParseIncrementalState JsonManifestParseState JsonManifestSemanticState JsonManifestWALRangeField +JsonNonTerminal JsonObjectAgg JsonObjectConstructor JsonOutput JsonParseExpr JsonParseContext JsonParseErrorType +JsonParserSem +JsonParserStack JsonPath JsonPathBool JsonPathExecContext -- 2.34.1 [text/x-patch] v8-0002-Add-support-for-incrementally-parsing-backup-mani.patch (7.6K, ../../[email protected]/3-v8-0002-Add-support-for-incrementally-parsing-backup-mani.patch) download | inline diff: From 645127e193dfe556c51b45947d31340e4dc92217 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan <[email protected]> Date: Sat, 13 Jan 2024 08:16:11 -0500 Subject: [PATCH v8 2/5] Add support for incrementally parsing backup manifests This adds the infrastructure for using the new non-recusrive JSON parser in processing manifests. It's important that callers make sure that the last piece of json handed to the incremental manifest parser contains the entire last few lines of the manifest, including the checksum. --- src/common/parse_manifest.c | 120 ++++++++++++++++++++++++++-- src/include/common/parse_manifest.h | 5 ++ 2 files changed, 117 insertions(+), 8 deletions(-) diff --git a/src/common/parse_manifest.c b/src/common/parse_manifest.c index 92a97714f3..ef68b41726 100644 --- a/src/common/parse_manifest.c +++ b/src/common/parse_manifest.c @@ -88,6 +88,13 @@ typedef struct char *manifest_checksum; } JsonManifestParseState; +typedef struct JsonManifestParseIncrementalState +{ + JsonLexContext lex; + JsonSemAction sem; + pg_cryptohash_ctx *manifest_ctx; +} JsonManifestParseIncrementalState; + static JsonParseErrorType json_manifest_object_start(void *state); static JsonParseErrorType json_manifest_object_end(void *state); static JsonParseErrorType json_manifest_array_start(void *state); @@ -99,7 +106,8 @@ static JsonParseErrorType json_manifest_scalar(void *state, char *token, static void json_manifest_finalize_file(JsonManifestParseState *parse); static void json_manifest_finalize_wal_range(JsonManifestParseState *parse); static void verify_manifest_checksum(JsonManifestParseState *parse, - char *buffer, size_t size); + char *buffer, size_t size, + pg_cryptohash_ctx *incr_ctx); static void json_manifest_parse_failure(JsonManifestParseContext *context, char *msg); @@ -107,6 +115,89 @@ static int hexdecode_char(char c); static bool hexdecode_string(uint8 *result, char *input, int nbytes); static bool parse_xlogrecptr(XLogRecPtr *result, char *input); +/* + * Set up for incremental parsing of the manifest. + * + */ + +JsonManifestParseIncrementalState * +json_parse_manifest_incremental_init(JsonManifestParseContext *context) +{ + JsonManifestParseIncrementalState *incstate; + JsonManifestParseState *parse; + pg_cryptohash_ctx *manifest_ctx; + + incstate = palloc(sizeof(JsonManifestParseIncrementalState)); + parse = palloc(sizeof(JsonManifestParseState)); + + parse->context = context; + parse->state = JM_EXPECT_TOPLEVEL_START; + parse->saw_version_field = false; + + makeJsonLexContextIncremental(&(incstate->lex), PG_UTF8, true); + + incstate->sem.semstate = parse; + incstate->sem.object_start = json_manifest_object_start; + incstate->sem.object_end = json_manifest_object_end; + incstate->sem.array_start = json_manifest_array_start; + incstate->sem.array_end = json_manifest_array_end; + incstate->sem.object_field_start = json_manifest_object_field_start; + incstate->sem.object_field_end = NULL; + incstate->sem.array_element_start = NULL; + incstate->sem.array_element_end = NULL; + incstate->sem.scalar = json_manifest_scalar; + + manifest_ctx = pg_cryptohash_create(PG_SHA256); + if (manifest_ctx == NULL) + context->error_cb(context, "out of memory"); + if (pg_cryptohash_init(manifest_ctx) < 0) + context->error_cb(context, "could not initialize checksum of manifest"); + incstate->manifest_ctx = manifest_ctx; + + return incstate; +} + +/* + * parse the manifest in pieces. + * + * The caller must ensure that the final piece contains the final lines + * with the complete checksum. + */ + +void +json_parse_manifest_incremental_chunk( + JsonManifestParseIncrementalState *incstate, char *chunk, int size, + bool is_last) +{ + JsonParseErrorType res, + expected; + JsonManifestParseState *parse = incstate->sem.semstate; + JsonManifestParseContext *context = parse->context; + + res = pg_parse_json_incremental(&(incstate->lex), &(incstate->sem), + chunk, size, is_last); + + expected = is_last ? JSON_SUCCESS : JSON_INCOMPLETE; + + if (res != expected) + json_manifest_parse_failure(context, "parsing failed"); + + if (is_last && parse->state != JM_EXPECT_EOF) + json_manifest_parse_failure(context, "manifest ended unexpectedly"); + + if (!is_last) + { + if (pg_cryptohash_update(incstate->manifest_ctx, + (uint8 *) chunk, size) < 0) + context->error_cb(context, "could not update checksum of manifest"); + } + else + { + verify_manifest_checksum(parse, chunk, size, incstate->manifest_ctx); + } +} + + /* * Main entrypoint to parse a JSON-format backup manifest. * @@ -152,7 +243,7 @@ json_parse_manifest(JsonManifestParseContext *context, char *buffer, json_manifest_parse_failure(context, "manifest ended unexpectedly"); /* Verify the manifest checksum. */ - verify_manifest_checksum(&parse, buffer, size); + verify_manifest_checksum(&parse, buffer, size, NULL); freeJsonLexContext(lex); } @@ -378,6 +469,8 @@ json_manifest_object_field_start(void *state, char *fname, bool isnull) break; } + pfree(fname); + return JSON_SUCCESS; } @@ -628,10 +721,14 @@ json_manifest_finalize_wal_range(JsonManifestParseState *parse) * The last line of the manifest file is excluded from the manifest checksum, * because the last line is expected to contain the checksum that covers * the rest of the file. + * + * For an incremental parse, this will just be called on the last chunk of the + * manifest, and the cryptohash context paswed in. For a non-incremental + * parse incr_ctx will be NULL. */ static void verify_manifest_checksum(JsonManifestParseState *parse, char *buffer, - size_t size) + size_t size, pg_cryptohash_ctx *incr_ctx) { JsonManifestParseContext *context = parse->context; size_t i; @@ -666,11 +763,18 @@ verify_manifest_checksum(JsonManifestParseState *parse, char *buffer, "last line not newline-terminated"); /* Checksum the rest. */ - manifest_ctx = pg_cryptohash_create(PG_SHA256); - if (manifest_ctx == NULL) - context->error_cb(context, "out of memory"); - if (pg_cryptohash_init(manifest_ctx) < 0) - context->error_cb(context, "could not initialize checksum of manifest"); + if (incr_ctx == NULL) + { + manifest_ctx = pg_cryptohash_create(PG_SHA256); + if (manifest_ctx == NULL) + context->error_cb(context, "out of memory"); + if (pg_cryptohash_init(manifest_ctx) < 0) + context->error_cb(context, "could not initialize checksum of manifest"); + } + else + { + manifest_ctx = incr_ctx; + } if (pg_cryptohash_update(manifest_ctx, (uint8 *) buffer, penultimate_newline + 1) < 0) context->error_cb(context, "could not update checksum of manifest"); if (pg_cryptohash_final(manifest_ctx, manifest_checksum_actual, diff --git a/src/include/common/parse_manifest.h b/src/include/common/parse_manifest.h index f74be0db35..73db43662f 100644 --- a/src/include/common/parse_manifest.h +++ b/src/include/common/parse_manifest.h @@ -20,6 +20,7 @@ struct JsonManifestParseContext; typedef struct JsonManifestParseContext JsonManifestParseContext; +typedef struct JsonManifestParseIncrementalState JsonManifestParseIncrementalState; typedef void (*json_manifest_per_file_callback) (JsonManifestParseContext *, char *pathname, @@ -42,5 +43,9 @@ struct JsonManifestParseContext extern void json_parse_manifest(JsonManifestParseContext *context, char *buffer, size_t size); +extern JsonManifestParseIncrementalState *json_parse_manifest_incremental_init(JsonManifestParseContext *context); +extern void json_parse_manifest_incremental_chunk( + JsonManifestParseIncrementalState *incstate, char *chunk, int size, + bool is_last); #endif -- 2.34.1 [text/x-patch] v8-0003-Use-incremental-parsing-of-backup-manifests.patch (11.5K, ../../[email protected]/4-v8-0003-Use-incremental-parsing-of-backup-manifests.patch) download | inline diff: From e6c9d41205599d9e622858a8699e47e3c5e9136e Mon Sep 17 00:00:00 2001 From: Andrew Dunstan <[email protected]> Date: Sat, 13 Jan 2024 08:16:41 -0500 Subject: [PATCH v8 3/5] Use incremental parsing of backup manifests. This changes the three callers to json_parse_manifest() to use json_parse_manifest_incremental_chunk() if appropriate. In the case of the backend caller, since we don't know the size of the manifest in advance we always call the incremental parser. --- src/backend/backup/basebackup_incremental.c | 53 ++++++++---- src/bin/pg_combinebackup/load_manifest.c | 93 ++++++++++++++++----- src/bin/pg_verifybackup/pg_verifybackup.c | 91 ++++++++++++++------ 3 files changed, 177 insertions(+), 60 deletions(-) diff --git a/src/backend/backup/basebackup_incremental.c b/src/backend/backup/basebackup_incremental.c index 0504c465db..9e1973b2d3 100644 --- a/src/backend/backup/basebackup_incremental.c +++ b/src/backend/backup/basebackup_incremental.c @@ -31,6 +31,14 @@ #define BLOCKS_PER_READ 512 +/* + * we expect the find the last lines of the manifest, including the checksum, + * in the last MIN_CHUNK bytes of the manifest. We trigger an incremental + * parse step if we are about to overflow MAX_CHUNK bytes. + */ +#define MIN_CHUNK 1024 +#define MAX_CHUNK (128 * 1024) + /* * Details extracted from the WAL ranges present in the supplied backup manifest. */ @@ -110,6 +118,11 @@ struct IncrementalBackupInfo * turns out to be a problem in practice, we'll need to be more clever. */ BlockRefTable *brtab; + + /* + * State object for incremental JSON parsing + */ + JsonManifestParseIncrementalState *inc_state; }; static void manifest_process_file(JsonManifestParseContext *context, @@ -136,6 +149,7 @@ CreateIncrementalBackupInfo(MemoryContext mcxt) { IncrementalBackupInfo *ib; MemoryContext oldcontext; + JsonManifestParseContext *context; oldcontext = MemoryContextSwitchTo(mcxt); @@ -151,6 +165,15 @@ CreateIncrementalBackupInfo(MemoryContext mcxt) */ ib->manifest_files = backup_file_create(mcxt, 10000, NULL); + context = palloc0(sizeof(JsonManifestParseContext)); + /* Parse the manifest. */ + context->private_data = ib; + context->per_file_cb = manifest_process_file; + context->per_wal_range_cb = manifest_process_wal_range; + context->error_cb = manifest_report_error; + + ib->inc_state = json_parse_manifest_incremental_init(context); + MemoryContextSwitchTo(oldcontext); return ib; @@ -170,13 +193,19 @@ AppendIncrementalManifestData(IncrementalBackupInfo *ib, const char *data, /* Switch to our memory context. */ oldcontext = MemoryContextSwitchTo(ib->mcxt); - /* - * XXX. Our json parser is at present incapable of parsing json blobs - * incrementally, so we have to accumulate the entire backup manifest - * before we can do anything with it. This should really be fixed, since - * some users might have very large numbers of files in the data - * directory. - */ + if (ib->buf.len >= MIN_CHUNK && ib->buf.len + len > MAX_CHUNK) + { + /* + * time for an incremental parse. We'll do all but the last but so + * that we have enough left for the final piece. + */ + json_parse_manifest_incremental_chunk( + ib->inc_state, ib->buf.data, ib->buf.len - MIN_CHUNK, false); + /* now remove what we just parsed */ + memmove(ib->buf.data, ib->buf.data + (ib->buf.len - MIN_CHUNK), MIN_CHUNK + 1); + ib->buf.len = MIN_CHUNK; + } + appendBinaryStringInfo(&ib->buf, data, len); /* Switch back to previous memory context. */ @@ -190,18 +219,14 @@ AppendIncrementalManifestData(IncrementalBackupInfo *ib, const char *data, void FinalizeIncrementalManifest(IncrementalBackupInfo *ib) { - JsonManifestParseContext context; MemoryContext oldcontext; /* Switch to our memory context. */ oldcontext = MemoryContextSwitchTo(ib->mcxt); - /* Parse the manifest. */ - context.private_data = ib; - context.per_file_cb = manifest_process_file; - context.per_wal_range_cb = manifest_process_wal_range; - context.error_cb = manifest_report_error; - json_parse_manifest(&context, ib->buf.data, ib->buf.len); + /* parse the last chunk of the manifest */ + json_parse_manifest_incremental_chunk( + ib->inc_state, ib->buf.data, ib->buf.len, true); /* Done with the buffer, so release memory. */ pfree(ib->buf.data); diff --git a/src/bin/pg_combinebackup/load_manifest.c b/src/bin/pg_combinebackup/load_manifest.c index 2b8e74fcf3..982be78e28 100644 --- a/src/bin/pg_combinebackup/load_manifest.c +++ b/src/bin/pg_combinebackup/load_manifest.c @@ -34,6 +34,12 @@ */ #define ESTIMATED_BYTES_PER_MANIFEST_LINE 100 +/* + * size of json chunk to be read in + * + */ +#define READ_CHUNK_SIZE (128 * 1024) + /* * Define a hash table which we can use to store information about the files * mentioned in the backup manifest. @@ -105,6 +111,7 @@ load_backup_manifest(char *backup_directory) int rc; JsonManifestParseContext context; manifest_data *result; + int chunk_size = READ_CHUNK_SIZE; /* Open the manifest file. */ snprintf(pathname, MAXPGPATH, "%s/backup_manifest", backup_directory); @@ -129,34 +136,76 @@ load_backup_manifest(char *backup_directory) /* Create the hash table. */ ht = manifest_files_create(initial_size, NULL); - /* - * Slurp in the whole file. - * - * This is not ideal, but there's currently no way to get pg_parse_json() - * to perform incremental parsing. - */ - buffer = pg_malloc(statbuf.st_size); - rc = read(fd, buffer, statbuf.st_size); - if (rc != statbuf.st_size) - { - if (rc < 0) - pg_fatal("could not read file \"%s\": %m", pathname); - else - pg_fatal("could not read file \"%s\": read %d of %lld", - pathname, rc, (long long int) statbuf.st_size); - } - - /* Close the manifest file. */ - close(fd); - - /* Parse the manifest. */ result = pg_malloc0(sizeof(manifest_data)); result->files = ht; context.private_data = result; context.per_file_cb = combinebackup_per_file_cb; context.per_wal_range_cb = combinebackup_per_wal_range_cb; context.error_cb = report_manifest_error; - json_parse_manifest(&context, buffer, statbuf.st_size); + + /* + * Parse the file, in chunks if necessary. + */ + if (statbuf.st_size <= chunk_size) + { + buffer = pg_malloc(statbuf.st_size); + rc = read(fd, buffer, statbuf.st_size); + if (rc != statbuf.st_size) + { + if (rc < 0) + pg_fatal("could not read file \"%s\": %m", pathname); + else + pg_fatal("could not read file \"%s\": read %d of %lld", + pathname, rc, (long long int) statbuf.st_size); + } + + /* Close the manifest file. */ + close(fd); + + /* Parse the manifest. */ + json_parse_manifest(&context, buffer, statbuf.st_size); + } + else + { + int bytes_left = statbuf.st_size; + JsonManifestParseIncrementalState *inc_state; + + inc_state = json_parse_manifest_incremental_init(&context); + + buffer = pg_malloc(chunk_size + 1); + + while (bytes_left > 0) + { + int bytes_to_read = chunk_size; + + /* + * Make sure that the last chunk is sufficiently large. (i.e. at + * least half the chunk size) so that it will contain fully the + * piece at the end with the checksum. + */ + if (bytes_left < chunk_size) + bytes_to_read = bytes_left; + else if (bytes_left < 2 * chunk_size) + bytes_to_read = bytes_left / 2; + rc = read(fd, buffer, bytes_to_read); + buffer[rc] = '\0'; /* useful for writing log traces */ + if (rc != bytes_to_read) + { + if (rc < 0) + pg_fatal("could not read file \"%s\": %m", pathname); + else + pg_fatal("could not read file \"%s\": read %lld of %lld", + pathname, + (long long int)(statbuf.st_size + rc - bytes_left), + (long long int) statbuf.st_size); + } + bytes_left -= rc; + json_parse_manifest_incremental_chunk( + inc_state, buffer, rc, bytes_left == 0); + } + + close(fd); + } /* All done. */ pfree(buffer); diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c index ae8c18f373..02b160f9fc 100644 --- a/src/bin/pg_verifybackup/pg_verifybackup.c +++ b/src/bin/pg_verifybackup/pg_verifybackup.c @@ -42,7 +42,7 @@ /* * How many bytes should we try to read from a file at once? */ -#define READ_CHUNK_SIZE 4096 +#define READ_CHUNK_SIZE (128 * 1024) /* * Each file described by the manifest file is parsed to produce an object @@ -399,6 +399,8 @@ parse_manifest_file(char *manifest_path, manifest_files_hash **ht_p, parser_context private_context; JsonManifestParseContext context; + int chunk_size = READ_CHUNK_SIZE; + /* Open the manifest file. */ if ((fd = open(manifest_path, O_RDONLY | PG_BINARY, 0)) < 0) report_fatal_error("could not open file \"%s\": %m", manifest_path); @@ -414,28 +416,6 @@ parse_manifest_file(char *manifest_path, manifest_files_hash **ht_p, /* Create the hash table. */ ht = manifest_files_create(initial_size, NULL); - /* - * Slurp in the whole file. - * - * This is not ideal, but there's currently no easy way to get - * pg_parse_json() to perform incremental parsing. - */ - buffer = pg_malloc(statbuf.st_size); - rc = read(fd, buffer, statbuf.st_size); - if (rc != statbuf.st_size) - { - if (rc < 0) - report_fatal_error("could not read file \"%s\": %m", - manifest_path); - else - report_fatal_error("could not read file \"%s\": read %d of %lld", - manifest_path, rc, (long long int) statbuf.st_size); - } - - /* Close the manifest file. */ - close(fd); - - /* Parse the manifest. */ private_context.ht = ht; private_context.first_wal_range = NULL; private_context.last_wal_range = NULL; @@ -443,7 +423,70 @@ parse_manifest_file(char *manifest_path, manifest_files_hash **ht_p, context.per_file_cb = verifybackup_per_file_cb; context.per_wal_range_cb = verifybackup_per_wal_range_cb; context.error_cb = report_manifest_error; - json_parse_manifest(&context, buffer, statbuf.st_size); + + /* + * Parse the file, in chunks if necessary. + */ + if (statbuf.st_size <= chunk_size) + { + buffer = pg_malloc(statbuf.st_size); + rc = read(fd, buffer, statbuf.st_size); + if (rc != statbuf.st_size) + { + if (rc < 0) + pg_fatal("could not read file \"%s\": %m", manifest_path); + else + pg_fatal("could not read file \"%s\": read %d of %lld", + manifest_path, rc, (long long int) statbuf.st_size); + } + + /* Close the manifest file. */ + close(fd); + + /* Parse the manifest. */ + json_parse_manifest(&context, buffer, statbuf.st_size); + } + else + { + int bytes_left = statbuf.st_size; + JsonManifestParseIncrementalState *inc_state; + + inc_state = json_parse_manifest_incremental_init(&context); + + buffer = pg_malloc(chunk_size + 1); + + while (bytes_left > 0) + { + int bytes_to_read = chunk_size; + + /* + * Make sure that the last chunk is sufficiently large. (i.e. at + * least half the chunk size) so that it will contain fully the + * piece at the end with the checksum. + */ + if (bytes_left < chunk_size) + bytes_to_read = bytes_left; + else if (bytes_left < 2 * chunk_size) + bytes_to_read = bytes_left / 2; + rc = read(fd, buffer, bytes_to_read); + buffer[rc] = '\0'; /* useful for writing log traces */ + if (rc != bytes_to_read) + { + if (rc < 0) + pg_fatal("could not read file \"%s\": %m", manifest_path); + else + pg_fatal("could not read file \"%s\": read %lld of %lld", + manifest_path, + (long long int)(statbuf.st_size + rc - bytes_left), + (long long int) statbuf.st_size); + } + bytes_left -= rc; + json_parse_manifest_incremental_chunk( + inc_state, buffer, rc, bytes_left == 0); + } + + close(fd); + } /* Done with the buffer. */ pfree(buffer); -- 2.34.1 [text/x-patch] v8-0004-add-logging-traces-to-see-if-we-can-find-out-what.patch (3.3K, ../../[email protected]/5-v8-0004-add-logging-traces-to-see-if-we-can-find-out-what.patch) download | inline diff: From 1796070f97c661c750b958209055f91b971e4123 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan <[email protected]> Date: Fri, 26 Jan 2024 12:09:48 -0500 Subject: [PATCH v8 4/5] add logging traces to see if we can find out what is upsetting the cfbot --- src/backend/backup/basebackup_incremental.c | 15 ++++++++++++++- src/bin/pg_combinebackup/t/003_timeline.pl | 2 ++ src/common/parse_manifest.c | 7 +++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/backend/backup/basebackup_incremental.c b/src/backend/backup/basebackup_incremental.c index 9e1973b2d3..c23bb40bc9 100644 --- a/src/backend/backup/basebackup_incremental.c +++ b/src/backend/backup/basebackup_incremental.c @@ -193,12 +193,18 @@ AppendIncrementalManifestData(IncrementalBackupInfo *ib, const char *data, /* Switch to our memory context. */ oldcontext = MemoryContextSwitchTo(ib->mcxt); - if (ib->buf.len >= MIN_CHUNK && ib->buf.len + len > MAX_CHUNK) + if (ib->buf.len > MIN_CHUNK && ib->buf.len + len > MAX_CHUNK) { /* * time for an incremental parse. We'll do all but the last but so * that we have enough left for the final piece. */ + char chunk_start[100], chunk_end[100]; + + snprintf(chunk_start, 100, "%s", ib->buf.data); + snprintf(chunk_end, 100, "%s", ib->buf.data + (ib->buf.len - (MIN_CHUNK + 99))); + elog(NOTICE,"incremental manifest:\nchunk_start='%s',\nchunk_end='%s'", chunk_start, chunk_end); + json_parse_manifest_incremental_chunk( ib->inc_state, ib->buf.data, ib->buf.len - MIN_CHUNK, false); /* now remove what we just parsed */ @@ -224,6 +230,13 @@ FinalizeIncrementalManifest(IncrementalBackupInfo *ib) /* Switch to our memory context. */ oldcontext = MemoryContextSwitchTo(ib->mcxt); + { + char chunk_start[100]; + + snprintf(chunk_start, 100, "%s", ib->buf.data); + elog(NOTICE,"incremental manifest:\nfinal chunk_start='%s'", chunk_start); + } + /* parse the last chunk of the manifest */ json_parse_manifest_incremental_chunk( ib->inc_state, ib->buf.data, ib->buf.len, true); diff --git a/src/bin/pg_combinebackup/t/003_timeline.pl b/src/bin/pg_combinebackup/t/003_timeline.pl index 82bd886c87..243081e134 100644 --- a/src/bin/pg_combinebackup/t/003_timeline.pl +++ b/src/bin/pg_combinebackup/t/003_timeline.pl @@ -35,6 +35,8 @@ EOM # Now take an incremental backup. my $backup2path = $node1->backup_dir . '/backup2'; +my $manifest = slurp_file("$backup1path/backup_manifest"); +note "$backup1path/backup_manifest:\n$manifest"; $node1->command_ok( [ 'pg_basebackup', '-D', $backup2path, '--no-sync', '-cfast', '--incremental', $backup1path . '/backup_manifest' ], diff --git a/src/common/parse_manifest.c b/src/common/parse_manifest.c index ef68b41726..774039bb66 100644 --- a/src/common/parse_manifest.c +++ b/src/common/parse_manifest.c @@ -607,8 +607,11 @@ json_manifest_finalize_file(JsonManifestParseState *parse) /* Parse size. */ size = strtoul(parse->size, &ep, 10); if (*ep) - json_manifest_parse_failure(parse->context, - "file size is not an integer"); + { + char msg[200]; + snprintf(msg, 200, "file size is not an integer: '%s'", parse->size); + json_manifest_parse_failure(parse->context, msg); + } /* Parse the checksum algorithm, if it's present. */ if (parse->algorithm == NULL) -- 2.34.1 [text/x-patch] v8-0005-fixes-for-non-null-terminated-inputs-for-incremen.patch (6.1K, ../../[email protected]/6-v8-0005-fixes-for-non-null-terminated-inputs-for-incremen.patch) download | inline diff: From 86d0bcfe2bb6752c3cf773d28eb7c201cb41bdf0 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan <[email protected]> Date: Thu, 22 Feb 2024 03:04:41 -0500 Subject: [PATCH v8 5/5] fixes for non-null terminated inputs for incremental json parsing --- src/bin/pg_combinebackup/load_manifest.c | 5 ++- src/bin/pg_verifybackup/pg_verifybackup.c | 5 ++- src/common/jsonapi.c | 43 ++++++++++++++----- .../test_json_parser_incremental.c | 5 ++- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/bin/pg_combinebackup/load_manifest.c b/src/bin/pg_combinebackup/load_manifest.c index 982be78e28..ae73d01190 100644 --- a/src/bin/pg_combinebackup/load_manifest.c +++ b/src/bin/pg_combinebackup/load_manifest.c @@ -172,7 +172,7 @@ load_backup_manifest(char *backup_directory) inc_state = json_parse_manifest_incremental_init(&context); - buffer = pg_malloc(chunk_size + 1); + buffer = pg_malloc(chunk_size + 64); while (bytes_left > 0) { @@ -188,7 +188,6 @@ load_backup_manifest(char *backup_directory) else if (bytes_left < 2 * chunk_size) bytes_to_read = bytes_left / 2; rc = read(fd, buffer, bytes_to_read); - buffer[rc] = '\0'; /* useful for writing log traces */ if (rc != bytes_to_read) { if (rc < 0) @@ -199,6 +198,8 @@ load_backup_manifest(char *backup_directory) (long long int)(statbuf.st_size + rc - bytes_left), (long long int) statbuf.st_size); } + /* exercise non-null-terminated chunks */ + strcpy(buffer + rc, "1+23 trailing junk"); bytes_left -= rc; json_parse_manifest_incremental_chunk( inc_state, buffer, rc, bytes_left == 0); diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c index 02b160f9fc..6eaa376bf0 100644 --- a/src/bin/pg_verifybackup/pg_verifybackup.c +++ b/src/bin/pg_verifybackup/pg_verifybackup.c @@ -453,7 +453,7 @@ parse_manifest_file(char *manifest_path, manifest_files_hash **ht_p, inc_state = json_parse_manifest_incremental_init(&context); - buffer = pg_malloc(chunk_size + 1); + buffer = pg_malloc(chunk_size + 64); while (bytes_left > 0) { @@ -469,7 +469,6 @@ parse_manifest_file(char *manifest_path, manifest_files_hash **ht_p, else if (bytes_left < 2 * chunk_size) bytes_to_read = bytes_left / 2; rc = read(fd, buffer, bytes_to_read); - buffer[rc] = '\0'; /* useful for writing log traces */ if (rc != bytes_to_read) { if (rc < 0) @@ -480,6 +479,8 @@ parse_manifest_file(char *manifest_path, manifest_files_hash **ht_p, (long long int)(statbuf.st_size + rc - bytes_left), (long long int) statbuf.st_size); } + /* test for non-null terminated chunk */ + strcpy(buffer + rc, "1+23 trailing junk"); bytes_left -= rc; json_parse_manifest_incremental_chunk( inc_state, buffer, rc, bytes_left == 0); diff --git a/src/common/jsonapi.c b/src/common/jsonapi.c index 25fca8851d..11a22faa18 100644 --- a/src/common/jsonapi.c +++ b/src/common/jsonapi.c @@ -1317,14 +1317,37 @@ json_lex(JsonLexContext *lex) if (c == '-' || (c >= '0' && c <= '9')) { /* for numbers look for possible numeric continuations */ - size_t nums = strspn(lex->input, "+-.eE0123456789"); - for (int i = 0; i < nums; i++) + bool numend = false; + + for (int i = 0; i < lex->input_length && !numend; i++) { char cc = lex->input[i]; - appendStringInfoCharMacro(ptok, cc); - added++; + switch (cc) + { + case '+': + case '-': + case 'e': + case 'E': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + appendStringInfoCharMacro(ptok, cc); + added++; + } + break; + default: + numend = true; + } } } /* add any remaining alpha_numeric chars */ @@ -1496,8 +1519,8 @@ json_lex(JsonLexContext *lex) if (lex->incremental && !lex->inc_state->is_last_chunk && p == lex->input + lex->input_length) { - appendStringInfoString( - &(lex->inc_state->partial_token), s); + appendBinaryStringInfo( + &(lex->inc_state->partial_token), s, end - s); return JSON_INCOMPLETE; } @@ -1554,8 +1577,8 @@ json_lex_string(JsonLexContext *lex) do { \ if (lex->incremental && !lex->inc_state->is_last_chunk) \ { \ - appendStringInfoString(&lex->inc_state->partial_token, \ - lex->token_start); \ + appendBinaryStringInfo(&lex->inc_state->partial_token, \ + lex->token_start, end - lex->token_start); \ return JSON_INCOMPLETE; \ } \ lex->token_terminator = s; \ @@ -1893,8 +1916,8 @@ json_lex_number(JsonLexContext *lex, char *s, if (lex->incremental && !lex->inc_state->is_last_chunk && len >= lex->input_length) { - appendStringInfoString(&lex->inc_state->partial_token, - lex->token_start); + appendBinaryStringInfo(&lex->inc_state->partial_token, + lex->token_start, s - lex->token_start); return JSON_INCOMPLETE; } else if (num_err != NULL) diff --git a/src/test/modules/test_json_parser/test_json_parser_incremental.c b/src/test/modules/test_json_parser/test_json_parser_incremental.c index edb51ef403..dee5c6f7d1 100644 --- a/src/test/modules/test_json_parser/test_json_parser_incremental.c +++ b/src/test/modules/test_json_parser/test_json_parser_incremental.c @@ -42,10 +42,11 @@ main(int argc, char **argv) while ((n_read = fread(buff, 1, 60, json_file)) > 0) { appendBinaryStringInfo(&json, buff, n_read); + appendStringInfoString(&json, "1+23 trailing junk"); if (!feof(json_file)) { result = pg_parse_json_incremental(&lex, &nullSemAction, - json.data, json.len, + json.data, n_read, false); if (result != JSON_INCOMPLETE) { @@ -59,7 +60,7 @@ main(int argc, char **argv) else { result = pg_parse_json_incremental(&lex, &nullSemAction, - json.data, json.len, + json.data, n_read, true); if (result != JSON_SUCCESS) { -- 2.34.1 ^ permalink raw reply [nested|flat] 107+ messages in thread
end of thread, other threads:[~2024-02-22 09:38 UTC | newest] Thread overview: 107+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v44 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-07 08:04 [PATCH v45 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v49 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v48 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2021-01-20 15:53 [PATCH v46 3/3] Replace assuming a composite object on a scalar Dmitrii Dolgov <[email protected]> 2024-02-21 20:26 Re: WIP Incremental JSON Parser Jacob Champion <[email protected]> 2024-02-22 09:38 ` Re: WIP Incremental JSON Parser Andrew Dunstan <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox