From ad3756449c8896eb60886b7baf6358beebd88d91 Mon Sep 17 00:00:00 2001 From: Felix He Date: Tue, 19 Aug 2025 11:01:52 -0700 Subject: [PATCH 01/39] SNOW-2246517: update docstrings and changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7762d8bdc0..d9d3edb6c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,8 @@ - `current_transaction` - `getbit` +- Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. + #### Bug Fixes - Fixed the repr of TimestampType to match the actual subtype it represents. From cfd23dc3c83ab7c080d0beec1725b6dd8c6e5014 Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 3 Sep 2025 15:57:59 -0700 Subject: [PATCH 02/39] SNOW-2246517: update changelog --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9d3edb6c3..7762d8bdc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,8 +113,6 @@ - `current_transaction` - `getbit` -- Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. - #### Bug Fixes - Fixed the repr of TimestampType to match the actual subtype it represents. From 9b3d66a2343a94876feb93e8348d9973865ebc6d Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 6 Aug 2025 10:27:04 -0700 Subject: [PATCH 03/39] SNOW-2246517: add to test_types.py --- CHANGELOG.md | 2 +- tests/utils.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7762d8bdc0..22ec1e06e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -294,7 +294,7 @@ - Added a ttl cache to describe queries. Repeated queries in a 15 second interval will use the cached value rather than requery Snowflake. - Added a parameter `fetch_with_process` to `DataFrameReader.dbapi` (PrPr) to enable multiprocessing for parallel data fetching in local ingestion. By default, local ingestion uses multithreading. Multiprocessing may improve performance for CPU-bound tasks like Parquet file generation. - Added a new function `snowflake.snowpark.functions.model` that allows users to call methods of a model. - +- Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. #### Improvements - Added support for row validation using XSD schema using `rowValidationXSDPath` option when reading XML files with a row tag using `rowTag` option. diff --git a/tests/utils.py b/tests/utils.py index ede0148a11..cd2c5538fd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -443,6 +443,8 @@ def verify_schema( meta.scale, meta.internal_size, max_string_size or session._conn.max_string_size, + 0, + 0, ) assert ( sp_type == field.datatype From 7926ee333851e1160d46b3f8c3998e1eb984bf5b Mon Sep 17 00:00:00 2001 From: Felix He Date: Tue, 19 Aug 2025 11:01:52 -0700 Subject: [PATCH 04/39] SNOW-2246517: update docstrings and changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ec1e06e9..7762d8bdc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -294,7 +294,7 @@ - Added a ttl cache to describe queries. Repeated queries in a 15 second interval will use the cached value rather than requery Snowflake. - Added a parameter `fetch_with_process` to `DataFrameReader.dbapi` (PrPr) to enable multiprocessing for parallel data fetching in local ingestion. By default, local ingestion uses multithreading. Multiprocessing may improve performance for CPU-bound tasks like Parquet file generation. - Added a new function `snowflake.snowpark.functions.model` that allows users to call methods of a model. -- Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. + #### Improvements - Added support for row validation using XSD schema using `rowValidationXSDPath` option when reading XML files with a row tag using `rowTag` option. From 817b3d5a2484a66fa16d24090f2a777847704249 Mon Sep 17 00:00:00 2001 From: Felix He Date: Mon, 25 Aug 2025 12:17:23 -0700 Subject: [PATCH 05/39] SNOW-2272863: remove extra change: --- tests/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index cd2c5538fd..ede0148a11 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -443,8 +443,6 @@ def verify_schema( meta.scale, meta.internal_size, max_string_size or session._conn.max_string_size, - 0, - 0, ) assert ( sp_type == field.datatype From 65abe59eb7470002f53c1e06ce718b27d71a4aa2 Mon Sep 17 00:00:00 2001 From: Felix He Date: Mon, 8 Sep 2025 11:29:36 -0700 Subject: [PATCH 06/39] SNOW-2272863: updated datatype mapper --- tests/integ/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integ/conftest.py b/tests/integ/conftest.py index 6f1a088b62..2ff90e2494 100644 --- a/tests/integ/conftest.py +++ b/tests/integ/conftest.py @@ -274,7 +274,7 @@ def session( .config("local_testing", local_testing_mode) .config( "session_parameters", - {"feature_interval_types": "ENABLED", "enable_interval_subtypes": "true"}, + {"feature_interval_types": "ENABLED"}, ) .create() ) From 8eaa5047074b7423449d05637f2be62e70d7bec0 Mon Sep 17 00:00:00 2001 From: Felix He Date: Mon, 8 Sep 2025 12:11:50 -0700 Subject: [PATCH 07/39] SNOW-2272863: re-enable parameter --- tests/integ/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integ/conftest.py b/tests/integ/conftest.py index 2ff90e2494..6f1a088b62 100644 --- a/tests/integ/conftest.py +++ b/tests/integ/conftest.py @@ -274,7 +274,7 @@ def session( .config("local_testing", local_testing_mode) .config( "session_parameters", - {"feature_interval_types": "ENABLED"}, + {"feature_interval_types": "ENABLED", "enable_interval_subtypes": "true"}, ) .create() ) From 9a185cb410b05856e6569ef50043ccbc374b05c0 Mon Sep 17 00:00:00 2001 From: Felix He Date: Mon, 4 Aug 2025 15:41:04 -0700 Subject: [PATCH 08/39] SNOW-2246517: Support YearMonthIntervalType --- src/snowflake/snowpark/types.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/snowflake/snowpark/types.py b/src/snowflake/snowpark/types.py index a989532141..f063cc87e3 100644 --- a/src/snowflake/snowpark/types.py +++ b/src/snowflake/snowpark/types.py @@ -1093,6 +1093,7 @@ def _fill_ast(self, ast: proto.DataType) -> None: IntegerType, LongType, DateType, + YearMonthIntervalType, NullType, ] @@ -1103,10 +1104,16 @@ def _fill_ast(self, ast: proto.DataType) -> None: TimestampType(timezone=TimestampTimeZone.TZ), ] +_interval_types: List[DataType] = [ + YearMonthIntervalType(), + # TODO: Add DayToSecondIntervalType when implemented +] + _all_atomic_types: Dict[str, Type[DataType]] = {t.typeName(): t for t in _atomic_types} _all_timestamp_types: Dict[str, DataType] = { t.json_value(): t for t in _timestamp_types } +_all_interval_types: Dict[str, DataType] = {t.json_value(): t for t in _interval_types} _complex_types: List[Type[Union[ArrayType, MapType, StructType]]] = [ ArrayType, @@ -1132,6 +1139,8 @@ def _parse_datatype_json_value(json_value: Union[dict, str]) -> DataType: return _all_atomic_types[json_value]() if json_value in _all_timestamp_types: return TimestampType(timezone=_all_timestamp_types[json_value].tz) + elif json_value in _all_interval_types: + return _all_interval_types[json_value] elif json_value == "decimal": return DecimalType() elif json_value == "variant": From 8d3a8ecc51454e58e8ddfd4ab3171653362f766e Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 6 Aug 2025 08:49:18 -0700 Subject: [PATCH 09/39] SNOW-2246517: update tests --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ff8ee68349..6708c96190 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,7 @@ env/ venv*/ ENV/ env.bak/ +snowpark-venv/ # Spyder project settings .spyderproject From e095a53fe890e3d48e7e98faf965baf1cd86e6c2 Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 6 Aug 2025 10:27:04 -0700 Subject: [PATCH 10/39] SNOW-2246517: add to test_types.py --- CHANGELOG.md | 2 +- tests/utils.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7762d8bdc0..22ec1e06e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -294,7 +294,7 @@ - Added a ttl cache to describe queries. Repeated queries in a 15 second interval will use the cached value rather than requery Snowflake. - Added a parameter `fetch_with_process` to `DataFrameReader.dbapi` (PrPr) to enable multiprocessing for parallel data fetching in local ingestion. By default, local ingestion uses multithreading. Multiprocessing may improve performance for CPU-bound tasks like Parquet file generation. - Added a new function `snowflake.snowpark.functions.model` that allows users to call methods of a model. - +- Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. #### Improvements - Added support for row validation using XSD schema using `rowValidationXSDPath` option when reading XML files with a row tag using `rowTag` option. diff --git a/tests/utils.py b/tests/utils.py index ede0148a11..cd2c5538fd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -443,6 +443,8 @@ def verify_schema( meta.scale, meta.internal_size, max_string_size or session._conn.max_string_size, + 0, + 0, ) assert ( sp_type == field.datatype From 745a6ba40c3c64767b1548ac5f95ee308c39dabe Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 7 Aug 2025 09:52:09 -0700 Subject: [PATCH 11/39] SNOW-2246517: add tests to test_datatype_suite --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ec1e06e9..6f628389ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -295,6 +295,7 @@ - Added a parameter `fetch_with_process` to `DataFrameReader.dbapi` (PrPr) to enable multiprocessing for parallel data fetching in local ingestion. By default, local ingestion uses multithreading. Multiprocessing may improve performance for CPU-bound tasks like Parquet file generation. - Added a new function `snowflake.snowpark.functions.model` that allows users to call methods of a model. - Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. + #### Improvements - Added support for row validation using XSD schema using `rowValidationXSDPath` option when reading XML files with a row tag using `rowTag` option. From 4e65fc9d373fea2eebcde64f6fc8ee590d4f63e0 Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 7 Aug 2025 11:45:00 -0700 Subject: [PATCH 12/39] SNOW-2246517: revert change for start_field and end_field and determine year month interval type by using scale instead --- tests/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index cd2c5538fd..ede0148a11 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -443,8 +443,6 @@ def verify_schema( meta.scale, meta.internal_size, max_string_size or session._conn.max_string_size, - 0, - 0, ) assert ( sp_type == field.datatype From 46904d2bec9669e3a8a2f821d8b6a7579861a11a Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 13 Aug 2025 11:08:28 -0700 Subject: [PATCH 13/39] SNOW-2246517: add tests and xfail --- tests/integ/scala/test_datatype_suite.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/integ/scala/test_datatype_suite.py b/tests/integ/scala/test_datatype_suite.py index ccb036f85f..902c20248f 100644 --- a/tests/integ/scala/test_datatype_suite.py +++ b/tests/integ/scala/test_datatype_suite.py @@ -219,6 +219,10 @@ def server_side_max_string(structured_type_session): "config.getoption('local_testing_mode', default=False)", reason="FEAT: function to_geography not supported", ) +@pytest.mark.xfail( + reason="SNOW-2255664: Waiting on Python Connector release for IntervalType type_code", + strict=True, +) def test_verify_datatypes_reference(session): schema = StructType( [ @@ -349,6 +353,10 @@ def test_verify_datatypes_reference_vector(session): "config.getoption('local_testing_mode', default=False)", reason="FEAT: function to_geography not supported", ) +@pytest.mark.xfail( + reason="SNOW-2255664: Waiting on Python Connector release for IntervalType type_code", + strict=True, +) def test_dtypes(session): schema = StructType( [ From 092376bb927544517fe9450a402be8197bebbce8 Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 13 Aug 2025 11:14:32 -0700 Subject: [PATCH 14/39] SNOW-2246517: disable parameter --- tests/integ/scala/test_datatype_suite.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integ/scala/test_datatype_suite.py b/tests/integ/scala/test_datatype_suite.py index 902c20248f..6f85fd8bf0 100644 --- a/tests/integ/scala/test_datatype_suite.py +++ b/tests/integ/scala/test_datatype_suite.py @@ -302,6 +302,7 @@ def test_verify_datatypes_reference(session): ] ) Utils.is_schema_same(df.schema, expected_schema, case_sensitive=False) + session.sql("alter session set feature_interval_types=disabled;").collect() def test_verify_datatypes_reference2(session): @@ -433,6 +434,7 @@ def test_dtypes(session): ("ARRAY", "array"), ("MAP", "map"), ] + session.sql("alter session set feature_interval_types=disabled;").collect() @pytest.mark.skipif( From 71de9a80aaca0831f9416b970d8f20ed91b90fe8 Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 14 Aug 2025 14:46:55 -0700 Subject: [PATCH 15/39] SNOW-2246517: remove xfails --- tests/integ/scala/test_datatype_suite.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/integ/scala/test_datatype_suite.py b/tests/integ/scala/test_datatype_suite.py index 6f85fd8bf0..4e3aa46cc5 100644 --- a/tests/integ/scala/test_datatype_suite.py +++ b/tests/integ/scala/test_datatype_suite.py @@ -219,10 +219,6 @@ def server_side_max_string(structured_type_session): "config.getoption('local_testing_mode', default=False)", reason="FEAT: function to_geography not supported", ) -@pytest.mark.xfail( - reason="SNOW-2255664: Waiting on Python Connector release for IntervalType type_code", - strict=True, -) def test_verify_datatypes_reference(session): schema = StructType( [ @@ -354,10 +350,6 @@ def test_verify_datatypes_reference_vector(session): "config.getoption('local_testing_mode', default=False)", reason="FEAT: function to_geography not supported", ) -@pytest.mark.xfail( - reason="SNOW-2255664: Waiting on Python Connector release for IntervalType type_code", - strict=True, -) def test_dtypes(session): schema = StructType( [ From fae094b86e91330c3df326fb49b6ac8976fe7c1a Mon Sep 17 00:00:00 2001 From: Felix He Date: Tue, 19 Aug 2025 11:01:52 -0700 Subject: [PATCH 16/39] SNOW-2246517: update docstrings and changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f628389ea..d4d86bac13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,8 @@ - `current_transaction` - `getbit` +- Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. + #### Bug Fixes - Fixed the repr of TimestampType to match the actual subtype it represents. @@ -128,6 +130,7 @@ ### Snowpark pandas API Updates #### New Features + - Completed support for `pd.read_snowflake()`, `pd.to_iceberg()`, `pd.to_pandas()`, `pd.to_snowpark()`, `pd.to_snowflake()`, `DataFrame.to_iceberg()`, `DataFrame.to_pandas()`, `DataFrame.to_snowpark()`, @@ -138,6 +141,7 @@ - Added support for `Index.get_level_values()`. #### Improvements + - Set the default transfer limit in hybrid execution for data leaving Snowflake to 100k, which can be overridden with the SnowflakePandasTransferThreshold environment variable. This configuration is appropriate for scenarios with two available engines, "Pandas" and "Snowflake" on relational workloads. - Improve import error message by adding `--upgrade` to `pip install "snowflake-snowpark-python[modin]"` in the error message. - Reduce the telemetry messages from the modin client by pre-aggregating into 5 second windows and only keeping a narrow band of metrics which are useful for tracking hybrid execution and native pandas performance. @@ -294,7 +298,6 @@ - Added a ttl cache to describe queries. Repeated queries in a 15 second interval will use the cached value rather than requery Snowflake. - Added a parameter `fetch_with_process` to `DataFrameReader.dbapi` (PrPr) to enable multiprocessing for parallel data fetching in local ingestion. By default, local ingestion uses multithreading. Multiprocessing may improve performance for CPU-bound tasks like Parquet file generation. - Added a new function `snowflake.snowpark.functions.model` that allows users to call methods of a model. -- Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. #### Improvements From 1f05b8876d4fa74e2b408dcb11aeee4abe2070ea Mon Sep 17 00:00:00 2001 From: Felix He Date: Tue, 19 Aug 2025 13:25:32 -0700 Subject: [PATCH 17/39] SNOW-2246517: address comments, update tests, add comments --- tests/integ/scala/test_datatype_suite.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integ/scala/test_datatype_suite.py b/tests/integ/scala/test_datatype_suite.py index 4e3aa46cc5..91084a977e 100644 --- a/tests/integ/scala/test_datatype_suite.py +++ b/tests/integ/scala/test_datatype_suite.py @@ -216,8 +216,8 @@ def server_side_max_string(structured_type_session): @pytest.mark.skipif( - "config.getoption('local_testing_mode', default=False)", - reason="FEAT: function to_geography not supported", + "config.getoption('local_testing_mode', default=False)" or IS_IN_STORED_PROC, + reason="FEAT: function to_geography not supported or alter session is not supported in stored proc", ) def test_verify_datatypes_reference(session): schema = StructType( @@ -347,8 +347,8 @@ def test_verify_datatypes_reference_vector(session): @pytest.mark.skipif( - "config.getoption('local_testing_mode', default=False)", - reason="FEAT: function to_geography not supported", + "config.getoption('local_testing_mode', default=False)" or IS_IN_STORED_PROC, + reason="FEAT: function to_geography not supported or alter session is not supported in stored proc", ) def test_dtypes(session): schema = StructType( From f2c9e472b74547b8d2c35104ad81f6d30aa5d8ab Mon Sep 17 00:00:00 2001 From: Felix He Date: Fri, 22 Aug 2025 11:58:56 -0700 Subject: [PATCH 18/39] SNOW-2246517: updated --- tests/integ/scala/test_datatype_suite.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/integ/scala/test_datatype_suite.py b/tests/integ/scala/test_datatype_suite.py index 91084a977e..4ce1d47220 100644 --- a/tests/integ/scala/test_datatype_suite.py +++ b/tests/integ/scala/test_datatype_suite.py @@ -216,8 +216,11 @@ def server_side_max_string(structured_type_session): @pytest.mark.skipif( - "config.getoption('local_testing_mode', default=False)" or IS_IN_STORED_PROC, - reason="FEAT: function to_geography not supported or alter session is not supported in stored proc", + "config.getoption('local_testing_mode', default=False)", + reason="FEAT: function to_geography not supported", +) +@pytest.mark.skipif( + IS_IN_STORED_PROC, reason="Alter Session not supported in stored procedure." ) def test_verify_datatypes_reference(session): schema = StructType( @@ -347,8 +350,11 @@ def test_verify_datatypes_reference_vector(session): @pytest.mark.skipif( - "config.getoption('local_testing_mode', default=False)" or IS_IN_STORED_PROC, - reason="FEAT: function to_geography not supported or alter session is not supported in stored proc", + "config.getoption('local_testing_mode', default=False)", + reason="FEAT: function to_geography not supported", +) +@pytest.mark.skipif( + IS_IN_STORED_PROC, reason="Alter Session not supported in stored procedure." ) def test_dtypes(session): schema = StructType( From a9d803b3d5f982a109b9317f01eb50f9ba9e5a16 Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 3 Sep 2025 15:54:48 -0700 Subject: [PATCH 19/39] SNOW-2246517: address nits --- .gitignore | 1 - tests/integ/scala/test_datatype_suite.py | 8 -------- 2 files changed, 9 deletions(-) diff --git a/.gitignore b/.gitignore index 6708c96190..ff8ee68349 100644 --- a/.gitignore +++ b/.gitignore @@ -106,7 +106,6 @@ env/ venv*/ ENV/ env.bak/ -snowpark-venv/ # Spyder project settings .spyderproject diff --git a/tests/integ/scala/test_datatype_suite.py b/tests/integ/scala/test_datatype_suite.py index 4ce1d47220..ccb036f85f 100644 --- a/tests/integ/scala/test_datatype_suite.py +++ b/tests/integ/scala/test_datatype_suite.py @@ -219,9 +219,6 @@ def server_side_max_string(structured_type_session): "config.getoption('local_testing_mode', default=False)", reason="FEAT: function to_geography not supported", ) -@pytest.mark.skipif( - IS_IN_STORED_PROC, reason="Alter Session not supported in stored procedure." -) def test_verify_datatypes_reference(session): schema = StructType( [ @@ -301,7 +298,6 @@ def test_verify_datatypes_reference(session): ] ) Utils.is_schema_same(df.schema, expected_schema, case_sensitive=False) - session.sql("alter session set feature_interval_types=disabled;").collect() def test_verify_datatypes_reference2(session): @@ -353,9 +349,6 @@ def test_verify_datatypes_reference_vector(session): "config.getoption('local_testing_mode', default=False)", reason="FEAT: function to_geography not supported", ) -@pytest.mark.skipif( - IS_IN_STORED_PROC, reason="Alter Session not supported in stored procedure." -) def test_dtypes(session): schema = StructType( [ @@ -432,7 +425,6 @@ def test_dtypes(session): ("ARRAY", "array"), ("MAP", "map"), ] - session.sql("alter session set feature_interval_types=disabled;").collect() @pytest.mark.skipif( From 4f7d1ea0fd2d3392131c0330bee65847e0a69cb1 Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 3 Sep 2025 17:00:10 -0700 Subject: [PATCH 20/39] SNOW-2246517: remove redundant changes --- src/snowflake/snowpark/types.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/snowflake/snowpark/types.py b/src/snowflake/snowpark/types.py index f063cc87e3..a989532141 100644 --- a/src/snowflake/snowpark/types.py +++ b/src/snowflake/snowpark/types.py @@ -1093,7 +1093,6 @@ def _fill_ast(self, ast: proto.DataType) -> None: IntegerType, LongType, DateType, - YearMonthIntervalType, NullType, ] @@ -1104,16 +1103,10 @@ def _fill_ast(self, ast: proto.DataType) -> None: TimestampType(timezone=TimestampTimeZone.TZ), ] -_interval_types: List[DataType] = [ - YearMonthIntervalType(), - # TODO: Add DayToSecondIntervalType when implemented -] - _all_atomic_types: Dict[str, Type[DataType]] = {t.typeName(): t for t in _atomic_types} _all_timestamp_types: Dict[str, DataType] = { t.json_value(): t for t in _timestamp_types } -_all_interval_types: Dict[str, DataType] = {t.json_value(): t for t in _interval_types} _complex_types: List[Type[Union[ArrayType, MapType, StructType]]] = [ ArrayType, @@ -1139,8 +1132,6 @@ def _parse_datatype_json_value(json_value: Union[dict, str]) -> DataType: return _all_atomic_types[json_value]() if json_value in _all_timestamp_types: return TimestampType(timezone=_all_timestamp_types[json_value].tz) - elif json_value in _all_interval_types: - return _all_interval_types[json_value] elif json_value == "decimal": return DecimalType() elif json_value == "variant": From 82a126781b1085fd572569a4b1eb9c2c7b1388dc Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 20 Aug 2025 15:39:03 -0700 Subject: [PATCH 21/39] SNOW-2272863: add more tests --- tests/integ/test_dataframe.py | 115 ++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/integ/test_dataframe.py b/tests/integ/test_dataframe.py index 22f963f121..7a00800fe0 100644 --- a/tests/integ/test_dataframe.py +++ b/tests/integ/test_dataframe.py @@ -1905,6 +1905,121 @@ def test_create_dataframe_with_day_time_interval_type(session): assert interval_sql_result[0][2] == datetime.timedelta(days=-2, seconds=63870) +@pytest.mark.skipif( + "config.getoption('local_testing_mode', default=False)" or IS_IN_STORED_PROC, + reason="FEAT: session.sql is not supported in stored proc or local testing", +) +def test_create_dataframe_with_day_time_interval_type(session): + session.sql("alter session set feature_interval_types=enabled;").collect() + + schema = StructType([StructField("interval_col", DayTimeIntervalType())]) + data = [["1 12:30:45"], ["-2 08:15:30"]] + df = session.create_dataframe(data, schema=schema) + + assert isinstance(df.schema.fields[0].datatype, DayTimeIntervalType) + result = df.collect() + assert len(result) == 2 + assert result[0][0] == datetime.timedelta(days=1, seconds=45045) + assert result[1][0] == datetime.timedelta(days=-3, seconds=56670) + + test_schema = StructType( + [ + StructField("test_date", DateType()), + StructField("interval_col", DayTimeIntervalType()), + ] + ) + + test_data = [["2023-01-15", "1 06:30:00"], ["2022-06-30", "-0 03:15:45"]] + + test_df = session.create_dataframe(test_data, schema=test_schema) + + addition_result = test_df.select( + (test_df.test_date + test_df.interval_col).alias("date_plus_interval") + ).collect() + + subtraction_result = test_df.select( + (test_df.test_date - test_df.interval_col).alias("date_minus_interval") + ).collect() + + assert len(addition_result) == 2 + assert addition_result[0][0] == datetime.datetime(2023, 1, 16, 6, 30) + assert addition_result[1][0] == datetime.datetime(2022, 6, 29, 20, 44, 15) + + assert len(subtraction_result) == 2 + assert subtraction_result[0][0] == datetime.datetime(2023, 1, 13, 17, 30) + assert subtraction_result[1][0] == datetime.datetime(2022, 6, 30, 3, 15, 45) + + interval_arithmetic_df = session.sql( + """ + SELECT + DATE '2023-01-01' + INTERVAL '1 12:00:00' DAY TO SECOND as addition_result, + DATE '2023-12-31' - INTERVAL '0 06:30:15' DAY TO SECOND as subtraction_result + """ + ) + + arithmetic_result = interval_arithmetic_df.collect() + assert len(arithmetic_result) == 1 + assert arithmetic_result[0][0] == datetime.datetime(2023, 1, 2, 12, 0) + assert arithmetic_result[0][1] == datetime.datetime(2023, 12, 30, 17, 29, 45) + + interval_schema = StructType( + [ + StructField("interval1", DayTimeIntervalType()), + StructField("interval2", DayTimeIntervalType()), + ] + ) + + interval_data = [ + ["2 12:30:45", "1 06:15:30"], + ["1 00:00:00", "-0 12:30:00"], + ["-1 08:45:15", "2 04:30:45"], + ] + + interval_df = session.create_dataframe(interval_data, schema=interval_schema) + + interval_addition_result = interval_df.select( + (interval_df.interval1 + interval_df.interval2).alias("interval_sum") + ).collect() + + interval_subtraction_result = interval_df.select( + (interval_df.interval1 - interval_df.interval2).alias("interval_diff") + ).collect() + + assert len(interval_addition_result) == 3 + assert interval_addition_result[0][0] == datetime.timedelta(days=3, seconds=67575) + assert interval_addition_result[1][0] == datetime.timedelta(seconds=41400) + assert interval_addition_result[2][0] == datetime.timedelta(seconds=71130) + + assert len(interval_subtraction_result) == 3 + assert interval_subtraction_result[0][0] == datetime.timedelta( + days=1, seconds=22515 + ) + assert interval_subtraction_result[1][0] == datetime.timedelta( + days=1, seconds=45000 + ) + assert interval_subtraction_result[2][0] == datetime.timedelta( + days=-4, seconds=38640 + ) + + interval_sql_df = session.sql( + """ + SELECT + INTERVAL '2 12:30:45' DAY TO SECOND + INTERVAL '1 06:15:30' DAY TO SECOND as interval_addition, + INTERVAL '3 00:00:00' DAY TO SECOND - INTERVAL '1 12:30:00' DAY TO SECOND as interval_subtraction, + INTERVAL '1 06:30:00' DAY TO SECOND - INTERVAL '2 12:45:30' DAY TO SECOND as interval_subtraction_2, + """ + ) + + interval_sql_result = interval_sql_df.collect() + assert len(interval_sql_result) == 1 + assert len(interval_sql_result[0]) == 3 + assert interval_sql_result[0][0] == datetime.timedelta(days=3, seconds=67575) + assert interval_sql_result[0][1] == datetime.timedelta(days=1, seconds=41400) + assert interval_sql_result[0][2] == datetime.timedelta(days=-2, seconds=63870) + + session.sql("alter session set feature_interval_types=disabled;").collect() + + def test_create_dataframe_with_semi_structured_data_types(session): data = [ [ From b03c6ba70f65565cf68b381d00780b2f1f248124 Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 21 Aug 2025 11:04:11 -0700 Subject: [PATCH 22/39] SNOW-2272863: add more tests --- tests/integ/scala/test_dataframe_suite.py | 165 ++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/tests/integ/scala/test_dataframe_suite.py b/tests/integ/scala/test_dataframe_suite.py index 04cf7ed40b..e6ce5d0aae 100644 --- a/tests/integ/scala/test_dataframe_suite.py +++ b/tests/integ/scala/test_dataframe_suite.py @@ -3469,3 +3469,168 @@ def test_day_time_interval_type_dataframe(session): df.schema.fields[9].datatype.start_field == 2 and df.schema.fields[9].datatype.end_field == 3 ) # MINUTE TO SECOND + + +@pytest.mark.skipif( + "config.getoption('local_testing_mode', default=False)" or IS_IN_STORED_PROC, + reason="FEAT: session.sql is not supported in stored proc or local testing", +) +def test_day_time_interval_type_dataframe(session): + session.sql("alter session set feature_interval_types=enabled;").collect() + session.sql("alter session set enable_interval_subtypes=true;").collect() + + schema = StructType( + [ + StructField("ID", LongType(), nullable=False), + StructField("DT_INTERVAL", DayTimeIntervalType(), nullable=False), + ] + ) + + df = session.sql( + """ + SELECT 1 as id, INTERVAL '1 12:30:45' DAY TO SECOND as dt_interval + UNION ALL + SELECT 2 as id, INTERVAL '2 08:15:30' DAY TO SECOND as dt_interval + UNION ALL + SELECT 3 as id, INTERVAL '0 06:45:15' DAY TO SECOND as dt_interval + """ + ) + + result = df.collect() + assert len(result) == 3 + assert df.schema == schema + + assert result[0]["DT_INTERVAL"] == timedelta(days=1, seconds=45045) + assert result[1]["DT_INTERVAL"] == timedelta(days=2, seconds=29730) + assert result[2]["DT_INTERVAL"] == timedelta(seconds=24315) + + df = session.sql( + """ + SELECT 1 as id, INTERVAL '-1 12:30:45' DAY TO SECOND as dt_interval + UNION ALL + SELECT 2 as id, INTERVAL '-2 08:15:30' DAY TO SECOND as dt_interval + UNION ALL + SELECT 3 as id, INTERVAL '-0 06:45:15' DAY TO SECOND as dt_interval + """ + ) + + result = df.collect() + assert len(result) == 3 + assert df.schema == schema + + assert result[0]["DT_INTERVAL"] == timedelta(days=-2, seconds=41355) + assert result[1]["DT_INTERVAL"] == timedelta(days=-3, seconds=56670) + assert result[2]["DT_INTERVAL"] == timedelta(days=-1, seconds=62085) + + test_cases = [ + [ + "2", + "1", + "30", + "45", + "1 12", + "1 12:30", + "1 12:30:45", + "12:30", + "12:30:45", + "30:45", + ], + [ + "-3", + "-2", + "-45", + "-30", + "-2 06", + "-2 06:15", + "-2 06:15:30", + "-18:45", + "-18:45:15", + "-90:30", + ], + ] + schema = StructType( + [ + StructField("interval_col_day_0_0", DayTimeIntervalType(0, 0)), + StructField("interval_col_hour_1_1", DayTimeIntervalType(1, 1)), + StructField("interval_col_minute_2_2", DayTimeIntervalType(2, 2)), + StructField("interval_col_second_3_3", DayTimeIntervalType(3, 3)), + StructField("interval_col_day_hour_0_1", DayTimeIntervalType(0, 1)), + StructField("interval_col_day_minute_0_2", DayTimeIntervalType(0, 2)), + StructField("interval_col_day_second_0_3", DayTimeIntervalType(0, 3)), + StructField("interval_col_hour_minute_1_2", DayTimeIntervalType(1, 2)), + StructField("interval_col_hour_second_1_3", DayTimeIntervalType(1, 3)), + StructField("interval_col_minute_second_2_3", DayTimeIntervalType(2, 3)), + ] + ) + df = session.create_dataframe(test_cases, schema=schema) + + test_result = df.collect() + + assert len(test_result) == 2 + + assert test_result[0][0] == timedelta(days=2) + assert test_result[0][1] == timedelta(hours=1) + assert test_result[0][2] == timedelta(minutes=30) + assert test_result[0][3] == timedelta(seconds=45) + assert test_result[0][4] == timedelta(days=1, hours=12) + assert test_result[0][5] == timedelta(days=1, hours=12, minutes=30) + assert test_result[0][6] == timedelta(days=1, hours=12, minutes=30, seconds=45) + assert test_result[0][7] == timedelta(seconds=45000) + assert test_result[0][8] == timedelta(seconds=45045) + assert test_result[0][9] == timedelta(seconds=1845) + + assert test_result[1][0] == timedelta(days=-3) + assert test_result[1][1] == timedelta(hours=-2) + assert test_result[1][2] == timedelta(minutes=-45) + assert test_result[1][3] == timedelta(seconds=-30) + assert test_result[1][4] == timedelta(days=-2, hours=-6) + assert test_result[1][5] == timedelta(days=-2, hours=-6, minutes=-15) + assert test_result[1][6] == timedelta(days=-2, hours=-6, minutes=-15, seconds=-30) + assert test_result[1][7] == timedelta(days=-1, seconds=18900) + assert test_result[1][8] == timedelta(days=-1, seconds=18885) + assert test_result[1][9] == timedelta(days=-1, seconds=80970) + + assert isinstance(df.schema.fields[0].datatype, DayTimeIntervalType) + assert ( + df.schema.fields[0].datatype.start_field == 0 + and df.schema.fields[0].datatype.end_field == 0 + ) # DAY + assert ( + df.schema.fields[1].datatype.start_field == 1 + and df.schema.fields[1].datatype.end_field == 1 + ) # HOUR + assert ( + df.schema.fields[2].datatype.start_field == 2 + and df.schema.fields[2].datatype.end_field == 2 + ) # MINUTE + assert ( + df.schema.fields[3].datatype.start_field == 3 + and df.schema.fields[3].datatype.end_field == 3 + ) # SECOND + assert ( + df.schema.fields[4].datatype.start_field == 0 + and df.schema.fields[4].datatype.end_field == 1 + ) # DAY TO HOUR + assert ( + df.schema.fields[5].datatype.start_field == 0 + and df.schema.fields[5].datatype.end_field == 2 + ) # DAY TO MINUTE + assert ( + df.schema.fields[6].datatype.start_field == 0 + and df.schema.fields[6].datatype.end_field == 3 + ) # DAY TO SECOND + assert ( + df.schema.fields[7].datatype.start_field == 1 + and df.schema.fields[7].datatype.end_field == 2 + ) # HOUR TO MINUTE + assert ( + df.schema.fields[8].datatype.start_field == 1 + and df.schema.fields[8].datatype.end_field == 3 + ) # HOUR TO SECOND + assert ( + df.schema.fields[9].datatype.start_field == 2 + and df.schema.fields[9].datatype.end_field == 3 + ) # MINUTE TO SECOND + + session.sql("alter session set feature_interval_types=disabled;").collect() + session.sql("alter session set enable_interval_subtypes=false;").collect() From 0f7f0a8650d7c148a20324b283a4ed3ac641e9f7 Mon Sep 17 00:00:00 2001 From: Felix He Date: Mon, 25 Aug 2025 10:37:34 -0700 Subject: [PATCH 23/39] SNOW-2272863: add skipifs --- tests/integ/scala/test_dataframe_suite.py | 8 ++++++-- tests/integ/test_dataframe.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/integ/scala/test_dataframe_suite.py b/tests/integ/scala/test_dataframe_suite.py index e6ce5d0aae..eeac27b4da 100644 --- a/tests/integ/scala/test_dataframe_suite.py +++ b/tests/integ/scala/test_dataframe_suite.py @@ -3472,8 +3472,12 @@ def test_day_time_interval_type_dataframe(session): @pytest.mark.skipif( - "config.getoption('local_testing_mode', default=False)" or IS_IN_STORED_PROC, - reason="FEAT: session.sql is not supported in stored proc or local testing", + "config.getoption('local_testing_mode', default=False)", + reason="FEAT: session.sql is not supported in local testing", +) +@pytest.mark.skipif( + IS_IN_STORED_PROC, + reason="FEAT: session.sql is not supported in stored procedure", ) def test_day_time_interval_type_dataframe(session): session.sql("alter session set feature_interval_types=enabled;").collect() diff --git a/tests/integ/test_dataframe.py b/tests/integ/test_dataframe.py index 7a00800fe0..ec63641972 100644 --- a/tests/integ/test_dataframe.py +++ b/tests/integ/test_dataframe.py @@ -1906,8 +1906,12 @@ def test_create_dataframe_with_day_time_interval_type(session): @pytest.mark.skipif( - "config.getoption('local_testing_mode', default=False)" or IS_IN_STORED_PROC, - reason="FEAT: session.sql is not supported in stored proc or local testing", + "config.getoption('local_testing_mode', default=False)", + reason="FEAT: session.sql is not supported in local testing", +) +@pytest.mark.skipif( + IS_IN_STORED_PROC, + reason="FEAT: session.sql is not supported in stored procedure", ) def test_create_dataframe_with_day_time_interval_type(session): session.sql("alter session set feature_interval_types=enabled;").collect() From ff34b481e07ba9b67e56a62e891c519cd182156c Mon Sep 17 00:00:00 2001 From: Felix He Date: Tue, 19 Aug 2025 09:58:20 -0700 Subject: [PATCH 24/39] SNOW-2272863: Add DayTimeInterval support --- .../_internal/analyzer/datatype_mapper.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/snowflake/snowpark/_internal/analyzer/datatype_mapper.py b/src/snowflake/snowpark/_internal/analyzer/datatype_mapper.py index cb7f2a7944..a8dc366c6a 100644 --- a/src/snowflake/snowpark/_internal/analyzer/datatype_mapper.py +++ b/src/snowflake/snowpark/_internal/analyzer/datatype_mapper.py @@ -265,6 +265,23 @@ def str_to_sql_for_day_time_interval(value: str, datatype: DayTimeIntervalType) return f"INTERVAL '{extracted_value}' {datatype._FIELD_NAMES[start_field].upper()} TO {datatype._FIELD_NAMES[end_field].upper()}" +def str_to_sql_for_day_time_interval(value: str, datatype: DayTimeIntervalType) -> str: + start_field = datatype.start_field if datatype.start_field is not None else 0 + end_field = datatype.end_field if datatype.end_field is not None else 1 + if datatype.start_field == datatype.end_field: + extracted_value = value.split(" ")[1] + return ( + f"INTERVAL '{extracted_value}' {datatype._FIELD_NAMES[start_field].upper()}" + ) + else: + extracted_value = value.split(" ")[1] + if datatype.start_field == 0: + second_value = value.split(" ")[2] + return f"INTERVAL '{extracted_value} {second_value}' {datatype._FIELD_NAMES[start_field].upper()} TO {datatype._FIELD_NAMES[end_field].upper()}" + else: + return f"INTERVAL '{extracted_value}' {datatype._FIELD_NAMES[start_field].upper()} TO {datatype._FIELD_NAMES[end_field].upper()}" + + def float_nan_inf_to_sql(value: float) -> str: """ convert the float nan and inf value to a snowflake compatible sql. From f8ce1ca24445eeaa67b27da5413ece5acad25010 Mon Sep 17 00:00:00 2001 From: Felix He Date: Mon, 25 Aug 2025 14:08:00 -0700 Subject: [PATCH 25/39] SNOW-2296280: Add make_dt_interval function --- CHANGELOG.md | 1 + docs/source/snowpark/functions.rst | 1 + src/snowflake/snowpark/functions.py | 178 +++++++++++++++++++++++ tests/integ/scala/test_function_suite.py | 72 ++++++++- 4 files changed, 251 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d86bac13..2d0fbc1ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. - Added a new function `interval_year_month_from_parts` that allows users to easily create `YearMonthIntervalType` without using SQL. - Added a new datatype `DayTimeIntervalType` that allows users to create intervals for datetime operations. +- Added a new function `interval_day_time_from_parts` that allows users to easily create `DayTimeIntervalType` without using SQL. - Added support for `FileOperation.list` to list files in a stage with metadata. - Added support for `FileOperation.remove` to remove files in a stage. - Added a new function `snowflake.snowpark.functions.vectorized` that allows users to mark a function as vectorized UDF. diff --git a/docs/source/snowpark/functions.rst b/docs/source/snowpark/functions.rst index 9a7bd59279..49a9667678 100644 --- a/docs/source/snowpark/functions.rst +++ b/docs/source/snowpark/functions.rst @@ -283,6 +283,7 @@ Functions lpad ltrim make_interval + make_dt_interval map_cat map_concat map_contains_key diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 069db97980..1ad39d6885 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11042,6 +11042,184 @@ def get_col_name(col): return res +@publicapi +def make_dt_interval( + days: Union[ColumnOrName, int, None] = None, + hours: Union[ColumnOrName, int, None] = None, + mins: Union[ColumnOrName, int, None] = None, + secs: Union[ColumnOrName, int, None] = None, + _emit_ast: bool = True, +) -> Column: + """ + Creates a day-time interval expression using with specified days, hours, mins and seconds. + + This DayTime is not to be confused with the interval created by make_interval. + You can define a table column to be of data type DayTimeIntervalType. + + Args: + days: The number of days, positive or negative (defaults to 0) + hours: The number of hours, positive or negative (defaults to 0) + mins: The number of minutes, positive or negative (defaults to 0) + secs: The number of seconds, positive or negative (defaults to 0) + + Returns: + A Column representing a day-time interval + + Example:: + + >>> from snowflake.snowpark.functions import make_dt_interval + >>> + >>> df = session.create_dataframe([[1, 12, 30, 01.001001]], ['day', 'hour', 'min', 'sec']) + >>> df.select(make_dt_interval(col("day"), col("hour"), col("min"), col("sec")).alias("interval")).show() + ------------------ + |"INTERVAL" | + ------------------ + |1 12:30:01.001 | + ------------------ + + + """ + import builtins + + # Check original types before converting None values to determine the path + original_days = days + original_hours = hours + original_mins = mins + original_secs = secs + + # Convert None to 0 for type checking + days = 0 if days is None else days + hours = 0 if hours is None else hours + mins = 0 if mins is None else mins + secs = 0 if secs is None else secs + + # Use original values to determine if all are literals (int/float/None) + if ( + isinstance(original_days, (int, float, type(None))) + and isinstance(original_hours, (int, float, type(None))) + and isinstance(original_mins, (int, float, type(None))) + and isinstance(original_secs, (int, float, type(None))) + ): + # All are literals (including None) - use literal path + + # Convert all values to handle potential floats + total_seconds = days * 86400 + hours * 3600 + mins * 60 + secs + + # Determine sign + is_negative = total_seconds < 0 + abs_total_seconds = builtins.abs(total_seconds) + + # Extract components + days_part = int(abs_total_seconds // 86400) + remaining_seconds = abs_total_seconds % 86400 + hours_part = int(remaining_seconds // 3600) + remaining_seconds = remaining_seconds % 3600 + mins_part = int(remaining_seconds // 60) + secs_part = remaining_seconds % 60 + + if secs_part == int(secs_part): + secs_formatted = f"{int(secs_part):02d}" + else: + secs_formatted = f"{secs_part:06.3f}" + + # Build the interval string in format: D HH:MM:SS.FFF + sign_prefix = "-" if is_negative else "" + interval_value = f"{sign_prefix}{days_part} {hours_part:02d}:{mins_part:02d}:{secs_formatted}" + + return sql_expr( + f"""INTERVAL '{interval_value}' DAY TO SECOND""", + _emit_ast=_emit_ast, + ) + else: + # Handle None values by converting to lit(0) for column operations + days_col = ( + lit(0) + if original_days is None + else _to_col_if_str(original_days, "make_dt_interval") + ) + hours_col = ( + lit(0) + if original_hours is None + else _to_col_if_str(original_hours, "make_dt_interval") + ) + mins_col = ( + lit(0) + if original_mins is None + else _to_col_if_str(original_mins, "make_dt_interval") + ) + secs_col = ( + lit(0) + if original_secs is None + else _to_col_if_str(original_secs, "make_dt_interval") + ) + + total_seconds = ( + days_col * lit(86400) + + hours_col * lit(3600) + + mins_col * lit(60) + + secs_col + ) + + is_negative = total_seconds < lit(0) + abs_total_seconds = iff(is_negative, total_seconds * lit(-1), total_seconds) + + days_part = cast(floor(abs_total_seconds / lit(86400)), "int") + remaining_after_days = abs_total_seconds % lit(86400) + + hours_part = cast(floor(remaining_after_days / lit(3600)), "int") + remaining_after_hours = remaining_after_days % lit(3600) + + mins_part = cast(floor(remaining_after_hours / lit(60)), "int") + secs_part = remaining_after_hours % lit(60) + + hours_str = iff( + hours_part < lit(10), + concat(lit("0"), cast(hours_part, "str")), + cast(hours_part, "str"), + ) + + mins_str = iff( + mins_part < lit(10), + concat(lit("0"), cast(mins_part, "str")), + cast(mins_part, "str"), + ) + + secs_int = cast(floor(secs_part), "int") + secs_str = iff( + secs_int < lit(10), + concat(lit("0"), cast(secs_int, "str")), + cast(secs_int, "str"), + ) + + has_fraction = secs_part != cast(secs_int, "double") + fractional_part = secs_part - cast(secs_int, "double") + + fraction_str = iff( + has_fraction, + concat( + lit("."), + lpad(cast(round(fractional_part * lit(1000)), "str"), 3, lit("0")), + ), + lit(""), + ) + + secs_formatted = concat(secs_str, fraction_str) + + sign_prefix = iff(is_negative, lit("-"), lit("")) + interval_value = concat( + sign_prefix, + cast(days_part, "str"), + lit(" "), + hours_str, + lit(":"), + mins_str, + lit(":"), + secs_formatted, + ) + + return cast(interval_value, "INTERVAL DAY TO SECOND") + + @publicapi @deprecated( version="1.28.0", diff --git a/tests/integ/scala/test_function_suite.py b/tests/integ/scala/test_function_suite.py index 7504db2ad3..323980c3f8 100644 --- a/tests/integ/scala/test_function_suite.py +++ b/tests/integ/scala/test_function_suite.py @@ -5,7 +5,7 @@ import json from contextlib import contextmanager -from datetime import date, datetime, time +from datetime import date, datetime, time, timedelta from decimal import Decimal from functools import partial @@ -136,6 +136,7 @@ lpad, ltrim, interval_year_month_from_parts, + make_dt_interval, max, md5, mean, @@ -5724,3 +5725,72 @@ def test_interval_year_month_from_parts(session): assert result_nulls[0]['interval_year_month_from_parts("YEARS", "MONTHS")'] is None assert result_nulls[1]['interval_year_month_from_parts("YEARS", "MONTHS")'] is None assert result_nulls[2]['interval_year_month_from_parts("YEARS", "MONTHS")'] is None + + +@pytest.mark.skipif( + "config.getoption('local_testing_mode', default=False)", + reason="FEAT: Alter Session not supported in local testing", +) +@pytest.mark.skipif( + IS_IN_STORED_PROC, reason="Alter Session not supported in stored procedure." +) +def test_make_dt_interval(session): + session.sql("alter session set feature_interval_types=enabled;").collect() + + df = session.create_dataframe([[1]], ["dummy"]) + + result = df.select(make_dt_interval(1, 2, 30, 15.5).alias("interval")).collect() + assert result[0]["INTERVAL"] == timedelta(days=1, seconds=9015, microseconds=500000) + + result = df.select(make_dt_interval(5).alias("interval")).collect() + assert result[0]["INTERVAL"] == "+000000005 00:00:00.000000000" + + result = df.select(make_dt_interval(hours=12).alias("interval")).collect() + assert result[0]["INTERVAL"] == timedelta(hours=12) + + result = df.select(make_dt_interval(secs=30.123).alias("interval")).collect() + assert result[0]["INTERVAL"] == timedelta(seconds=30.123) + + result = df.select(make_dt_interval(-2, 3, 15, 30).alias("interval")).collect() + assert result[0]["INTERVAL"] == timedelta(days=-2, hours=3, minutes=15, seconds=30) + + result = df.select(make_dt_interval(-1, -2, -30, -15).alias("interval")).collect() + assert result[0]["INTERVAL"] == timedelta( + days=-1, hours=-2, minutes=-30, seconds=-15 + ) + + result = df.select(make_dt_interval(0, 0, 0, 0).alias("interval")).collect() + assert result[0]["INTERVAL"] == timedelta() + + result = df.select(make_dt_interval(0, 5, 0, 30).alias("interval")).collect() + assert result[0]["INTERVAL"] == timedelta(hours=5, seconds=30) + + df_cols = session.create_dataframe( + [[1, 12, 30, 15.001], [2, 6, 45, 30.500], [0, 23, 59, 59.999]], + ["days", "hours", "mins", "secs"], + ) + + result = df_cols.select( + make_dt_interval(col("days"), col("hours"), col("mins"), col("secs")).alias( + "interval" + ) + ).collect() + + assert result[0]["INTERVAL"] == timedelta( + days=1, hours=12, minutes=30, seconds=15.001 + ) + assert result[1]["INTERVAL"] == timedelta( + days=2, hours=6, minutes=45, seconds=30.500 + ) + assert result[2]["INTERVAL"] == timedelta(hours=23, minutes=59, seconds=59.999) + + df_mixed = session.create_dataframe([[1, 30], [2, 45]], ["days", "mins"]) + + result = df_mixed.select( + make_dt_interval(days=col("days"), mins=col("mins")).alias("interval") + ).collect() + + assert result[0]["INTERVAL"] == timedelta(days=1, minutes=30) + assert result[1]["INTERVAL"] == timedelta(days=2, minutes=45) + + session.sql("alter session set feature_interval_types=disabled;").collect() From bf31efeeb9b1b97e46aebe2dca93145cd98194ce Mon Sep 17 00:00:00 2001 From: Felix He Date: Tue, 26 Aug 2025 15:09:59 -0700 Subject: [PATCH 26/39] SNOW-2296280: update tests and column name handling --- src/snowflake/snowpark/functions.py | 200 ++++++++--------------- tests/integ/scala/test_function_suite.py | 178 ++++++++++++++++---- 2 files changed, 210 insertions(+), 168 deletions(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 1ad39d6885..35f40b1709 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11044,10 +11044,10 @@ def get_col_name(col): @publicapi def make_dt_interval( - days: Union[ColumnOrName, int, None] = None, - hours: Union[ColumnOrName, int, None] = None, - mins: Union[ColumnOrName, int, None] = None, - secs: Union[ColumnOrName, int, None] = None, + days: Optional[ColumnOrName] = None, + hours: Optional[ColumnOrName] = None, + mins: Optional[ColumnOrName] = None, + secs: Optional[ColumnOrName] = None, _emit_ast: bool = True, ) -> Column: """ @@ -11057,10 +11057,10 @@ def make_dt_interval( You can define a table column to be of data type DayTimeIntervalType. Args: - days: The number of days, positive or negative (defaults to 0) - hours: The number of hours, positive or negative (defaults to 0) - mins: The number of minutes, positive or negative (defaults to 0) - secs: The number of seconds, positive or negative (defaults to 0) + days: The number of days, positive or negative + hours: The number of hours, positive or negative + mins: The number of minutes, positive or negative + secs: The number of seconds, positive or negative Returns: A Column representing a day-time interval @@ -11079,145 +11079,81 @@ def make_dt_interval( """ - import builtins + days_col = lit(0) if days is None else _to_col_if_str(days, "make_dt_interval") + hours_col = lit(0) if hours is None else _to_col_if_str(hours, "make_dt_interval") + mins_col = lit(0) if mins is None else _to_col_if_str(mins, "make_dt_interval") + secs_col = lit(0) if secs is None else _to_col_if_str(secs, "make_dt_interval") - # Check original types before converting None values to determine the path - original_days = days - original_hours = hours - original_mins = mins - original_secs = secs - - # Convert None to 0 for type checking - days = 0 if days is None else days - hours = 0 if hours is None else hours - mins = 0 if mins is None else mins - secs = 0 if secs is None else secs - - # Use original values to determine if all are literals (int/float/None) - if ( - isinstance(original_days, (int, float, type(None))) - and isinstance(original_hours, (int, float, type(None))) - and isinstance(original_mins, (int, float, type(None))) - and isinstance(original_secs, (int, float, type(None))) - ): - # All are literals (including None) - use literal path - - # Convert all values to handle potential floats - total_seconds = days * 86400 + hours * 3600 + mins * 60 + secs - - # Determine sign - is_negative = total_seconds < 0 - abs_total_seconds = builtins.abs(total_seconds) - - # Extract components - days_part = int(abs_total_seconds // 86400) - remaining_seconds = abs_total_seconds % 86400 - hours_part = int(remaining_seconds // 3600) - remaining_seconds = remaining_seconds % 3600 - mins_part = int(remaining_seconds // 60) - secs_part = remaining_seconds % 60 - - if secs_part == int(secs_part): - secs_formatted = f"{int(secs_part):02d}" - else: - secs_formatted = f"{secs_part:06.3f}" + total_seconds = ( + days_col * lit(86400) + hours_col * lit(3600) + mins_col * lit(60) + secs_col + ) - # Build the interval string in format: D HH:MM:SS.FFF - sign_prefix = "-" if is_negative else "" - interval_value = f"{sign_prefix}{days_part} {hours_part:02d}:{mins_part:02d}:{secs_formatted}" + is_negative = total_seconds < lit(0) + abs_total_seconds = abs(total_seconds) - return sql_expr( - f"""INTERVAL '{interval_value}' DAY TO SECOND""", - _emit_ast=_emit_ast, - ) - else: - # Handle None values by converting to lit(0) for column operations - days_col = ( - lit(0) - if original_days is None - else _to_col_if_str(original_days, "make_dt_interval") - ) - hours_col = ( - lit(0) - if original_hours is None - else _to_col_if_str(original_hours, "make_dt_interval") - ) - mins_col = ( - lit(0) - if original_mins is None - else _to_col_if_str(original_mins, "make_dt_interval") - ) - secs_col = ( - lit(0) - if original_secs is None - else _to_col_if_str(original_secs, "make_dt_interval") - ) + days_part = cast(floor(abs_total_seconds / lit(86400)), "int") + remaining_after_days = abs_total_seconds % lit(86400) - total_seconds = ( - days_col * lit(86400) - + hours_col * lit(3600) - + mins_col * lit(60) - + secs_col - ) + hours_part = cast(floor(remaining_after_days / lit(3600)), "int") + remaining_after_hours = remaining_after_days % lit(3600) - is_negative = total_seconds < lit(0) - abs_total_seconds = iff(is_negative, total_seconds * lit(-1), total_seconds) + mins_part = cast(floor(remaining_after_hours / lit(60)), "int") + secs_part = remaining_after_hours % lit(60) - days_part = cast(floor(abs_total_seconds / lit(86400)), "int") - remaining_after_days = abs_total_seconds % lit(86400) + hours_str = iff( + hours_part < lit(10), + concat(lit("0"), cast(hours_part, "str")), + cast(hours_part, "str"), + ) - hours_part = cast(floor(remaining_after_days / lit(3600)), "int") - remaining_after_hours = remaining_after_days % lit(3600) + mins_str = iff( + mins_part < lit(10), + concat(lit("0"), cast(mins_part, "str")), + cast(mins_part, "str"), + ) - mins_part = cast(floor(remaining_after_hours / lit(60)), "int") - secs_part = remaining_after_hours % lit(60) + secs_int = cast(floor(secs_part), "int") + secs_str = iff( + secs_int < lit(10), + concat(lit("0"), cast(secs_int, "str")), + cast(secs_int, "str"), + ) - hours_str = iff( - hours_part < lit(10), - concat(lit("0"), cast(hours_part, "str")), - cast(hours_part, "str"), - ) + has_fraction = secs_part != cast(secs_int, "double") + fractional_part = secs_part - cast(secs_int, "double") - mins_str = iff( - mins_part < lit(10), - concat(lit("0"), cast(mins_part, "str")), - cast(mins_part, "str"), - ) + fraction_str = iff( + has_fraction, + concat( + lit("."), + lpad(cast(round(fractional_part * lit(1000)), "str"), 3, lit("0")), + ), + lit(""), + ) - secs_int = cast(floor(secs_part), "int") - secs_str = iff( - secs_int < lit(10), - concat(lit("0"), cast(secs_int, "str")), - cast(secs_int, "str"), - ) + secs_formatted = concat(secs_str, fraction_str) - has_fraction = secs_part != cast(secs_int, "double") - fractional_part = secs_part - cast(secs_int, "double") + sign_prefix = iff(is_negative, lit("-"), lit("")) + interval_value = concat( + sign_prefix, + cast(days_part, "str"), + lit(" "), + hours_str, + lit(":"), + mins_str, + lit(":"), + secs_formatted, + ) - fraction_str = iff( - has_fraction, - concat( - lit("."), - lpad(cast(round(fractional_part * lit(1000)), "str"), 3, lit("0")), - ), - lit(""), - ) + def get_col_name(col, original_param): + if original_param is None: + return "0" + else: + return str(col._expr1) - secs_formatted = concat(secs_str, fraction_str) - - sign_prefix = iff(is_negative, lit("-"), lit("")) - interval_value = concat( - sign_prefix, - cast(days_part, "str"), - lit(" "), - hours_str, - lit(":"), - mins_str, - lit(":"), - secs_formatted, - ) + alias_name = f"make_dt_interval({get_col_name(days_col, days)}, {get_col_name(hours_col, hours)}, {get_col_name(mins_col, mins)}, {get_col_name(secs_col, secs)})" - return cast(interval_value, "INTERVAL DAY TO SECOND") + return cast(interval_value, "INTERVAL DAY TO SECOND").alias(alias_name) @publicapi diff --git a/tests/integ/scala/test_function_suite.py b/tests/integ/scala/test_function_suite.py index 323980c3f8..e5889ca67b 100644 --- a/tests/integ/scala/test_function_suite.py +++ b/tests/integ/scala/test_function_suite.py @@ -216,6 +216,7 @@ from snowflake.snowpark.mock._functions import LocalTimezone from snowflake.snowpark.types import ( DateType, + DayTimeIntervalType, StructField, StructType, TimestampTimeZone, @@ -5737,60 +5738,165 @@ def test_interval_year_month_from_parts(session): def test_make_dt_interval(session): session.sql("alter session set feature_interval_types=enabled;").collect() - df = session.create_dataframe([[1]], ["dummy"]) + df = session.create_dataframe( + [ + (0, 0, 0, 0.0), + (1, 0, 0, 0.0), + (0, 1, 0, 0.0), + (0, 0, 1, 0.0), + (0, 0, 0, 1.0), + (1, 2, 3, 4.5), + (5, 10, 30, 45.123), + (0, 25, 90, 120.999), + (-1, 0, 0, 0.0), + (0, -1, 0, 0.0), + (0, 0, -1, 0.0), + (0, 0, 0, -1.0), + (-1, -2, -3, -4.5), + (2, -1, 30, -15.0), + (365, 24, 60, 3600.0), + ], + schema=["days", "hours", "mins", "secs"], + ) + + result = df.select( + make_dt_interval(col("days"), col("hours"), col("mins"), col("secs")).alias( + "interval_result" + ), + ).collect() - result = df.select(make_dt_interval(1, 2, 30, 15.5).alias("interval")).collect() - assert result[0]["INTERVAL"] == timedelta(days=1, seconds=9015, microseconds=500000) + expected_values = [ + timedelta(0), + timedelta(days=1), + timedelta(hours=1), + timedelta(minutes=1), + timedelta(seconds=1.0), + timedelta(days=1, hours=2, minutes=3, seconds=4.5), + timedelta(days=5, hours=10, minutes=30, seconds=45.123), + timedelta(hours=25, minutes=90, seconds=120.999), + timedelta(days=-1), + timedelta(hours=-1), + timedelta(minutes=-1), + timedelta(seconds=-1.0), + timedelta(days=-1, hours=-2, minutes=-3, seconds=-4.5), + timedelta(days=2, hours=-1, minutes=30, seconds=-15.0), + timedelta(days=365, hours=24, minutes=60, seconds=3600.0), + ] - result = df.select(make_dt_interval(5).alias("interval")).collect() - assert result[0]["INTERVAL"] == "+000000005 00:00:00.000000000" + assert len(result) == 15 + for i, expected in enumerate(expected_values): + assert result[i]["INTERVAL_RESULT"] == expected - result = df.select(make_dt_interval(hours=12).alias("interval")).collect() - assert result[0]["INTERVAL"] == timedelta(hours=12) + df_only_days = session.create_dataframe([(10,), (-5,), (0,)], schema=["days"]) + days_schema_result = df_only_days.select(make_dt_interval(days=col("days"))) + assert days_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) - result = df.select(make_dt_interval(secs=30.123).alias("interval")).collect() - assert result[0]["INTERVAL"] == timedelta(seconds=30.123) + result_days = days_schema_result.collect() + assert result_days[0]["make_dt_interval(days, 0, 0, 0)"] == timedelta(days=10) + assert result_days[1]["make_dt_interval(days, 0, 0, 0)"] == timedelta(days=-5) + assert result_days[2]["make_dt_interval(days, 0, 0, 0)"] == timedelta(0) - result = df.select(make_dt_interval(-2, 3, 15, 30).alias("interval")).collect() - assert result[0]["INTERVAL"] == timedelta(days=-2, hours=3, minutes=15, seconds=30) + df_only_hours = session.create_dataframe([(25,), (-12,), (0,)], schema=["hours"]) + hours_schema_result = df_only_hours.select(make_dt_interval(hours=col("hours"))) + assert hours_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) - result = df.select(make_dt_interval(-1, -2, -30, -15).alias("interval")).collect() - assert result[0]["INTERVAL"] == timedelta( - days=-1, hours=-2, minutes=-30, seconds=-15 + result_hours = hours_schema_result.collect() + assert result_hours[0]["make_dt_interval(0, hours, 0, 0)"] == timedelta(hours=25) + assert result_hours[1]["make_dt_interval(0, hours, 0, 0)"] == timedelta(hours=-12) + assert result_hours[2]["make_dt_interval(0, hours, 0, 0)"] == timedelta(0) + + df_only_mins = session.create_dataframe([(90,), (-45,), (0,)], schema=["mins"]) + mins_schema_result = df_only_mins.select(make_dt_interval(mins=col("mins"))) + assert mins_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) + + result_mins = mins_schema_result.collect() + assert result_mins[0]["make_dt_interval(0, 0, mins, 0)"] == timedelta(minutes=90) + assert result_mins[1]["make_dt_interval(0, 0, mins, 0)"] == timedelta(minutes=-45) + assert result_mins[2]["make_dt_interval(0, 0, mins, 0)"] == timedelta(0) + + df_only_secs = session.create_dataframe( + [(3661.5,), (-1800.25,), (0.0,)], schema=["secs"] ) + secs_schema_result = df_only_secs.select(make_dt_interval(secs=col("secs"))) + assert secs_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) - result = df.select(make_dt_interval(0, 0, 0, 0).alias("interval")).collect() - assert result[0]["INTERVAL"] == timedelta() + result_secs = secs_schema_result.collect() + assert result_secs[0]["make_dt_interval(0, 0, 0, secs)"] == timedelta( + seconds=3661.5 + ) + assert result_secs[1]["make_dt_interval(0, 0, 0, secs)"] == timedelta( + seconds=-1800.25 + ) + assert result_secs[2]["make_dt_interval(0, 0, 0, secs)"] == timedelta(0) + + df_literals = session.create_dataframe([(1,)], schema=["dummy"]) + literals_schema_result = df_literals.select( + make_dt_interval(lit(1), lit(2), lit(3), lit(4.5)).alias("all_literal"), + make_dt_interval(days=lit(7)).alias("days_only"), + make_dt_interval(hours=lit(12)).alias("hours_only"), + make_dt_interval(mins=lit(30)).alias("mins_only"), + make_dt_interval(secs=lit(45.5)).alias("secs_only"), + ) - result = df.select(make_dt_interval(0, 5, 0, 30).alias("interval")).collect() - assert result[0]["INTERVAL"] == timedelta(hours=5, seconds=30) + for field in literals_schema_result.schema.fields: + assert field.datatype == DayTimeIntervalType(0, 3) - df_cols = session.create_dataframe( - [[1, 12, 30, 15.001], [2, 6, 45, 30.500], [0, 23, 59, 59.999]], - ["days", "hours", "mins", "secs"], + result_literals = literals_schema_result.collect() + assert result_literals[0]["ALL_LITERAL"] == timedelta( + days=1, hours=2, minutes=3, seconds=4.5 ) + assert result_literals[0]["DAYS_ONLY"] == timedelta(days=7) + assert result_literals[0]["HOURS_ONLY"] == timedelta(hours=12) + assert result_literals[0]["MINS_ONLY"] == timedelta(minutes=30) + assert result_literals[0]["SECS_ONLY"] == timedelta(seconds=45.5) - result = df_cols.select( - make_dt_interval(col("days"), col("hours"), col("mins"), col("secs")).alias( - "interval" - ) - ).collect() + df_mixed_params = session.create_dataframe( + [(2, 30), (1, 90), (0, 0)], schema=["days", "mins"] + ) + mixed_schema_result = df_mixed_params.select( + make_dt_interval(days=col("days"), mins=col("mins")) + ) + assert mixed_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) - assert result[0]["INTERVAL"] == timedelta( - days=1, hours=12, minutes=30, seconds=15.001 + result_mixed = mixed_schema_result.collect() + assert result_mixed[0]["make_dt_interval(days, 0, mins, 0)"] == timedelta( + days=2, minutes=30 ) - assert result[1]["INTERVAL"] == timedelta( - days=2, hours=6, minutes=45, seconds=30.500 + assert result_mixed[1]["make_dt_interval(days, 0, mins, 0)"] == timedelta( + days=1, minutes=90 ) - assert result[2]["INTERVAL"] == timedelta(hours=23, minutes=59, seconds=59.999) + assert result_mixed[2]["make_dt_interval(days, 0, mins, 0)"] == timedelta(0) - df_mixed = session.create_dataframe([[1, 30], [2, 45]], ["days", "mins"]) + df_schema_test = session.create_dataframe( + [(1, 2, 3, 4.0)], schema=["d", "h", "m", "s"] + ) + schema_result = df_schema_test.select( + make_dt_interval(col("d"), col("h"), col("m"), col("s")) + ) + schema_fields = schema_result.schema.fields + assert len(schema_fields) == 1 + assert schema_fields[0].datatype == DayTimeIntervalType(0, 3) - result = df_mixed.select( - make_dt_interval(days=col("days"), mins=col("mins")).alias("interval") + df_nulls = session.create_dataframe( + [ + (None, 1, 2, 3.0), + (1, None, 2, 3.0), + (1, 2, None, 3.0), + (1, 2, 3, None), + (None, None, None, None), + ], + schema=["days", "hours", "mins", "secs"], + ) + result_nulls = df_nulls.select( + make_dt_interval(col("days"), col("hours"), col("mins"), col("secs")).alias( + "interval_result" + ) ).collect() - assert result[0]["INTERVAL"] == timedelta(days=1, minutes=30) - assert result[1]["INTERVAL"] == timedelta(days=2, minutes=45) + assert result_nulls[0]["INTERVAL_RESULT"] is None + assert result_nulls[1]["INTERVAL_RESULT"] is None + assert result_nulls[2]["INTERVAL_RESULT"] is None + assert result_nulls[3]["INTERVAL_RESULT"] is None + assert result_nulls[4]["INTERVAL_RESULT"] is None session.sql("alter session set feature_interval_types=disabled;").collect() From 4fb4e9ed516f188d05a4cecd70a07848057aeca2 Mon Sep 17 00:00:00 2001 From: Felix He Date: Tue, 26 Aug 2025 15:49:43 -0700 Subject: [PATCH 27/39] SNOW-2296280: updated column name getter --- src/snowflake/snowpark/functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 35f40b1709..1571c68c1d 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11145,13 +11145,13 @@ def make_dt_interval( secs_formatted, ) - def get_col_name(col, original_param): - if original_param is None: - return "0" + def get_col_name(col): + if isinstance(col._expr1, Literal): + return str(col._expr1.value) else: return str(col._expr1) - alias_name = f"make_dt_interval({get_col_name(days_col, days)}, {get_col_name(hours_col, hours)}, {get_col_name(mins_col, mins)}, {get_col_name(secs_col, secs)})" + alias_name = f"make_dt_interval({get_col_name(days_col)}, {get_col_name(hours_col)}, {get_col_name(mins_col)}, {get_col_name(secs_col)})" return cast(interval_value, "INTERVAL DAY TO SECOND").alias(alias_name) From 9908689b741c936850a5382d9edc2a36f6b6d5c1 Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 3 Sep 2025 13:34:40 -0700 Subject: [PATCH 28/39] SNOW-2296280: updated function name --- docs/source/snowpark/functions.rst | 2 +- .../_internal/analyzer/datatype_mapper.py | 17 --- src/snowflake/snowpark/functions.py | 26 +++-- tests/integ/scala/test_function_suite.py | 102 +++++++++++------- 4 files changed, 83 insertions(+), 64 deletions(-) diff --git a/docs/source/snowpark/functions.rst b/docs/source/snowpark/functions.rst index 49a9667678..dc0c502212 100644 --- a/docs/source/snowpark/functions.rst +++ b/docs/source/snowpark/functions.rst @@ -283,7 +283,7 @@ Functions lpad ltrim make_interval - make_dt_interval + interval_day_time_from_parts map_cat map_concat map_contains_key diff --git a/src/snowflake/snowpark/_internal/analyzer/datatype_mapper.py b/src/snowflake/snowpark/_internal/analyzer/datatype_mapper.py index a8dc366c6a..cb7f2a7944 100644 --- a/src/snowflake/snowpark/_internal/analyzer/datatype_mapper.py +++ b/src/snowflake/snowpark/_internal/analyzer/datatype_mapper.py @@ -265,23 +265,6 @@ def str_to_sql_for_day_time_interval(value: str, datatype: DayTimeIntervalType) return f"INTERVAL '{extracted_value}' {datatype._FIELD_NAMES[start_field].upper()} TO {datatype._FIELD_NAMES[end_field].upper()}" -def str_to_sql_for_day_time_interval(value: str, datatype: DayTimeIntervalType) -> str: - start_field = datatype.start_field if datatype.start_field is not None else 0 - end_field = datatype.end_field if datatype.end_field is not None else 1 - if datatype.start_field == datatype.end_field: - extracted_value = value.split(" ")[1] - return ( - f"INTERVAL '{extracted_value}' {datatype._FIELD_NAMES[start_field].upper()}" - ) - else: - extracted_value = value.split(" ")[1] - if datatype.start_field == 0: - second_value = value.split(" ")[2] - return f"INTERVAL '{extracted_value} {second_value}' {datatype._FIELD_NAMES[start_field].upper()} TO {datatype._FIELD_NAMES[end_field].upper()}" - else: - return f"INTERVAL '{extracted_value}' {datatype._FIELD_NAMES[start_field].upper()} TO {datatype._FIELD_NAMES[end_field].upper()}" - - def float_nan_inf_to_sql(value: float) -> str: """ convert the float nan and inf value to a snowflake compatible sql. diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 1571c68c1d..d3335b191e 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11043,7 +11043,7 @@ def get_col_name(col): @publicapi -def make_dt_interval( +def interval_day_time_from_parts( days: Optional[ColumnOrName] = None, hours: Optional[ColumnOrName] = None, mins: Optional[ColumnOrName] = None, @@ -11067,10 +11067,10 @@ def make_dt_interval( Example:: - >>> from snowflake.snowpark.functions import make_dt_interval + >>> from snowflake.snowpark.functions import interval_day_time_from_parts >>> >>> df = session.create_dataframe([[1, 12, 30, 01.001001]], ['day', 'hour', 'min', 'sec']) - >>> df.select(make_dt_interval(col("day"), col("hour"), col("min"), col("sec")).alias("interval")).show() + >>> df.select(interval_day_time_from_parts(col("day"), col("hour"), col("min"), col("sec")).alias("interval")).show() ------------------ |"INTERVAL" | ------------------ @@ -11079,10 +11079,20 @@ def make_dt_interval( """ - days_col = lit(0) if days is None else _to_col_if_str(days, "make_dt_interval") - hours_col = lit(0) if hours is None else _to_col_if_str(hours, "make_dt_interval") - mins_col = lit(0) if mins is None else _to_col_if_str(mins, "make_dt_interval") - secs_col = lit(0) if secs is None else _to_col_if_str(secs, "make_dt_interval") + days_col = ( + lit(0) if days is None else _to_col_if_str(days, "interval_day_time_from_parts") + ) + hours_col = ( + lit(0) + if hours is None + else _to_col_if_str(hours, "interval_day_time_from_parts") + ) + mins_col = ( + lit(0) if mins is None else _to_col_if_str(mins, "interval_day_time_from_parts") + ) + secs_col = ( + lit(0) if secs is None else _to_col_if_str(secs, "interval_day_time_from_parts") + ) total_seconds = ( days_col * lit(86400) + hours_col * lit(3600) + mins_col * lit(60) + secs_col @@ -11151,7 +11161,7 @@ def get_col_name(col): else: return str(col._expr1) - alias_name = f"make_dt_interval({get_col_name(days_col)}, {get_col_name(hours_col)}, {get_col_name(mins_col)}, {get_col_name(secs_col)})" + alias_name = f"interval_day_time_from_parts({get_col_name(days_col)}, {get_col_name(hours_col)}, {get_col_name(mins_col)}, {get_col_name(secs_col)})" return cast(interval_value, "INTERVAL DAY TO SECOND").alias(alias_name) diff --git a/tests/integ/scala/test_function_suite.py b/tests/integ/scala/test_function_suite.py index e5889ca67b..26bc411b80 100644 --- a/tests/integ/scala/test_function_suite.py +++ b/tests/integ/scala/test_function_suite.py @@ -136,7 +136,7 @@ lpad, ltrim, interval_year_month_from_parts, - make_dt_interval, + interval_day_time_from_parts, max, md5, mean, @@ -5735,7 +5735,7 @@ def test_interval_year_month_from_parts(session): @pytest.mark.skipif( IS_IN_STORED_PROC, reason="Alter Session not supported in stored procedure." ) -def test_make_dt_interval(session): +def test_interval_day_time_from_parts(session): session.sql("alter session set feature_interval_types=enabled;").collect() df = session.create_dataframe( @@ -5760,9 +5760,9 @@ def test_make_dt_interval(session): ) result = df.select( - make_dt_interval(col("days"), col("hours"), col("mins"), col("secs")).alias( - "interval_result" - ), + interval_day_time_from_parts( + col("days"), col("hours"), col("mins"), col("secs") + ).alias("interval_result"), ).collect() expected_values = [ @@ -5788,54 +5788,78 @@ def test_make_dt_interval(session): assert result[i]["INTERVAL_RESULT"] == expected df_only_days = session.create_dataframe([(10,), (-5,), (0,)], schema=["days"]) - days_schema_result = df_only_days.select(make_dt_interval(days=col("days"))) + days_schema_result = df_only_days.select( + interval_day_time_from_parts(days=col("days")) + ) assert days_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) result_days = days_schema_result.collect() - assert result_days[0]["make_dt_interval(days, 0, 0, 0)"] == timedelta(days=10) - assert result_days[1]["make_dt_interval(days, 0, 0, 0)"] == timedelta(days=-5) - assert result_days[2]["make_dt_interval(days, 0, 0, 0)"] == timedelta(0) + assert result_days[0]["interval_day_time_from_parts(days, 0, 0, 0)"] == timedelta( + days=10 + ) + assert result_days[1]["interval_day_time_from_parts(days, 0, 0, 0)"] == timedelta( + days=-5 + ) + assert result_days[2]["interval_day_time_from_parts(days, 0, 0, 0)"] == timedelta(0) df_only_hours = session.create_dataframe([(25,), (-12,), (0,)], schema=["hours"]) - hours_schema_result = df_only_hours.select(make_dt_interval(hours=col("hours"))) + hours_schema_result = df_only_hours.select( + interval_day_time_from_parts(hours=col("hours")) + ) assert hours_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) result_hours = hours_schema_result.collect() - assert result_hours[0]["make_dt_interval(0, hours, 0, 0)"] == timedelta(hours=25) - assert result_hours[1]["make_dt_interval(0, hours, 0, 0)"] == timedelta(hours=-12) - assert result_hours[2]["make_dt_interval(0, hours, 0, 0)"] == timedelta(0) + assert result_hours[0]["interval_day_time_from_parts(0, hours, 0, 0)"] == timedelta( + hours=25 + ) + assert result_hours[1]["interval_day_time_from_parts(0, hours, 0, 0)"] == timedelta( + hours=-12 + ) + assert result_hours[2]["interval_day_time_from_parts(0, hours, 0, 0)"] == timedelta( + 0 + ) df_only_mins = session.create_dataframe([(90,), (-45,), (0,)], schema=["mins"]) - mins_schema_result = df_only_mins.select(make_dt_interval(mins=col("mins"))) + mins_schema_result = df_only_mins.select( + interval_day_time_from_parts(mins=col("mins")) + ) assert mins_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) result_mins = mins_schema_result.collect() - assert result_mins[0]["make_dt_interval(0, 0, mins, 0)"] == timedelta(minutes=90) - assert result_mins[1]["make_dt_interval(0, 0, mins, 0)"] == timedelta(minutes=-45) - assert result_mins[2]["make_dt_interval(0, 0, mins, 0)"] == timedelta(0) + assert result_mins[0]["interval_day_time_from_parts(0, 0, mins, 0)"] == timedelta( + minutes=90 + ) + assert result_mins[1]["interval_day_time_from_parts(0, 0, mins, 0)"] == timedelta( + minutes=-45 + ) + assert result_mins[2]["interval_day_time_from_parts(0, 0, mins, 0)"] == timedelta(0) df_only_secs = session.create_dataframe( [(3661.5,), (-1800.25,), (0.0,)], schema=["secs"] ) - secs_schema_result = df_only_secs.select(make_dt_interval(secs=col("secs"))) + secs_schema_result = df_only_secs.select( + interval_day_time_from_parts(secs=col("secs")) + ) assert secs_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) result_secs = secs_schema_result.collect() - assert result_secs[0]["make_dt_interval(0, 0, 0, secs)"] == timedelta( + assert result_secs[0]["interval_day_time_from_parts(0, 0, 0, secs)"] == timedelta( seconds=3661.5 ) - assert result_secs[1]["make_dt_interval(0, 0, 0, secs)"] == timedelta( + assert result_secs[1]["interval_day_time_from_parts(0, 0, 0, secs)"] == timedelta( seconds=-1800.25 ) - assert result_secs[2]["make_dt_interval(0, 0, 0, secs)"] == timedelta(0) + assert result_secs[2]["interval_day_time_from_parts(0, 0, 0, secs)"] == timedelta(0) df_literals = session.create_dataframe([(1,)], schema=["dummy"]) literals_schema_result = df_literals.select( - make_dt_interval(lit(1), lit(2), lit(3), lit(4.5)).alias("all_literal"), - make_dt_interval(days=lit(7)).alias("days_only"), - make_dt_interval(hours=lit(12)).alias("hours_only"), - make_dt_interval(mins=lit(30)).alias("mins_only"), - make_dt_interval(secs=lit(45.5)).alias("secs_only"), + interval_day_time_from_parts(lit(1), lit(2), lit(3), lit(4.5)).alias( + "all_literal" + ), + interval_day_time_from_parts(days=lit(7)).alias("days_only"), + interval_day_time_from_parts(hours=lit(12)).alias("hours_only"), + interval_day_time_from_parts(mins=lit(30)).alias("mins_only"), + interval_day_time_from_parts(secs=lit(45.5)).alias("secs_only"), ) for field in literals_schema_result.schema.fields: @@ -5854,24 +5878,26 @@ def test_make_dt_interval(session): [(2, 30), (1, 90), (0, 0)], schema=["days", "mins"] ) mixed_schema_result = df_mixed_params.select( - make_dt_interval(days=col("days"), mins=col("mins")) + interval_day_time_from_parts(days=col("days"), mins=col("mins")) ) assert mixed_schema_result.schema.fields[0].datatype == DayTimeIntervalType(0, 3) result_mixed = mixed_schema_result.collect() - assert result_mixed[0]["make_dt_interval(days, 0, mins, 0)"] == timedelta( - days=2, minutes=30 - ) - assert result_mixed[1]["make_dt_interval(days, 0, mins, 0)"] == timedelta( - days=1, minutes=90 - ) - assert result_mixed[2]["make_dt_interval(days, 0, mins, 0)"] == timedelta(0) + assert result_mixed[0][ + "interval_day_time_from_parts(days, 0, mins, 0)" + ] == timedelta(days=2, minutes=30) + assert result_mixed[1][ + "interval_day_time_from_parts(days, 0, mins, 0)" + ] == timedelta(days=1, minutes=90) + assert result_mixed[2][ + "interval_day_time_from_parts(days, 0, mins, 0)" + ] == timedelta(0) df_schema_test = session.create_dataframe( [(1, 2, 3, 4.0)], schema=["d", "h", "m", "s"] ) schema_result = df_schema_test.select( - make_dt_interval(col("d"), col("h"), col("m"), col("s")) + interval_day_time_from_parts(col("d"), col("h"), col("m"), col("s")) ) schema_fields = schema_result.schema.fields assert len(schema_fields) == 1 @@ -5888,9 +5914,9 @@ def test_make_dt_interval(session): schema=["days", "hours", "mins", "secs"], ) result_nulls = df_nulls.select( - make_dt_interval(col("days"), col("hours"), col("mins"), col("secs")).alias( - "interval_result" - ) + interval_day_time_from_parts( + col("days"), col("hours"), col("mins"), col("secs") + ).alias("interval_result") ).collect() assert result_nulls[0]["INTERVAL_RESULT"] is None From 75aa70a24ce1c2a6c49dc0f1809f17247212bf37 Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 3 Sep 2025 17:37:19 -0700 Subject: [PATCH 29/39] SNOW-2296280: fix rebase and address comments --- src/snowflake/snowpark/functions.py | 4 + tests/integ/scala/test_dataframe_suite.py | 169 ---------------------- tests/integ/scala/test_function_suite.py | 4 - tests/integ/test_dataframe.py | 119 --------------- 4 files changed, 4 insertions(+), 292 deletions(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index d3335b191e..c2998ceada 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11042,6 +11042,10 @@ def get_col_name(col): return res +@private_preview( + version="1.38.0", + extra_doc_string="Type DayTimeIntervalType is currently in private preview and needs to be enabled by setting parameter `FEATURE_INTERVAL_TYPES` to `ENABLED`.", +) @publicapi def interval_day_time_from_parts( days: Optional[ColumnOrName] = None, diff --git a/tests/integ/scala/test_dataframe_suite.py b/tests/integ/scala/test_dataframe_suite.py index eeac27b4da..04cf7ed40b 100644 --- a/tests/integ/scala/test_dataframe_suite.py +++ b/tests/integ/scala/test_dataframe_suite.py @@ -3469,172 +3469,3 @@ def test_day_time_interval_type_dataframe(session): df.schema.fields[9].datatype.start_field == 2 and df.schema.fields[9].datatype.end_field == 3 ) # MINUTE TO SECOND - - -@pytest.mark.skipif( - "config.getoption('local_testing_mode', default=False)", - reason="FEAT: session.sql is not supported in local testing", -) -@pytest.mark.skipif( - IS_IN_STORED_PROC, - reason="FEAT: session.sql is not supported in stored procedure", -) -def test_day_time_interval_type_dataframe(session): - session.sql("alter session set feature_interval_types=enabled;").collect() - session.sql("alter session set enable_interval_subtypes=true;").collect() - - schema = StructType( - [ - StructField("ID", LongType(), nullable=False), - StructField("DT_INTERVAL", DayTimeIntervalType(), nullable=False), - ] - ) - - df = session.sql( - """ - SELECT 1 as id, INTERVAL '1 12:30:45' DAY TO SECOND as dt_interval - UNION ALL - SELECT 2 as id, INTERVAL '2 08:15:30' DAY TO SECOND as dt_interval - UNION ALL - SELECT 3 as id, INTERVAL '0 06:45:15' DAY TO SECOND as dt_interval - """ - ) - - result = df.collect() - assert len(result) == 3 - assert df.schema == schema - - assert result[0]["DT_INTERVAL"] == timedelta(days=1, seconds=45045) - assert result[1]["DT_INTERVAL"] == timedelta(days=2, seconds=29730) - assert result[2]["DT_INTERVAL"] == timedelta(seconds=24315) - - df = session.sql( - """ - SELECT 1 as id, INTERVAL '-1 12:30:45' DAY TO SECOND as dt_interval - UNION ALL - SELECT 2 as id, INTERVAL '-2 08:15:30' DAY TO SECOND as dt_interval - UNION ALL - SELECT 3 as id, INTERVAL '-0 06:45:15' DAY TO SECOND as dt_interval - """ - ) - - result = df.collect() - assert len(result) == 3 - assert df.schema == schema - - assert result[0]["DT_INTERVAL"] == timedelta(days=-2, seconds=41355) - assert result[1]["DT_INTERVAL"] == timedelta(days=-3, seconds=56670) - assert result[2]["DT_INTERVAL"] == timedelta(days=-1, seconds=62085) - - test_cases = [ - [ - "2", - "1", - "30", - "45", - "1 12", - "1 12:30", - "1 12:30:45", - "12:30", - "12:30:45", - "30:45", - ], - [ - "-3", - "-2", - "-45", - "-30", - "-2 06", - "-2 06:15", - "-2 06:15:30", - "-18:45", - "-18:45:15", - "-90:30", - ], - ] - schema = StructType( - [ - StructField("interval_col_day_0_0", DayTimeIntervalType(0, 0)), - StructField("interval_col_hour_1_1", DayTimeIntervalType(1, 1)), - StructField("interval_col_minute_2_2", DayTimeIntervalType(2, 2)), - StructField("interval_col_second_3_3", DayTimeIntervalType(3, 3)), - StructField("interval_col_day_hour_0_1", DayTimeIntervalType(0, 1)), - StructField("interval_col_day_minute_0_2", DayTimeIntervalType(0, 2)), - StructField("interval_col_day_second_0_3", DayTimeIntervalType(0, 3)), - StructField("interval_col_hour_minute_1_2", DayTimeIntervalType(1, 2)), - StructField("interval_col_hour_second_1_3", DayTimeIntervalType(1, 3)), - StructField("interval_col_minute_second_2_3", DayTimeIntervalType(2, 3)), - ] - ) - df = session.create_dataframe(test_cases, schema=schema) - - test_result = df.collect() - - assert len(test_result) == 2 - - assert test_result[0][0] == timedelta(days=2) - assert test_result[0][1] == timedelta(hours=1) - assert test_result[0][2] == timedelta(minutes=30) - assert test_result[0][3] == timedelta(seconds=45) - assert test_result[0][4] == timedelta(days=1, hours=12) - assert test_result[0][5] == timedelta(days=1, hours=12, minutes=30) - assert test_result[0][6] == timedelta(days=1, hours=12, minutes=30, seconds=45) - assert test_result[0][7] == timedelta(seconds=45000) - assert test_result[0][8] == timedelta(seconds=45045) - assert test_result[0][9] == timedelta(seconds=1845) - - assert test_result[1][0] == timedelta(days=-3) - assert test_result[1][1] == timedelta(hours=-2) - assert test_result[1][2] == timedelta(minutes=-45) - assert test_result[1][3] == timedelta(seconds=-30) - assert test_result[1][4] == timedelta(days=-2, hours=-6) - assert test_result[1][5] == timedelta(days=-2, hours=-6, minutes=-15) - assert test_result[1][6] == timedelta(days=-2, hours=-6, minutes=-15, seconds=-30) - assert test_result[1][7] == timedelta(days=-1, seconds=18900) - assert test_result[1][8] == timedelta(days=-1, seconds=18885) - assert test_result[1][9] == timedelta(days=-1, seconds=80970) - - assert isinstance(df.schema.fields[0].datatype, DayTimeIntervalType) - assert ( - df.schema.fields[0].datatype.start_field == 0 - and df.schema.fields[0].datatype.end_field == 0 - ) # DAY - assert ( - df.schema.fields[1].datatype.start_field == 1 - and df.schema.fields[1].datatype.end_field == 1 - ) # HOUR - assert ( - df.schema.fields[2].datatype.start_field == 2 - and df.schema.fields[2].datatype.end_field == 2 - ) # MINUTE - assert ( - df.schema.fields[3].datatype.start_field == 3 - and df.schema.fields[3].datatype.end_field == 3 - ) # SECOND - assert ( - df.schema.fields[4].datatype.start_field == 0 - and df.schema.fields[4].datatype.end_field == 1 - ) # DAY TO HOUR - assert ( - df.schema.fields[5].datatype.start_field == 0 - and df.schema.fields[5].datatype.end_field == 2 - ) # DAY TO MINUTE - assert ( - df.schema.fields[6].datatype.start_field == 0 - and df.schema.fields[6].datatype.end_field == 3 - ) # DAY TO SECOND - assert ( - df.schema.fields[7].datatype.start_field == 1 - and df.schema.fields[7].datatype.end_field == 2 - ) # HOUR TO MINUTE - assert ( - df.schema.fields[8].datatype.start_field == 1 - and df.schema.fields[8].datatype.end_field == 3 - ) # HOUR TO SECOND - assert ( - df.schema.fields[9].datatype.start_field == 2 - and df.schema.fields[9].datatype.end_field == 3 - ) # MINUTE TO SECOND - - session.sql("alter session set feature_interval_types=disabled;").collect() - session.sql("alter session set enable_interval_subtypes=false;").collect() diff --git a/tests/integ/scala/test_function_suite.py b/tests/integ/scala/test_function_suite.py index 26bc411b80..040683fea8 100644 --- a/tests/integ/scala/test_function_suite.py +++ b/tests/integ/scala/test_function_suite.py @@ -5736,8 +5736,6 @@ def test_interval_year_month_from_parts(session): IS_IN_STORED_PROC, reason="Alter Session not supported in stored procedure." ) def test_interval_day_time_from_parts(session): - session.sql("alter session set feature_interval_types=enabled;").collect() - df = session.create_dataframe( [ (0, 0, 0, 0.0), @@ -5924,5 +5922,3 @@ def test_interval_day_time_from_parts(session): assert result_nulls[2]["INTERVAL_RESULT"] is None assert result_nulls[3]["INTERVAL_RESULT"] is None assert result_nulls[4]["INTERVAL_RESULT"] is None - - session.sql("alter session set feature_interval_types=disabled;").collect() diff --git a/tests/integ/test_dataframe.py b/tests/integ/test_dataframe.py index ec63641972..22f963f121 100644 --- a/tests/integ/test_dataframe.py +++ b/tests/integ/test_dataframe.py @@ -1905,125 +1905,6 @@ def test_create_dataframe_with_day_time_interval_type(session): assert interval_sql_result[0][2] == datetime.timedelta(days=-2, seconds=63870) -@pytest.mark.skipif( - "config.getoption('local_testing_mode', default=False)", - reason="FEAT: session.sql is not supported in local testing", -) -@pytest.mark.skipif( - IS_IN_STORED_PROC, - reason="FEAT: session.sql is not supported in stored procedure", -) -def test_create_dataframe_with_day_time_interval_type(session): - session.sql("alter session set feature_interval_types=enabled;").collect() - - schema = StructType([StructField("interval_col", DayTimeIntervalType())]) - data = [["1 12:30:45"], ["-2 08:15:30"]] - df = session.create_dataframe(data, schema=schema) - - assert isinstance(df.schema.fields[0].datatype, DayTimeIntervalType) - result = df.collect() - assert len(result) == 2 - assert result[0][0] == datetime.timedelta(days=1, seconds=45045) - assert result[1][0] == datetime.timedelta(days=-3, seconds=56670) - - test_schema = StructType( - [ - StructField("test_date", DateType()), - StructField("interval_col", DayTimeIntervalType()), - ] - ) - - test_data = [["2023-01-15", "1 06:30:00"], ["2022-06-30", "-0 03:15:45"]] - - test_df = session.create_dataframe(test_data, schema=test_schema) - - addition_result = test_df.select( - (test_df.test_date + test_df.interval_col).alias("date_plus_interval") - ).collect() - - subtraction_result = test_df.select( - (test_df.test_date - test_df.interval_col).alias("date_minus_interval") - ).collect() - - assert len(addition_result) == 2 - assert addition_result[0][0] == datetime.datetime(2023, 1, 16, 6, 30) - assert addition_result[1][0] == datetime.datetime(2022, 6, 29, 20, 44, 15) - - assert len(subtraction_result) == 2 - assert subtraction_result[0][0] == datetime.datetime(2023, 1, 13, 17, 30) - assert subtraction_result[1][0] == datetime.datetime(2022, 6, 30, 3, 15, 45) - - interval_arithmetic_df = session.sql( - """ - SELECT - DATE '2023-01-01' + INTERVAL '1 12:00:00' DAY TO SECOND as addition_result, - DATE '2023-12-31' - INTERVAL '0 06:30:15' DAY TO SECOND as subtraction_result - """ - ) - - arithmetic_result = interval_arithmetic_df.collect() - assert len(arithmetic_result) == 1 - assert arithmetic_result[0][0] == datetime.datetime(2023, 1, 2, 12, 0) - assert arithmetic_result[0][1] == datetime.datetime(2023, 12, 30, 17, 29, 45) - - interval_schema = StructType( - [ - StructField("interval1", DayTimeIntervalType()), - StructField("interval2", DayTimeIntervalType()), - ] - ) - - interval_data = [ - ["2 12:30:45", "1 06:15:30"], - ["1 00:00:00", "-0 12:30:00"], - ["-1 08:45:15", "2 04:30:45"], - ] - - interval_df = session.create_dataframe(interval_data, schema=interval_schema) - - interval_addition_result = interval_df.select( - (interval_df.interval1 + interval_df.interval2).alias("interval_sum") - ).collect() - - interval_subtraction_result = interval_df.select( - (interval_df.interval1 - interval_df.interval2).alias("interval_diff") - ).collect() - - assert len(interval_addition_result) == 3 - assert interval_addition_result[0][0] == datetime.timedelta(days=3, seconds=67575) - assert interval_addition_result[1][0] == datetime.timedelta(seconds=41400) - assert interval_addition_result[2][0] == datetime.timedelta(seconds=71130) - - assert len(interval_subtraction_result) == 3 - assert interval_subtraction_result[0][0] == datetime.timedelta( - days=1, seconds=22515 - ) - assert interval_subtraction_result[1][0] == datetime.timedelta( - days=1, seconds=45000 - ) - assert interval_subtraction_result[2][0] == datetime.timedelta( - days=-4, seconds=38640 - ) - - interval_sql_df = session.sql( - """ - SELECT - INTERVAL '2 12:30:45' DAY TO SECOND + INTERVAL '1 06:15:30' DAY TO SECOND as interval_addition, - INTERVAL '3 00:00:00' DAY TO SECOND - INTERVAL '1 12:30:00' DAY TO SECOND as interval_subtraction, - INTERVAL '1 06:30:00' DAY TO SECOND - INTERVAL '2 12:45:30' DAY TO SECOND as interval_subtraction_2, - """ - ) - - interval_sql_result = interval_sql_df.collect() - assert len(interval_sql_result) == 1 - assert len(interval_sql_result[0]) == 3 - assert interval_sql_result[0][0] == datetime.timedelta(days=3, seconds=67575) - assert interval_sql_result[0][1] == datetime.timedelta(days=1, seconds=41400) - assert interval_sql_result[0][2] == datetime.timedelta(days=-2, seconds=63870) - - session.sql("alter session set feature_interval_types=disabled;").collect() - - def test_create_dataframe_with_semi_structured_data_types(session): data = [ [ From ac676084f8e6043b4e794262bb3911c5495388ff Mon Sep 17 00:00:00 2001 From: Felix He Date: Wed, 3 Sep 2025 18:04:19 -0700 Subject: [PATCH 30/39] SNOW-2296280: added emit ast handling --- src/snowflake/snowpark/functions.py | 83 ++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index c2998ceada..541a8005fb 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11083,53 +11083,78 @@ def interval_day_time_from_parts( """ + # Handle AST emission + ast = None + if _emit_ast: + # Create AST for this custom function using build_function_expr + # Filter out None parameters to only include provided arguments + args = [] + if days is not None: + args.append(days) + if hours is not None: + args.append(hours) + if mins is not None: + args.append(mins) + if secs is not None: + args.append(secs) + ast = build_function_expr("interval_day_time_from_parts", args) + days_col = ( - lit(0) if days is None else _to_col_if_str(days, "interval_day_time_from_parts") + lit(0, _emit_ast=False) + if days is None + else _to_col_if_str(days, "interval_day_time_from_parts") ) hours_col = ( - lit(0) + lit(0, _emit_ast=False) if hours is None else _to_col_if_str(hours, "interval_day_time_from_parts") ) mins_col = ( - lit(0) if mins is None else _to_col_if_str(mins, "interval_day_time_from_parts") + lit(0, _emit_ast=False) + if mins is None + else _to_col_if_str(mins, "interval_day_time_from_parts") ) secs_col = ( - lit(0) if secs is None else _to_col_if_str(secs, "interval_day_time_from_parts") + lit(0, _emit_ast=False) + if secs is None + else _to_col_if_str(secs, "interval_day_time_from_parts") ) total_seconds = ( - days_col * lit(86400) + hours_col * lit(3600) + mins_col * lit(60) + secs_col + days_col * lit(86400, _emit_ast=False) + + hours_col * lit(3600, _emit_ast=False) + + mins_col * lit(60, _emit_ast=False) + + secs_col ) - is_negative = total_seconds < lit(0) + is_negative = total_seconds < lit(0, _emit_ast=False) abs_total_seconds = abs(total_seconds) - days_part = cast(floor(abs_total_seconds / lit(86400)), "int") - remaining_after_days = abs_total_seconds % lit(86400) + days_part = cast(floor(abs_total_seconds / lit(86400, _emit_ast=False)), "int") + remaining_after_days = abs_total_seconds % lit(86400, _emit_ast=False) - hours_part = cast(floor(remaining_after_days / lit(3600)), "int") - remaining_after_hours = remaining_after_days % lit(3600) + hours_part = cast(floor(remaining_after_days / lit(3600, _emit_ast=False)), "int") + remaining_after_hours = remaining_after_days % lit(3600, _emit_ast=False) - mins_part = cast(floor(remaining_after_hours / lit(60)), "int") - secs_part = remaining_after_hours % lit(60) + mins_part = cast(floor(remaining_after_hours / lit(60, _emit_ast=False)), "int") + secs_part = remaining_after_hours % lit(60, _emit_ast=False) hours_str = iff( - hours_part < lit(10), - concat(lit("0"), cast(hours_part, "str")), + hours_part < lit(10, _emit_ast=False), + concat(lit("0", _emit_ast=False), cast(hours_part, "str")), cast(hours_part, "str"), ) mins_str = iff( - mins_part < lit(10), - concat(lit("0"), cast(mins_part, "str")), + mins_part < lit(10, _emit_ast=False), + concat(lit("0", _emit_ast=False), cast(mins_part, "str")), cast(mins_part, "str"), ) secs_int = cast(floor(secs_part), "int") secs_str = iff( - secs_int < lit(10), - concat(lit("0"), cast(secs_int, "str")), + secs_int < lit(10, _emit_ast=False), + concat(lit("0", _emit_ast=False), cast(secs_int, "str")), cast(secs_int, "str"), ) @@ -11139,23 +11164,27 @@ def interval_day_time_from_parts( fraction_str = iff( has_fraction, concat( - lit("."), - lpad(cast(round(fractional_part * lit(1000)), "str"), 3, lit("0")), + lit(".", _emit_ast=False), + lpad( + cast(round(fractional_part * lit(1000, _emit_ast=False)), "str"), + 3, + lit("0", _emit_ast=False), + ), ), - lit(""), + lit("", _emit_ast=False), ) secs_formatted = concat(secs_str, fraction_str) - sign_prefix = iff(is_negative, lit("-"), lit("")) + sign_prefix = iff(is_negative, lit("-", _emit_ast=False), lit("", _emit_ast=False)) interval_value = concat( sign_prefix, cast(days_part, "str"), - lit(" "), + lit(" ", _emit_ast=False), hours_str, - lit(":"), + lit(":", _emit_ast=False), mins_str, - lit(":"), + lit(":", _emit_ast=False), secs_formatted, ) @@ -11167,7 +11196,9 @@ def get_col_name(col): alias_name = f"interval_day_time_from_parts({get_col_name(days_col)}, {get_col_name(hours_col)}, {get_col_name(mins_col)}, {get_col_name(secs_col)})" - return cast(interval_value, "INTERVAL DAY TO SECOND").alias(alias_name) + res = cast(interval_value, "INTERVAL DAY TO SECOND").alias(alias_name) + res._ast = ast + return res @publicapi From c8a073f431ee04c42c0e6cc9ccaa1c1d65968031 Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 11 Sep 2025 11:43:32 -0700 Subject: [PATCH 31/39] SNOW-2296280: update changelog --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0fbc1ad7..a0843fabc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,8 +114,6 @@ - `current_transaction` - `getbit` -- Added a new datatype `YearMonthIntervalType` that allows users to create intervals for datetime operations. - #### Bug Fixes - Fixed the repr of TimestampType to match the actual subtype it represents. @@ -131,7 +129,6 @@ ### Snowpark pandas API Updates #### New Features - - Completed support for `pd.read_snowflake()`, `pd.to_iceberg()`, `pd.to_pandas()`, `pd.to_snowpark()`, `pd.to_snowflake()`, `DataFrame.to_iceberg()`, `DataFrame.to_pandas()`, `DataFrame.to_snowpark()`, @@ -142,7 +139,6 @@ - Added support for `Index.get_level_values()`. #### Improvements - - Set the default transfer limit in hybrid execution for data leaving Snowflake to 100k, which can be overridden with the SnowflakePandasTransferThreshold environment variable. This configuration is appropriate for scenarios with two available engines, "Pandas" and "Snowflake" on relational workloads. - Improve import error message by adding `--upgrade` to `pip install "snowflake-snowpark-python[modin]"` in the error message. - Reduce the telemetry messages from the modin client by pre-aggregating into 5 second windows and only keeping a narrow band of metrics which are useful for tracking hybrid execution and native pandas performance. From 6cc144a637afbfeb22859f8d0a7a0ad19c0b8ab3 Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 11 Sep 2025 11:51:20 -0700 Subject: [PATCH 32/39] SNOW-2296280: remove redundant emit asts --- src/snowflake/snowpark/functions.py | 62 +++++++++++++---------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 541a8005fb..a610435666 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11073,6 +11073,7 @@ def interval_day_time_from_parts( >>> from snowflake.snowpark.functions import interval_day_time_from_parts >>> + >>> _ = session.sql("ALTER SESSION SET FEATURE_INTERVAL_TYPES=ENABLED;").collect() >>> df = session.create_dataframe([[1, 12, 30, 01.001001]], ['day', 'hour', 'min', 'sec']) >>> df.select(interval_day_time_from_parts(col("day"), col("hour"), col("min"), col("sec")).alias("interval")).show() ------------------ @@ -11100,61 +11101,52 @@ def interval_day_time_from_parts( ast = build_function_expr("interval_day_time_from_parts", args) days_col = ( - lit(0, _emit_ast=False) - if days is None - else _to_col_if_str(days, "interval_day_time_from_parts") + lit(0) if days is None else _to_col_if_str(days, "interval_day_time_from_parts") ) hours_col = ( - lit(0, _emit_ast=False) + lit(0) if hours is None else _to_col_if_str(hours, "interval_day_time_from_parts") ) mins_col = ( - lit(0, _emit_ast=False) - if mins is None - else _to_col_if_str(mins, "interval_day_time_from_parts") + lit(0) if mins is None else _to_col_if_str(mins, "interval_day_time_from_parts") ) secs_col = ( - lit(0, _emit_ast=False) - if secs is None - else _to_col_if_str(secs, "interval_day_time_from_parts") + lit(0) if secs is None else _to_col_if_str(secs, "interval_day_time_from_parts") ) total_seconds = ( - days_col * lit(86400, _emit_ast=False) - + hours_col * lit(3600, _emit_ast=False) - + mins_col * lit(60, _emit_ast=False) - + secs_col + days_col * lit(86400) + hours_col * lit(3600) + mins_col * lit(60) + secs_col ) - is_negative = total_seconds < lit(0, _emit_ast=False) + is_negative = total_seconds < lit(0) abs_total_seconds = abs(total_seconds) - days_part = cast(floor(abs_total_seconds / lit(86400, _emit_ast=False)), "int") - remaining_after_days = abs_total_seconds % lit(86400, _emit_ast=False) + days_part = cast(floor(abs_total_seconds / lit(86400)), "int") + remaining_after_days = abs_total_seconds % lit(86400) - hours_part = cast(floor(remaining_after_days / lit(3600, _emit_ast=False)), "int") - remaining_after_hours = remaining_after_days % lit(3600, _emit_ast=False) + hours_part = cast(floor(remaining_after_days / lit(3600)), "int") + remaining_after_hours = remaining_after_days % lit(3600) - mins_part = cast(floor(remaining_after_hours / lit(60, _emit_ast=False)), "int") - secs_part = remaining_after_hours % lit(60, _emit_ast=False) + mins_part = cast(floor(remaining_after_hours / lit(60)), "int") + secs_part = remaining_after_hours % lit(60) hours_str = iff( - hours_part < lit(10, _emit_ast=False), - concat(lit("0", _emit_ast=False), cast(hours_part, "str")), + hours_part < lit(10), + concat(lit("0"), cast(hours_part, "str")), cast(hours_part, "str"), ) mins_str = iff( - mins_part < lit(10, _emit_ast=False), - concat(lit("0", _emit_ast=False), cast(mins_part, "str")), + mins_part < lit(10), + concat(lit("0"), cast(mins_part, "str")), cast(mins_part, "str"), ) secs_int = cast(floor(secs_part), "int") secs_str = iff( - secs_int < lit(10, _emit_ast=False), - concat(lit("0", _emit_ast=False), cast(secs_int, "str")), + secs_int < lit(10), + concat(lit("0"), cast(secs_int, "str")), cast(secs_int, "str"), ) @@ -11164,27 +11156,27 @@ def interval_day_time_from_parts( fraction_str = iff( has_fraction, concat( - lit(".", _emit_ast=False), + lit("."), lpad( - cast(round(fractional_part * lit(1000, _emit_ast=False)), "str"), + cast(round(fractional_part * lit(1000)), "str"), 3, - lit("0", _emit_ast=False), + lit("0"), ), ), - lit("", _emit_ast=False), + lit(""), ) secs_formatted = concat(secs_str, fraction_str) - sign_prefix = iff(is_negative, lit("-", _emit_ast=False), lit("", _emit_ast=False)) + sign_prefix = iff(is_negative, lit("-"), lit("")) interval_value = concat( sign_prefix, cast(days_part, "str"), - lit(" ", _emit_ast=False), + lit(" "), hours_str, - lit(":", _emit_ast=False), + lit(":"), mins_str, - lit(":", _emit_ast=False), + lit(":"), secs_formatted, ) From 02e801b642c705959246024bad9009515a81f7c1 Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 11 Sep 2025 14:29:13 -0700 Subject: [PATCH 33/39] SNOW-2296280: added ast test --- tests/ast/data/functions2.test | 78 ++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/ast/data/functions2.test b/tests/ast/data/functions2.test index 77152ae03d..c4bd303ec9 100644 --- a/tests/ast/data/functions2.test +++ b/tests/ast/data/functions2.test @@ -450,6 +450,8 @@ df351 = df.select(function("avg")("B")) df352 = df.select(interval_year_month_from_parts("A", "B")) +df353 = df.select(interval_day_time_from_parts("A", "B")) + ## EXPECTED UNPARSER OUTPUT df = session.table("table1") @@ -900,6 +902,8 @@ df351 = df.select(call_function("avg", "B")) df352 = df.select(interval_year_month_from_parts("A", "B")) +df353 = df.select(interval_day_time_from_parts("A", "B")) + ## EXPECTED ENCODED AST interned_value_table { @@ -29253,6 +29257,80 @@ body { uid: 224 } } +body { + bind { + expr { + dataframe_select { + cols { + args { + apply_expr { + fn { + builtin_fn { + name { + name { + name_flat { + name: "interval_day_time_from_parts" + } + } + } + } + } + pos_args { + string_val { + src { + end_column: 64 + end_line: 475 + file: 2 + start_column: 26 + start_line: 475 + } + v: "A" + } + } + pos_args { + string_val { + src { + end_column: 64 + end_line: 475 + file: 2 + start_column: 26 + start_line: 475 + } + v: "B" + } + } + src { + end_column: 64 + end_line: 475 + file: 2 + start_column: 26 + start_line: 475 + } + } + } + variadic: true + } + df { + dataframe_ref { + id: 1 + } + } + src { + end_column: 65 + end_line: 475 + file: 2 + start_column: 16 + start_line: 475 + } + } + } + first_request_id: "\003U\"\366q\366P\346\260\261?\234\303\254\316\353" + symbol { + value: "df353" + } + uid: 225 + } +} client_ast_version: 1 client_language { python_language { From 5739af1938a51f47b04b34964f60b40037d65587 Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 11 Sep 2025 15:16:09 -0700 Subject: [PATCH 34/39] SNOW-2296280: update --- docs/source/snowpark/functions.rst | 2 +- src/snowflake/snowpark/functions.py | 23 ++++++++---------- tests/ast/data/functions2.test | 36 ++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/docs/source/snowpark/functions.rst b/docs/source/snowpark/functions.rst index dc0c502212..21da5f49db 100644 --- a/docs/source/snowpark/functions.rst +++ b/docs/source/snowpark/functions.rst @@ -235,6 +235,7 @@ Functions initcap insert instr + interval_day_time_from_parts interval_year_month_from_parts invoker_role invoker_share @@ -283,7 +284,6 @@ Functions lpad ltrim make_interval - interval_day_time_from_parts map_cat map_concat map_contains_key diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index a610435666..b20844a5d0 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11000,11 +11000,10 @@ def interval_year_month_from_parts( """ ast = None if _emit_ast: + # Always include both parameters to match the actual function execution args = [] - if years is not None: - args.append(years) - if months is not None: - args.append(months) + args.append(years if years is not None else lit(0)) + args.append(months if months is not None else lit(0)) ast = build_function_expr("interval_year_month_from_parts", args) years_col = ( @@ -11088,16 +11087,12 @@ def interval_day_time_from_parts( ast = None if _emit_ast: # Create AST for this custom function using build_function_expr - # Filter out None parameters to only include provided arguments + # Always include all 4 parameters to match the actual function execution args = [] - if days is not None: - args.append(days) - if hours is not None: - args.append(hours) - if mins is not None: - args.append(mins) - if secs is not None: - args.append(secs) + args.append(days if days is not None else lit(0)) + args.append(hours if hours is not None else lit(0)) + args.append(mins if mins is not None else lit(0)) + args.append(secs if secs is not None else lit(0)) ast = build_function_expr("interval_day_time_from_parts", args) days_col = ( @@ -11158,7 +11153,7 @@ def interval_day_time_from_parts( concat( lit("."), lpad( - cast(round(fractional_part * lit(1000)), "str"), + cast(floor(fractional_part * lit(1000) + lit(0.5)), "str"), 3, lit("0"), ), diff --git a/tests/ast/data/functions2.test b/tests/ast/data/functions2.test index c4bd303ec9..81ad4eeca2 100644 --- a/tests/ast/data/functions2.test +++ b/tests/ast/data/functions2.test @@ -450,7 +450,7 @@ df351 = df.select(function("avg")("B")) df352 = df.select(interval_year_month_from_parts("A", "B")) -df353 = df.select(interval_day_time_from_parts("A", "B")) +df353 = df.select(interval_day_time_from_parts("A", "B", "C", "D")) ## EXPECTED UNPARSER OUTPUT @@ -902,7 +902,7 @@ df351 = df.select(call_function("avg", "B")) df352 = df.select(interval_year_month_from_parts("A", "B")) -df353 = df.select(interval_day_time_from_parts("A", "B")) +df353 = df.select(interval_day_time_from_parts("A", "B", "C", "D")) ## EXPECTED ENCODED AST @@ -29278,7 +29278,7 @@ body { pos_args { string_val { src { - end_column: 64 + end_column: 74 end_line: 475 file: 2 start_column: 26 @@ -29290,7 +29290,7 @@ body { pos_args { string_val { src { - end_column: 64 + end_column: 74 end_line: 475 file: 2 start_column: 26 @@ -29299,8 +29299,32 @@ body { v: "B" } } + pos_args { + string_val { + src { + end_column: 74 + end_line: 475 + file: 2 + start_column: 26 + start_line: 475 + } + v: "C" + } + } + pos_args { + string_val { + src { + end_column: 74 + end_line: 475 + file: 2 + start_column: 26 + start_line: 475 + } + v: "D" + } + } src { - end_column: 64 + end_column: 74 end_line: 475 file: 2 start_column: 26 @@ -29316,7 +29340,7 @@ body { } } src { - end_column: 65 + end_column: 75 end_line: 475 file: 2 start_column: 16 From 6ed919fbb0645c1d54c9b41d927cdd512ea3afc4 Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 11 Sep 2025 15:23:22 -0700 Subject: [PATCH 35/39] SNOW-2296280: updated again for graphite comment --- src/snowflake/snowpark/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index b20844a5d0..60381e80da 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11153,7 +11153,7 @@ def interval_day_time_from_parts( concat( lit("."), lpad( - cast(floor(fractional_part * lit(1000) + lit(0.5)), "str"), + cast(round(fractional_part * lit(1000), 0), "str"), 3, lit("0"), ), From 425443cbf95799f147235b3cb20994a0e538535f Mon Sep 17 00:00:00 2001 From: Felix He Date: Thu, 11 Sep 2025 15:26:41 -0700 Subject: [PATCH 36/39] SNOW-2296280: update dataframe --- tests/integ/scala/test_function_suite.py | 67 +++++++++++------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/tests/integ/scala/test_function_suite.py b/tests/integ/scala/test_function_suite.py index 040683fea8..f1bee49123 100644 --- a/tests/integ/scala/test_function_suite.py +++ b/tests/integ/scala/test_function_suite.py @@ -5736,24 +5736,37 @@ def test_interval_year_month_from_parts(session): IS_IN_STORED_PROC, reason="Alter Session not supported in stored procedure." ) def test_interval_day_time_from_parts(session): + test_cases = [ + (0, 0, 0, 0.0, timedelta(0)), + (1, 0, 0, 0.0, timedelta(days=1)), + (0, 1, 0, 0.0, timedelta(hours=1)), + (0, 0, 1, 0.0, timedelta(minutes=1)), + (0, 0, 0, 1.0, timedelta(seconds=1.0)), + (1, 2, 3, 4.5, timedelta(days=1, hours=2, minutes=3, seconds=4.5)), + (5, 10, 30, 45.123, timedelta(days=5, hours=10, minutes=30, seconds=45.123)), + (0, 25, 90, 120.999, timedelta(hours=25, minutes=90, seconds=120.999)), + (-1, 0, 0, 0.0, timedelta(days=-1)), + (0, -1, 0, 0.0, timedelta(hours=-1)), + (0, 0, -1, 0.0, timedelta(minutes=-1)), + (0, 0, 0, -1.0, timedelta(seconds=-1.0)), + (-1, -2, -3, -4.5, timedelta(days=-1, hours=-2, minutes=-3, seconds=-4.5)), + (2, -1, 30, -15.0, timedelta(days=2, hours=-1, minutes=30, seconds=-15.0)), + ( + 365, + 24, + 60, + 3600.0, + timedelta(days=365, hours=24, minutes=60, seconds=3600.0), + ), + ] + + input_data = [ + (days, hours, mins, secs) for days, hours, mins, secs, _ in test_cases + ] + expected_values = [expected for _, _, _, _, expected in test_cases] + df = session.create_dataframe( - [ - (0, 0, 0, 0.0), - (1, 0, 0, 0.0), - (0, 1, 0, 0.0), - (0, 0, 1, 0.0), - (0, 0, 0, 1.0), - (1, 2, 3, 4.5), - (5, 10, 30, 45.123), - (0, 25, 90, 120.999), - (-1, 0, 0, 0.0), - (0, -1, 0, 0.0), - (0, 0, -1, 0.0), - (0, 0, 0, -1.0), - (-1, -2, -3, -4.5), - (2, -1, 30, -15.0), - (365, 24, 60, 3600.0), - ], + input_data, schema=["days", "hours", "mins", "secs"], ) @@ -5763,25 +5776,7 @@ def test_interval_day_time_from_parts(session): ).alias("interval_result"), ).collect() - expected_values = [ - timedelta(0), - timedelta(days=1), - timedelta(hours=1), - timedelta(minutes=1), - timedelta(seconds=1.0), - timedelta(days=1, hours=2, minutes=3, seconds=4.5), - timedelta(days=5, hours=10, minutes=30, seconds=45.123), - timedelta(hours=25, minutes=90, seconds=120.999), - timedelta(days=-1), - timedelta(hours=-1), - timedelta(minutes=-1), - timedelta(seconds=-1.0), - timedelta(days=-1, hours=-2, minutes=-3, seconds=-4.5), - timedelta(days=2, hours=-1, minutes=30, seconds=-15.0), - timedelta(days=365, hours=24, minutes=60, seconds=3600.0), - ] - - assert len(result) == 15 + assert len(result) == len(expected_values) for i, expected in enumerate(expected_values): assert result[i]["INTERVAL_RESULT"] == expected From f24d9fe24f2595b488f1bf35f9644743d3297e47 Mon Sep 17 00:00:00 2001 From: Felix He Date: Mon, 15 Sep 2025 13:26:37 -0700 Subject: [PATCH 37/39] SNOW-2296280: update show print --- src/snowflake/snowpark/functions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 60381e80da..4e239a49eb 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11075,11 +11075,11 @@ def interval_day_time_from_parts( >>> _ = session.sql("ALTER SESSION SET FEATURE_INTERVAL_TYPES=ENABLED;").collect() >>> df = session.create_dataframe([[1, 12, 30, 01.001001]], ['day', 'hour', 'min', 'sec']) >>> df.select(interval_day_time_from_parts(col("day"), col("hour"), col("min"), col("sec")).alias("interval")).show() - ------------------ - |"INTERVAL" | - ------------------ - |1 12:30:01.001 | - ------------------ + -------------------------- + |"INTERVAL" | + -------------------------- + |1 day, 12:30:01.001000 | + -------------------------- """ From 4e6f82ecce1b14c98c0636946e6118c4b7927fd6 Mon Sep 17 00:00:00 2001 From: Felix He Date: Mon, 15 Sep 2025 13:32:38 -0700 Subject: [PATCH 38/39] SNOW-2296280: accept graphite change --- src/snowflake/snowpark/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 4e239a49eb..b5a0f33ba4 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -11145,7 +11145,7 @@ def interval_day_time_from_parts( cast(secs_int, "str"), ) - has_fraction = secs_part != cast(secs_int, "double") + has_fraction = abs(secs_part - cast(secs_int, "double")) > 1e-10 fractional_part = secs_part - cast(secs_int, "double") fraction_str = iff( From 062e60200ad8e58e16bc6996cc32fd5770f42a52 Mon Sep 17 00:00:00 2001 From: Felix He Date: Mon, 15 Sep 2025 13:51:42 -0700 Subject: [PATCH 39/39] SNOW-2296280: remove extra parameter --- tests/integ/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integ/conftest.py b/tests/integ/conftest.py index 6f1a088b62..2ff90e2494 100644 --- a/tests/integ/conftest.py +++ b/tests/integ/conftest.py @@ -274,7 +274,7 @@ def session( .config("local_testing", local_testing_mode) .config( "session_parameters", - {"feature_interval_types": "ENABLED", "enable_interval_subtypes": "true"}, + {"feature_interval_types": "ENABLED"}, ) .create() )