Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the stage diff algorithm #1031

Merged
merged 3 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

## Fixes and improvements
* More human-friendly errors in case of corrupted `config.toml` file.
* Fixed a bug in `snow app` that caused files to be re-uploaded unnecessarily.

# v2.2.0

Expand Down
8 changes: 4 additions & 4 deletions src/snowflake/cli/plugins/stage/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
from snowflake.connector.cursor import SnowflakeCursor
from snowflake.connector.cursor import DictCursor

from .manager import StageManager

Expand Down Expand Up @@ -144,13 +144,13 @@ def strip_stage_name(path: str) -> str:
return "/".join(path.split("/")[1:])


def build_md5_map(list_stage_cursor: SnowflakeCursor) -> Dict[str, str]:
def build_md5_map(list_stage_cursor: DictCursor) -> Dict[str, str]:
"""
Returns a mapping of relative stage paths to their md5sums.
"""
return {
strip_stage_name(name): md5
for (name, size, md5, modified) in list_stage_cursor.fetchall()
strip_stage_name(file["name"]): file["md5"]
for file in list_stage_cursor.fetchall()
}


Expand Down
29 changes: 26 additions & 3 deletions tests/stage/test_diff.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import hashlib
from pathlib import Path
from typing import Dict, List, Tuple, Union
from typing import Dict, List, Union
from unittest import mock

import pytest
from snowflake.cli.api.exceptions import SnowflakeSQLExecutionError
from snowflake.cli.plugins.stage.diff import (
DiffResult,
build_md5_map,
delete_only_on_stage_files,
enumerate_files,
get_stage_path_from_file,
Expand Down Expand Up @@ -40,13 +41,18 @@ def md5_of(contents: Union[str, bytes]) -> str:

def stage_contents(
files: Dict[str, Union[str, bytes]], last_modified: str = DEFAULT_LAST_MODIFIED
) -> List[Tuple[str, int, str, str]]:
) -> List[Dict[str, Union[str, int]]]:
"""
Return file contents as they would be listed by a SNOWFLAKE_SSE stage
if they were uploaded with the given structure and contents.
"""
return [
(f"stage/{relpath}", len(contents), md5_of(contents), last_modified)
{
"name": f"stage/{relpath}",
"size": len(contents),
"md5": md5_of(contents),
"last_modified": last_modified,
}
for (relpath, contents) in files.items()
]

Expand Down Expand Up @@ -199,6 +205,23 @@ def test_put_files_on_stage(mock_put, overwrite_param):
assert mock_put.mock_calls == expected


def test_build_md5_map(mock_cursor):
actual = build_md5_map(
mock_cursor(
rows=stage_contents(FILE_CONTENTS),
columns=STAGE_LS_COLUMNS,
)
)

expected = {
"README.md": "9b650974f65cc49be96a5ed34ac6d1fd",
"my.jar": "fc605d0e2e50cf3e71873d57f4c598b0",
"ui/streamlit.py": "a7dfdfaf892ecfc5f164914123c7f2cc",
}

assert actual == expected


@mock.patch(f"{STAGE_MANAGER}.remove")
def test_sync_local_diff_with_stage(mock_remove, other_directory):
temp_dir = Path(other_directory)
Expand Down
10 changes: 9 additions & 1 deletion tests_integration/test_nativeapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,15 @@ def test_nativeapp_init_deploy(
dict(name=app_name),
)

# make sure we always delete the app
# re-deploying should be a no-op; make sure we don't issue any PUT commands
result = runner.invoke_with_connection_json(
["app", "deploy", "--debug"],
env=TEST_ENV,
)
assert result.exit_code == 0
assert "Successfully uploaded chunk 0 of file" not in result.output

# make sure we always delete the package
result = runner.invoke_with_connection_json(
["app", "teardown"],
env=TEST_ENV,
Expand Down
Loading