Skip to content

Commit 4e7720a

Browse files
SNOW-2155042 Add --format JSON_EXT that will expand nested JSONs (#2458)
* SNOW-2155042 Properly display nested JSONs * update RN * fix integration test * fix mocks * Use ENUMs * revert test changes * make json expanding opt-in * make expand_json globally available * ambr files, but I'm not sure if we shouldn't add format JSON_2 or something * one more * Replace --expand-json flag with JSON_EXT format option * ambr * review fixes
1 parent 168fd1f commit 4e7720a

File tree

20 files changed

+2846
-2416
lines changed

20 files changed

+2846
-2416
lines changed

RELEASE-NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
## Fixes and improvements
4444
* Fixed failing snow sql command when snowflake.yml is invalid and query has no templating.
4545
* Fix JSON serialization for `Decimal`, `time` and `binary`.
46+
* Added `--format=JSON_EXT` option to return JSON objects as proper JSON structures rather than strings.
4647
* Refactored Streamlit app deployment (using `FROM <stage>` syntax); removed deprecated Streamlit features
4748

4849

src/snowflake/cli/_app/printing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def __to_str(val):
104104

105105

106106
def is_structured_format(output_format):
107-
return output_format in [OutputFormat.JSON, OutputFormat.CSV]
107+
return output_format.is_json or output_format == OutputFormat.CSV
108108

109109

110110
def print_structured(

src/snowflake/cli/_plugins/nativeapp/commands.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
IncompatibleParametersError,
5656
UnmetParametersError,
5757
)
58-
from snowflake.cli.api.output.formats import OutputFormat
5958
from snowflake.cli.api.output.types import (
6059
CommandResult,
6160
MessageResult,
@@ -117,9 +116,9 @@ def app_diff(
117116
diff = ws.perform_action(
118117
package_id,
119118
EntityActions.DIFF,
120-
print_to_console=cli_context.output_format != OutputFormat.JSON,
119+
print_to_console=not cli_context.output_format.is_json,
121120
)
122-
if cli_context.output_format == OutputFormat.JSON:
121+
if cli_context.output_format.is_json:
123122
return ObjectResult(diff.to_dict())
124123

125124
return None
@@ -373,7 +372,7 @@ def app_validate(
373372
)
374373
package_id = options["package_entity_id"]
375374
package = ws.get_entity(package_id)
376-
if cli_context.output_format == OutputFormat.JSON:
375+
if cli_context.output_format.is_json:
377376
return ObjectResult(
378377
package.get_validation_result(
379378
action_ctx=ws.action_ctx,

src/snowflake/cli/_plugins/nativeapp/release_channel/commands.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from snowflake.cli.api.commands.decorators import with_project_definition
2727
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
2828
from snowflake.cli.api.entities.utils import EntityActions
29-
from snowflake.cli.api.output.formats import OutputFormat
3029
from snowflake.cli.api.output.types import (
3130
CollectionResult,
3231
CommandResult,
@@ -68,7 +67,7 @@ def release_channel_list(
6867
release_channel=channel,
6968
)
7069

71-
if cli_context.output_format == OutputFormat.JSON:
70+
if cli_context.output_format.is_json:
7271
return CollectionResult(channels)
7372

7473

src/snowflake/cli/_plugins/nativeapp/version/commands.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
)
3131
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
3232
from snowflake.cli.api.entities.utils import EntityActions
33-
from snowflake.cli.api.output.formats import OutputFormat
3433
from snowflake.cli.api.output.types import (
3534
CollectionResult,
3635
CommandResult,
@@ -105,7 +104,7 @@ def create(
105104
)
106105

107106
message = "Version create is now complete."
108-
if cli_context.output_format == OutputFormat.JSON:
107+
if cli_context.output_format.is_json:
109108
return ObjectResult(
110109
{
111110
"message": message,

src/snowflake/cli/_plugins/stage/commands.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
from snowflake.cli.api.console import cli_console
4949
from snowflake.cli.api.constants import ObjectType
5050
from snowflake.cli.api.identifiers import FQN
51-
from snowflake.cli.api.output.formats import OutputFormat
5251
from snowflake.cli.api.output.types import (
5352
CollectionResult,
5453
CommandResult,
@@ -219,7 +218,7 @@ def stage_diff(
219218
local_root=Path(folder_name),
220219
stage_path=StageManager.stage_path_parts_from_str(stage_name), # noqa: SLF001
221220
)
222-
if get_cli_context().output_format == OutputFormat.JSON:
221+
if get_cli_context().output_format.is_json:
223222
return ObjectResult(diff.to_dict())
224223
else:
225224
print_diff_to_console(diff)

src/snowflake/cli/api/cli_global_context.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,10 @@ def silent(self) -> bool:
196196
@property
197197
def _should_force_mute_intermediate_output(self) -> bool:
198198
"""Computes whether cli_console output should be muted."""
199-
return self._manager.output_format in [OutputFormat.JSON, OutputFormat.CSV]
199+
return (
200+
self._manager.output_format.is_json
201+
or self._manager.output_format == OutputFormat.CSV
202+
)
200203

201204
@property
202205
def enhanced_exit_codes(self) -> bool:

src/snowflake/cli/api/commands/flags.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ def _diag_log_allowlist_path_callback(path: str):
446446
rich_help_panel=_CLI_BEHAVIOUR,
447447
)
448448

449+
449450
SilentOption = typer.Option(
450451
False,
451452
"--silent",

src/snowflake/cli/api/output/formats.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@
1818
class OutputFormat(Enum):
1919
TABLE = "TABLE"
2020
JSON = "JSON"
21+
JSON_EXT = "JSON_EXT"
2122
CSV = "CSV"
23+
24+
@property
25+
def is_json(self) -> bool:
26+
return self in (OutputFormat.JSON, OutputFormat.JSON_EXT)

src/snowflake/cli/api/output/types.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,22 @@
1616

1717
import json
1818
import typing as t
19+
from enum import IntEnum
1920

21+
from snowflake.cli.api.cli_global_context import get_cli_context
22+
from snowflake.cli.api.output.formats import OutputFormat
2023
from snowflake.connector import DictCursor
2124
from snowflake.connector.cursor import SnowflakeCursor
2225

2326

27+
class SnowflakeColumnType(IntEnum):
28+
"""Snowflake column type codes for JSON-capable data types."""
29+
30+
VARIANT = 5
31+
OBJECT = 9
32+
ARRAY = 10
33+
34+
2435
class CommandResult:
2536
@property
2637
def result(self):
@@ -69,13 +80,48 @@ def result(self):
6980
class QueryResult(CollectionResult):
7081
def __init__(self, cursor: SnowflakeCursor | DictCursor):
7182
self.column_names = [col.name for col in cursor.description]
83+
# Store column type information to identify VARIANT columns (JSON data)
84+
self.column_types = [col.type_code for col in cursor.description]
7285
super().__init__(elements=self._prepare_payload(cursor))
7386
self._query = cursor.query
7487

7588
def _prepare_payload(self, cursor: SnowflakeCursor | DictCursor):
7689
if isinstance(cursor, DictCursor):
77-
return (k for k in cursor)
78-
return ({k: v for k, v in zip(self.column_names, row)} for row in cursor)
90+
return (self._process_columns(k) for k in cursor)
91+
return (
92+
self._process_columns({k: v for k, v in zip(self.column_names, row)})
93+
for row in cursor
94+
)
95+
96+
def _process_columns(self, row_dict):
97+
if get_cli_context().output_format != OutputFormat.JSON_EXT:
98+
return row_dict
99+
100+
processed_row = {}
101+
for i, (column_name, value) in enumerate(row_dict.items()):
102+
# Check if this column can contain JSON data
103+
if i < len(self.column_types) and self.column_types[i] in (
104+
SnowflakeColumnType.VARIANT,
105+
SnowflakeColumnType.OBJECT,
106+
SnowflakeColumnType.ARRAY,
107+
):
108+
# For ARRAY and OBJECT types, the values are always JSON strings that need parsing
109+
# For VARIANT types, only parse if the value is a string
110+
if self.column_types[i] in (
111+
SnowflakeColumnType.OBJECT,
112+
SnowflakeColumnType.ARRAY,
113+
) or isinstance(value, str):
114+
try:
115+
# Try to parse as JSON
116+
processed_row[column_name] = json.loads(value)
117+
except (json.JSONDecodeError, TypeError):
118+
# If parsing fails, keep the original value
119+
processed_row[column_name] = value
120+
else:
121+
processed_row[column_name] = value
122+
else:
123+
processed_row[column_name] = value
124+
return processed_row
79125

80126
@property
81127
def query(self):

0 commit comments

Comments
 (0)