From 7b37363201ffd580689f6b1e18b06f2eebf76d01 Mon Sep 17 00:00:00 2001 From: Adam Ling Date: Sun, 29 Mar 2026 18:40:05 +0000 Subject: [PATCH 1/2] add skill --- .claude/skills/local-testing-bug-fix/SKILL.md | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .claude/skills/local-testing-bug-fix/SKILL.md diff --git a/.claude/skills/local-testing-bug-fix/SKILL.md b/.claude/skills/local-testing-bug-fix/SKILL.md new file mode 100644 index 0000000000..5e4714b16b --- /dev/null +++ b/.claude/skills/local-testing-bug-fix/SKILL.md @@ -0,0 +1,95 @@ +--- +name: local-testing-bug-fix +description: Fix a GitHub Local Testing issue +--- + +Fix GitHub issue $ARGUMENTS (provide a GitHub issue URL or number) following our coding standards. + +## Prerequisites + +Before using this skill, ensure the following are set up: + +1. **`gh` CLI** — must be installed and authenticated (`gh auth status`) to fetch issue details +2. **`tests/parameters.py`** — must exist (gitignored, create it manually) with your Snowflake credentials: + ```python + CONNECTION_PARAMETERS = { + "account": "your_account", + "user": "your_user", + "password": "your_password", + "database": "your_db", + "schema": "public", + } + ``` + Required only for running live integ tests. Mock-only tests (`tests/mock/`) do not need credentials. + +1. Fetch and read the issue description (use `gh issue view ` or the URL) +2. Understand the requirements and reproduce the problem mentally +3. Determine if the fix is feasible (see Known Limitations below) +4. Find the relevant code in `src/snowflake/snowpark/_internal/mock/` +5. Implement the fix +6. Write or update tests (see Test approach below) +7. Create a commit with message format: `SNOW-XXXXXXX: ` + +## Codebase layout + +- Local testing implementation: `src/snowflake/snowpark/_internal/mock/` +- Mock-only tests (local testing exclusive): `tests/mock/` +- Shared integ tests (run against both live and local): `tests/integ/` + - Tests unsupported in local testing are explicitly skipped with the marker below + +## Skipping a test for local testing + +When a test in `tests/integ/` cannot run in local testing mode, mark it: + +```python +@pytest.mark.skipif( + "config.getoption('local_testing_mode', default=False)", + reason="", +) +def test_something(session): + ... +``` + +## Test approach + +Validate by comparing live session output to local testing session output: + +```python +from snowflake.snowpark import Session + +# live session — tries ~/.snowflake/connections.toml first, falls back to tests/parameters.py +try: + session = Session.builder.getOrCreate() +except Exception: + from tests.parameters import CONNECTION_PARAMETERS + session = Session.builder.configs(CONNECTION_PARAMETERS).create() + +# local testing session +local_session = Session.builder.config("local_testing", True).create() + +df = session... # DF operation +local_df = local_session... # same DF operation + +df.print_schema() +df.show() + +local_df.print_schema() +local_df.show() + +# compare the output, fix +``` + +Run integ tests in local testing mode: +```bash +pytest tests/integ/.py --local_testing_mode +``` + +Run mock-only tests: +```bash +pytest tests/mock/.py +``` + +## Known Limitations + +1. Local Testing does not support raw SQL +2. If the issue requires a feature that is fundamentally server-side (e.g., raw SQL execution, file stages), it may not be feasible — explain this to the user instead of partially implementing From ffaa57a955c9b882ac941d8175ebf3892c6cc1d5 Mon Sep 17 00:00:00 2001 From: Adam Ling Date: Sun, 29 Mar 2026 11:46:00 -0700 Subject: [PATCH 2/2] SNOW-3217482: Fix mock_convert_timezone to handle string timestamp inputs The mock convert_timezone function crashed with AttributeError when the source timestamp column contained string values instead of datetime objects. This adds string-to-datetime parsing via dateutil.parser.parse in the mock implementation, matching how Snowflake handles varchar timestamp inputs. Fixes #4110 Made-with: Cursor --- src/snowflake/snowpark/mock/_functions.py | 8 +++- tests/mock/test_functions.py | 46 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/snowflake/snowpark/mock/_functions.py b/src/snowflake/snowpark/mock/_functions.py index 1d5dd6568a..17a9dd70d9 100644 --- a/src/snowflake/snowpark/mock/_functions.py +++ b/src/snowflake/snowpark/mock/_functions.py @@ -2056,7 +2056,10 @@ def mock_convert_timezone( else: source_timezone, target_timezone, source_time = args return_type = TimestampTimeZone.NTZ - if source_time.sf_type.datatype.tz is not TimestampTimeZone.NTZ: + if ( + isinstance(source_time.sf_type.datatype, TimestampType) + and source_time.sf_type.datatype.tz is not TimestampTimeZone.NTZ + ): raise ValueError( "[Local Testing] convert_timezone can only convert NTZ timestamps when source_timezone is specified." ) @@ -2070,6 +2073,9 @@ def _convert(row): if source_time is None: return None + if isinstance(source_time, str): + source_time = dateutil.parser.parse(source_time) + if source_timezone is not None: # Using dateutil because it uses iana timezones while pytz would use Olson tzdb. source_time = source_time.replace(tzinfo=dateutil.tz.gettz(source_timezone)) diff --git a/tests/mock/test_functions.py b/tests/mock/test_functions.py index 08824f801b..1a903bf5da 100644 --- a/tests/mock/test_functions.py +++ b/tests/mock/test_functions.py @@ -19,6 +19,7 @@ col, concat_ws, contains, + convert_timezone, count, current_date, current_time, @@ -871,3 +872,48 @@ def test_save_as_table_column_order_name_array_type(session): assert len(result) == 1 assert result[0]["ID"] == 1 assert result[0]["TAGS"] is None + + +def test_convert_timezone_with_string_input(session): + """Regression test for SNOW-3217482 / GitHub #4110: + convert_timezone should parse string timestamps.""" + df = session.create_dataframe([["2023-03-10T08:00:00+03:00"]], schema=["date"]) + result = df.select(convert_timezone(lit("UTC"), col("date"))).collect() + assert result[0][0] == datetime.datetime( + 2023, 3, 10, 5, 0, tzinfo=datetime.timezone.utc + ) + + +def test_convert_timezone_with_string_input_ntz(session): + """String input without timezone info should use the session/local timezone.""" + df = session.create_dataframe([["2023-06-15T12:30:00"]], schema=["date"]) + result = df.select(convert_timezone(lit("UTC"), col("date"))).collect() + assert result[0][0] is not None + assert result[0][0].year == 2023 + assert result[0][0].month == 6 + assert result[0][0].day == 15 + + +def test_convert_timezone_with_string_input_null(session): + """String column with None values should return None.""" + df = session.create_dataframe([[None]], schema=["date"]) + result = df.select(convert_timezone(lit("UTC"), col("date"))).collect() + assert result[0][0] is None + + +def test_convert_timezone_with_string_input_three_args(session): + """Three-arg form: convert_timezone(target_tz, source_time, source_tz) with string input.""" + from snowflake.snowpark.mock._functions import LocalTimezone + import pytz + + LocalTimezone.set_local_timezone(pytz.timezone("Etc/GMT+8")) + try: + df = session.create_dataframe([["2023-03-10T08:00:00"]], schema=["date"]) + # target=UTC, source_time=date (NTZ string), source_tz=US/Eastern + # 08:00 in US/Eastern → 13:00 UTC (EST is UTC-5) + result = df.select( + convert_timezone(lit("UTC"), col("date"), lit("US/Eastern")) + ).collect() + assert result[0][0] == datetime.datetime(2023, 3, 10, 13, 0) + finally: + LocalTimezone.set_local_timezone()