| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -47,6 +47,7 @@ | |||
| 47 | 47 | json_extract, | |
| 48 | 48 | json_extract_array, | |
| 49 | 49 | json_extract_string_array, | |
| 50 | + json_keys, | ||
| 50 | 51 | json_query, | |
| 51 | 52 | json_query_array, | |
| 52 | 53 | json_set, | |
@@ -138,6 +139,7 @@ | |||
| 138 | 139 | "json_extract", | |
| 139 | 140 | "json_extract_array", | |
| 140 | 141 | "json_extract_string_array", | |
| 142 | + "json_keys", | ||
| 141 | 143 | "json_query", | |
| 142 | 144 | "json_query_array", | |
| 143 | 145 | "json_set", | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -421,6 +421,35 @@ def json_value_array( | |||
| 421 | 421 | return input._apply_unary_op(ops.JSONValueArray(json_path=json_path)) | |
| 422 | 422 | ||
| 423 | 423 | ||
| 424 | + def json_keys( | ||
| 425 | + input: series.Series, | ||
| 426 | + max_depth: Optional[int] = None, | ||
| 427 | + ) -> series.Series: | ||
| 428 | + """Returns all keys in the root of a JSON object as an ARRAY of STRINGs. | ||
| 429 | + | ||
| 430 | + **Examples:** | ||
| 431 | + | ||
| 432 | + >>> import bigframes.pandas as bpd | ||
| 433 | + >>> import bigframes.bigquery as bbq | ||
| 434 | + | ||
| 435 | + >>> s = bpd.Series(['{"b": {"c": 2}, "a": 1}'], dtype="json") | ||
| 436 | + >>> bbq.json_keys(s) | ||
| 437 | + 0 ['a' 'b' 'b.c'] | ||
| 438 | + dtype: list<item: string>[pyarrow] | ||
| 439 | + | ||
| 440 | + Args: | ||
| 441 | + input (bigframes.series.Series): | ||
| 442 | + The Series containing JSON data. | ||
| 443 | + max_depth (int, optional): | ||
| 444 | + Specifies the maximum depth of nested fields to search for keys. If not | ||
| 445 | + provided, searched keys at all levels. | ||
| 446 | + | ||
| 447 | + Returns: | ||
| 448 | + bigframes.series.Series: A new Series containing arrays of keys from the input JSON. | ||
| 449 | + """ | ||
| 450 | + return input._apply_unary_op(ops.JSONKeys(max_depth=max_depth)) | ||
| 451 | + | ||
| 452 | + | ||
| 424 | 453 | def to_json( | |
| 425 | 454 | input: series.Series, | |
| 426 | 455 | ) -> series.Series: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1234,6 +1234,11 @@ def json_value_array_op_impl(x: ibis_types.Value, op: ops.JSONValueArray): | |||
| 1234 | 1234 | return json_value_array(json_obj=x, json_path=op.json_path) | |
| 1235 | 1235 | ||
| 1236 | 1236 | ||
| 1237 | + @scalar_op_compiler.register_unary_op(ops.JSONKeys, pass_op=True) | ||
| 1238 | + def json_keys_op_impl(x: ibis_types.Value, op: ops.JSONKeys): | ||
| 1239 | + return json_keys(x, op.max_depth) | ||
| 1240 | + | ||
| 1241 | + | ||
| 1237 | 1242 | # Blob Ops | |
| 1238 | 1243 | @scalar_op_compiler.register_unary_op(ops.obj_fetch_metadata_op) | |
| 1239 | 1244 | def obj_fetch_metadata_op_impl(obj_ref: ibis_types.Value): | |
@@ -2059,6 +2064,14 @@ def to_json_string(value) -> ibis_dtypes.String: # type: ignore[empty-body] | |||
| 2059 | 2064 | """Convert value to JSON-formatted string.""" | |
| 2060 | 2065 | ||
| 2061 | 2066 | ||
| 2067 | + @ibis_udf.scalar.builtin(name="json_keys") | ||
| 2068 | + def json_keys( # type: ignore[empty-body] | ||
| 2069 | + json_obj: ibis_dtypes.JSON, | ||
| 2070 | + max_depth: ibis_dtypes.Int64, | ||
| 2071 | + ) -> ibis_dtypes.Array[ibis_dtypes.String]: | ||
| 2072 | + """Extracts unique JSON keys from a JSON expression.""" | ||
| 2073 | + | ||
| 2074 | + | ||
| 2062 | 2075 | @ibis_udf.scalar.builtin(name="json_value") | |
| 2063 | 2076 | def json_value( # type: ignore[empty-body] | |
| 2064 | 2077 | json_obj: ibis_dtypes.JSON, json_path: ibis_dtypes.String | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -39,6 +39,11 @@ def _(expr: TypedExpr, op: ops.JSONExtractStringArray) -> sge.Expression: | |||
| 39 | 39 | return sge.func("JSON_EXTRACT_STRING_ARRAY", expr.expr, sge.convert(op.json_path)) | |
| 40 | 40 | ||
| 41 | 41 | ||
| 42 | + @register_unary_op(ops.JSONKeys, pass_op=True) | ||
| 43 | + def _(expr: TypedExpr, op: ops.JSONKeys) -> sge.Expression: | ||
| 44 | + return sge.func("JSON_KEYS", expr.expr, sge.convert(op.max_depth)) | ||
| 45 | + | ||
| 46 | + | ||
| 42 | 47 | @register_unary_op(ops.JSONQuery, pass_op=True) | |
| 43 | 48 | def _(expr: TypedExpr, op: ops.JSONQuery) -> sge.Expression: | |
| 44 | 49 | return sge.func("JSON_QUERY", expr.expr, sge.convert(op.json_path)) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -128,6 +128,7 @@ | |||
| 128 | 128 | JSONExtract, | |
| 129 | 129 | JSONExtractArray, | |
| 130 | 130 | JSONExtractStringArray, | |
| 131 | + JSONKeys, | ||
| 131 | 132 | JSONQuery, | |
| 132 | 133 | JSONQueryArray, | |
| 133 | 134 | JSONSet, | |
@@ -381,6 +382,7 @@ | |||
| 381 | 382 | "JSONExtract", | |
| 382 | 383 | "JSONExtractArray", | |
| 383 | 384 | "JSONExtractStringArray", | |
| 385 | + "JSONKeys", | ||
| 384 | 386 | "JSONQuery", | |
| 385 | 387 | "JSONQueryArray", | |
| 386 | 388 | "JSONSet", | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -199,6 +199,23 @@ def output_type(self, *input_types): | |||
| 199 | 199 | return input_type | |
| 200 | 200 | ||
| 201 | 201 | ||
| 202 | + @dataclasses.dataclass(frozen=True) | ||
| 203 | + class JSONKeys(base_ops.UnaryOp): | ||
| 204 | + name: typing.ClassVar[str] = "json_keys" | ||
| 205 | + max_depth: typing.Optional[int] = None | ||
| 206 | + | ||
| 207 | + def output_type(self, *input_types): | ||
| 208 | + input_type = input_types[0] | ||
| 209 | + if input_type != dtypes.JSON_DTYPE: | ||
| 210 | + raise TypeError( | ||
| 211 | + "Input type must be a valid JSON object or JSON-formatted string type." | ||
| 212 | + + f" Received type: {input_type}" | ||
| 213 | + ) | ||
| 214 | + return pd.ArrowDtype( | ||
| 215 | + pa.list_(dtypes.bigframes_dtype_to_arrow_dtype(dtypes.STRING_DTYPE)) | ||
| 216 | + ) | ||
| 217 | + | ||
| 218 | + | ||
| 202 | 219 | @dataclasses.dataclass(frozen=True) | |
| 203 | 220 | class JSONDecode(base_ops.UnaryOp): | |
| 204 | 221 | name: typing.ClassVar[str] = "json_decode" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -434,3 +434,53 @@ def test_to_json_string_from_struct(): | |||
| 434 | 434 | ) | |
| 435 | 435 | ||
| 436 | 436 | pd.testing.assert_series_equal(actual.to_pandas(), expected.to_pandas()) | |
| 437 | + | ||
| 438 | + | ||
| 439 | + def test_json_keys(): | ||
| 440 | + json_data = [ | ||
| 441 | + '{"name": "Alice", "age": 30}', | ||
| 442 | + '{"city": "New York", "country": "USA", "active": true}', | ||
| 443 | + "{}", | ||
| 444 | + '{"items": [1, 2, 3]}', | ||
| 445 | + ] | ||
| 446 | + s = bpd.Series(json_data, dtype=dtypes.JSON_DTYPE) | ||
| 447 | + actual = bbq.json_keys(s) | ||
| 448 | + | ||
| 449 | + expected_data_pandas = [ | ||
| 450 | + ["age", "name"], | ||
| 451 | + [ | ||
| 452 | + "active", | ||
| 453 | + "city", | ||
| 454 | + "country", | ||
| 455 | + ], | ||
| 456 | + [], | ||
| 457 | + ["items"], | ||
| 458 | + ] | ||
| 459 | + expected = bpd.Series( | ||
| 460 | + expected_data_pandas, dtype=pd.ArrowDtype(pa.list_(pa.string())) | ||
| 461 | + ) | ||
| 462 | + pd.testing.assert_series_equal(actual.to_pandas(), expected.to_pandas()) | ||
| 463 | + | ||
| 464 | + | ||
| 465 | + def test_json_keys_with_max_depth(): | ||
| 466 | + json_data = [ | ||
| 467 | + '{"user": {"name": "Bob", "details": {"id": 123, "status": "approved"}}}', | ||
| 468 | + '{"user": {"name": "Charlie"}}', | ||
| 469 | + ] | ||
| 470 | + s = bpd.Series(json_data, dtype=dtypes.JSON_DTYPE) | ||
| 471 | + actual = bbq.json_keys(s, max_depth=2) | ||
| 472 | + | ||
| 473 | + expected_data_pandas = [ | ||
| 474 | + ["user", "user.details", "user.name"], | ||
| 475 | + ["user", "user.name"], | ||
| 476 | + ] | ||
| 477 | + expected = bpd.Series( | ||
| 478 | + expected_data_pandas, dtype=pd.ArrowDtype(pa.list_(pa.string())) | ||
| 479 | + ) | ||
| 480 | + pd.testing.assert_series_equal(actual.to_pandas(), expected.to_pandas()) | ||
| 481 | + | ||
| 482 | + | ||
| 483 | + def test_json_keys_from_string_error(): | ||
| 484 | + s = bpd.Series(['{"a": 1, "b": 2}', '{"c": 3}']) | ||
| 485 | + with pytest.raises(TypeError): | ||
| 486 | + bbq.json_keys(s) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,15 @@ | |||
| 1 | + WITH `bfcte_0` AS ( | ||
| 2 | + SELECT | ||
| 3 | + `json_col` | ||
| 4 | + FROM `bigframes-dev`.`sqlglot_test`.`json_types` | ||
| 5 | + ), `bfcte_1` AS ( | ||
| 6 | + SELECT | ||
| 7 | + *, | ||
| 8 | + JSON_KEYS(`json_col`, NULL) AS `bfcol_1`, | ||
| 9 | + JSON_KEYS(`json_col`, 2) AS `bfcol_2` | ||
| 10 | + FROM `bfcte_0` | ||
| 11 | + ) | ||
| 12 | + SELECT | ||
| 13 | + `bfcol_1` AS `json_keys`, | ||
| 14 | + `bfcol_2` AS `json_keys_w_max_depth` | ||
| 15 | + FROM `bfcte_1` | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -52,6 +52,19 @@ def test_json_extract_string_array(json_types_df: bpd.DataFrame, snapshot): | |||
| 52 | 52 | snapshot.assert_match(sql, "out.sql") | |
| 53 | 53 | ||
| 54 | 54 | ||
| 55 | + def test_json_keys(json_types_df: bpd.DataFrame, snapshot): | ||
| 56 | + col_name = "json_col" | ||
| 57 | + bf_df = json_types_df[[col_name]] | ||
| 58 | + | ||
| 59 | + ops_map = { | ||
| 60 | + "json_keys": ops.JSONKeys().as_expr(col_name), | ||
| 61 | + "json_keys_w_max_depth": ops.JSONKeys(max_depth=2).as_expr(col_name), | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + sql = utils._apply_ops_to_sql(bf_df, list(ops_map.values()), list(ops_map.keys())) | ||
| 65 | + snapshot.assert_match(sql, "out.sql") | ||
| 66 | + | ||
| 67 | + | ||
| 55 | 68 | def test_json_query(json_types_df: bpd.DataFrame, snapshot): | |
| 56 | 69 | col_name = "json_col" | |
| 57 | 70 | bf_df = json_types_df[[col_name]] | |
| Back | FazBrowse Home | New Git URL |
0 commit comments