diff --git a/udfs/community/README.md b/udfs/community/README.md index fab63f82e..839a50eea 100644 --- a/udfs/community/README.md +++ b/udfs/community/README.md @@ -130,6 +130,7 @@ SELECT bqutil.fn.int(1.684) * [jaccard](#jaccard) * [job_url](#job_urljob_id-string) * [json_extract_keys](#json_extract_keys) +* [json_extract_key_value_pairs](#json_extract_key_value_pairs) * [json_extract_values](#json_extract_values) * [json_typeof](#json_typeofjson-string) * [knots_to_mph](#knots_to_mphinput_knots-float64) @@ -1389,6 +1390,25 @@ bar hat ``` +### [json_extract_key_value_pairs()](json_extract_key_value_pairs.sqlx) +Returns all key/values pairs in the input JSON as an array +of STRUCT +Returns NULL if invalid JSON string is passed, + + +```sql +SELECT * FROM UNNEST( + bqutil.fn.json_extract_key_value_pairs( + '{"foo" : "cat", "bar": [1,2,3], "hat": {"qux": true}}' + ) +) + +key,value +foo,"cat" +bar,[1,2,3] +hat,{"qux":true} +``` + ### [json_extract_values()](json_extract_values.sqlx) Returns all values in the input JSON as an array of string Returns NULL if invalid JSON string is passed, diff --git a/udfs/community/json_extract_key_value_pairs.sqlx b/udfs/community/json_extract_key_value_pairs.sqlx new file mode 100644 index 000000000..5e9aec73b --- /dev/null +++ b/udfs/community/json_extract_key_value_pairs.sqlx @@ -0,0 +1,51 @@ +config { hasOutput: true } +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CREATE OR REPLACE FUNCTION ${self()}(json_str STRING) +RETURNS ARRAY> +LANGUAGE js +OPTIONS( + description = """Returns all key/values pairs in the input JSON as an array +of STRUCT +Returns NULL if invalid JSON string is passed, + + +```sql +SELECT * FROM UNNEST( + bqutil.fn.json_extract_key_value_pairs( + '{"foo" : "cat", "bar": [1,2,3], "hat": {"qux": true}}' + ) +) + +key,value +foo,"cat" +bar,[1,2,3] +hat,{"qux":true} +``` +""" +) +AS """ + try { + return Object.entries(JSON.parse(json_str)) + .map(([k, v])=> Object.fromEntries([ + ["key", k], + ["value", JSON.stringify(v)] + ])); + } catch { + return null; + } +"""; diff --git a/udfs/community/test_cases.js b/udfs/community/test_cases.js index 5357a3cb8..38b2dd8ec 100644 --- a/udfs/community/test_cases.js +++ b/udfs/community/test_cases.js @@ -58,6 +58,24 @@ generate_udf_test("json_extract_keys", [ expected_output: `cast(null as array)`, }, ]); +generate_udf_test("json_extract_key_value_pairs", [ + { + inputs: [`'{"foo" : "cat", "bar": 42, "hat": [1,2,3], "qux": {"bat": true}}'`], + expected_output: `([STRUCT("foo" AS key, '"cat"' AS value), STRUCT("bar" AS key, "42" AS value), STRUCT("hat" AS key, "[1,2,3]" AS value), STRUCT("qux" AS key, '{"bat":true}' AS value)])` + }, + { + inputs: [`'{}'`], + expected_output: `[]` + }, + { + inputs: [`'invalid_json'`], + expected_output: `cast(null as array>)`, + }, + { + inputs: [`string(null)`], + expected_output: `cast(null as array>)`, + }, +]); generate_udf_test("json_extract_values", [ { inputs: [`'{"foo" : "cat", "bar": "dog", "hat": "rat"}'`],