Skip to content

Commit 03095a6

Browse files
SNOW-2162936 Add --refresh option to snow stage copy (#2448)
* SNOW-2162936 Add `--refresh` option to `snow stage copy` * update `test_help_messages.ambr` * Add `--enable-directory` * Add log about refreshing stage * update RELEASE-NOTES.md * explicit assertion
1 parent 3326668 commit 03095a6

File tree

5 files changed

+146
-9
lines changed

5 files changed

+146
-9
lines changed

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ def copy(
117117
default=False,
118118
help="Specifies whether Snowflake uses gzip to compress files during upload. Ignored when downloading.",
119119
),
120+
refresh: bool = typer.Option(
121+
default=False,
122+
help="Specifies whether ALTER STAGE {name} REFRESH should be executed after uploading.",
123+
),
120124
**options,
121125
) -> CommandResult:
122126
"""
@@ -150,6 +154,7 @@ def copy(
150154
parallel=parallel,
151155
overwrite=overwrite,
152156
auto_compress=auto_compress,
157+
refresh=refresh,
153158
)
154159

155160

@@ -161,12 +166,19 @@ def stage_create(
161166
"--encryption",
162167
help="Type of encryption supported for all files stored on the stage.",
163168
),
169+
enable_directory: bool = typer.Option(
170+
False,
171+
"--enable-directory",
172+
help="Specifies whether directory support is enabled for the stage.",
173+
),
164174
**options,
165175
) -> CommandResult:
166176
"""
167177
Creates a named stage if it does not already exist.
168178
"""
169-
cursor = StageManager().create(fqn=stage_name, encryption=encryption)
179+
cursor = StageManager().create(
180+
fqn=stage_name, encryption=encryption, enable_directory=enable_directory
181+
)
170182
return SingleQueryResult(cursor)
171183

172184

@@ -265,6 +277,7 @@ def _put(
265277
parallel: int,
266278
overwrite: bool,
267279
auto_compress: bool,
280+
refresh: bool,
268281
):
269282
if recursive and not source_path.is_file():
270283
cursor_generator = StageManager().put_recursive(
@@ -274,7 +287,7 @@ def _put(
274287
parallel=parallel,
275288
auto_compress=auto_compress,
276289
)
277-
return CollectionResult(cursor_generator)
290+
output = CollectionResult(cursor_generator)
278291
else:
279292
cursor = StageManager().put(
280293
local_path=source_path.resolve(),
@@ -283,4 +296,10 @@ def _put(
283296
parallel=parallel,
284297
auto_compress=auto_compress,
285298
)
286-
return QueryResult(cursor)
299+
output = QueryResult(cursor)
300+
301+
if refresh:
302+
StageManager().refresh(
303+
StageManager.stage_path_parts_from_str(destination_path).stage_name
304+
)
305+
return output

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,11 +548,14 @@ def create(
548548
comment: Optional[str] = None,
549549
temporary: bool = False,
550550
encryption: InternalStageEncryptionType | None = None,
551+
enable_directory: bool = False,
551552
) -> SnowflakeCursor:
552553
temporary_str = "temporary " if temporary else ""
553554
query = f"create {temporary_str}stage if not exists {fqn.sql_identifier}"
554555
if encryption:
555556
query += f" encryption = (type = '{encryption.value}')"
557+
if enable_directory:
558+
query += f" directory = (enable = true)"
556559
if comment:
557560
query += f" comment='{comment}'"
558561
return self.execute_query(query)
@@ -768,6 +771,11 @@ def stage_path_parts_from_str(stage_path: str) -> StagePathParts:
768771
return VStagePathParts(stage_path)
769772
return DefaultStagePathParts(stage_path)
770773

774+
def refresh(self, stage_name):
775+
sql = f"ALTER STAGE {stage_name} REFRESH"
776+
log.info("Refreshing stage %s", stage_name)
777+
return self.execute_query(sql)
778+
771779
def _check_for_requirements_file(self, stage_path: StagePath) -> List[str]:
772780
"""Looks for requirements.txt file on stage."""
773781
current_dir = stage_path.parent if stage_path.is_file() else stage_path

tests/__snapshots__/test_help_messages.ambr

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17958,6 +17958,11 @@
1795817958
| downloading. |
1795917959
| [default: |
1796017960
| no-auto-compress] |
17961+
| --refresh --no-refresh Specifies whether ALTER |
17962+
| STAGE {name} REFRESH |
17963+
| should be executed after |
17964+
| uploading. |
17965+
| [default: no-refresh] |
1796117966
| --help -h Show this message and |
1796217967
| exit. |
1796317968
+------------------------------------------------------------------------------+
@@ -18086,12 +18091,16 @@
1808618091
| [required] |
1808718092
+------------------------------------------------------------------------------+
1808818093
+- Options --------------------------------------------------------------------+
18089-
| --encryption [SNOWFLAKE_FULL|SNOWFLAKE_ Type of encryption |
18090-
| SSE] supported for all files |
18091-
| stored on the stage. |
18092-
| [default: SNOWFLAKE_FULL] |
18093-
| --help -h Show this message and |
18094-
| exit. |
18094+
| --encryption [SNOWFLAKE_FULL|SNOWFL Type of encryption |
18095+
| AKE_SSE] supported for all files |
18096+
| stored on the stage. |
18097+
| [default: |
18098+
| SNOWFLAKE_FULL] |
18099+
| --enable-directory Specifies whether |
18100+
| directory support is |
18101+
| enabled for the stage. |
18102+
| --help -h Show this message and |
18103+
| exit. |
1809518104
+------------------------------------------------------------------------------+
1809618105
+- Connection configuration ---------------------------------------------------+
1809718106
| --connection,--environment -c TEXT Name of the connection, as |

tests/stage/test_stage.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,3 +1427,34 @@ def test_recursive_upload_with_provided_temp_directory():
14271427
tester.prepare(structure=NESTED_STRUCTURE)
14281428
StageManager().copy_to_tmp_dir(Path(source_directory), temp_directory_path)
14291429
tester.execute(local_path=source_directory)
1430+
1431+
1432+
@mock.patch(f"{STAGE_MANAGER}.execute_query")
1433+
def test_stage_create_enable_directory(mock_execute, runner, mock_cursor):
1434+
mock_execute.return_value = mock_cursor(["row"], [])
1435+
result = runner.invoke(["stage", "create", "stageName", "--enable-directory"])
1436+
assert result.exit_code == 0, result.output
1437+
mock_execute.assert_called_once_with(
1438+
"create stage if not exists IDENTIFIER('stageName') encryption = (type = 'SNOWFLAKE_FULL') directory = (enable = true)"
1439+
)
1440+
1441+
1442+
@mock.patch(f"{STAGE_MANAGER}.execute_query")
1443+
def test_stage_create_with_encryption_and_directory_options(
1444+
mock_execute, runner, mock_cursor
1445+
):
1446+
mock_execute.return_value = mock_cursor(["row"], [])
1447+
result = runner.invoke(
1448+
[
1449+
"stage",
1450+
"create",
1451+
"stageName",
1452+
"--encryption",
1453+
"SNOWFLAKE_SSE",
1454+
"--enable-directory",
1455+
]
1456+
)
1457+
assert result.exit_code == 0, result.output
1458+
mock_execute.assert_called_once_with(
1459+
"create stage if not exists IDENTIFIER('stageName') encryption = (type = 'SNOWFLAKE_SSE') directory = (enable = true)"
1460+
)

tests_integration/test_stage.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import tempfile
1919
import time
2020
from pathlib import Path
21+
2122
import pytest
2223

2324
from tests.stage.test_stage import RecursiveUploadTester, NESTED_STRUCTURE
@@ -788,6 +789,54 @@ def test_recursive_upload_glob_file_pattern(temporary_directory, runner, test_da
788789
]
789790

790791

792+
@pytest.mark.integration
793+
def test_stage_copy_refreshes_stream(
794+
runner, snowflake_session, test_database, tmp_path
795+
):
796+
stage_name = "test_stage_stream"
797+
stage_name_with_at = "@" + stage_name
798+
stream_name = "test_stream"
799+
800+
# Create stage and stream
801+
runner.invoke_with_connection_json(
802+
["stage", "create", stage_name_with_at, "--enable-directory"]
803+
)
804+
snowflake_session.execute_string(
805+
f"CREATE OR REPLACE STREAM {stream_name} ON STAGE {stage_name}"
806+
)
807+
808+
# Create test file
809+
test_file = tmp_path / "test.txt"
810+
test_file.write_text("test content")
811+
812+
# Initial copy without --refresh
813+
result = runner.invoke_with_connection_json(
814+
["stage", "copy", str(test_file), stage_name_with_at]
815+
)
816+
assert result.exit_code == 0
817+
818+
# Check stream has no changes
819+
stream_changes_cursor = snowflake_session.execute_string(
820+
f"SELECT * FROM {stream_name}"
821+
)
822+
stream_changes = row_from_snowflake_session(stream_changes_cursor)
823+
assert len(stream_changes) == 0
824+
825+
# Copy again with --refresh flag
826+
result = runner.invoke_with_connection_json(
827+
["stage", "copy", str(test_file), stage_name_with_at, "--refresh"]
828+
)
829+
assert result.exit_code == 0
830+
831+
# Check stream has changes after --refresh was specified
832+
stream_changes_cursor = snowflake_session.execute_string(
833+
f"SELECT * FROM {stream_name}"
834+
)
835+
stream_changes = row_from_snowflake_session(stream_changes_cursor)
836+
assert len(stream_changes) == 1
837+
assert stream_changes[0]["RELATIVE_PATH"] == "test.txt"
838+
839+
791840
@pytest.mark.integration
792841
def test_recursive_upload_no_recursive_glob_pattern(
793842
temporary_directory, runner, test_database
@@ -866,3 +915,24 @@ def test_create_encryption(runner, test_database, encryption):
866915
)
867916
assert result.exit_code == 0, result.output
868917
assert result.json == {"status": f"Stage area A_STAGE successfully created."}
918+
919+
920+
@pytest.mark.integration
921+
def test_create_directory_option(runner, test_database):
922+
stage_name = "stage_with_directory"
923+
result = runner.invoke_with_connection_json(
924+
["stage", "create", stage_name, "--enable-directory"]
925+
)
926+
assert result.exit_code == 0, result.output
927+
assert result.json == {
928+
"status": f"Stage area {stage_name.upper()} successfully created."
929+
}
930+
931+
# Verify directory is enabled
932+
result = runner.invoke_with_connection_json(["stage", "describe", stage_name])
933+
assert result.exit_code == 0, result.output
934+
assert any(
935+
row.get("parent_property") == "DIRECTORY"
936+
and row.get("property_value") == "true"
937+
for row in result.json
938+
)

0 commit comments

Comments
 (0)