From cbfe716e83bcfc51ea8f5a49594c118cfbe2dc5b Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Wed, 22 Apr 2026 15:46:32 -0700 Subject: [PATCH 01/14] fix: use None check for packages in non-anaconda artifact repositories In resolve_imports_and_packages, non-anaconda paths branched on the truthiness of `packages`, which treated an empty list the same as None. This meant passing `packages=[]` to opt out of session-level package resolution silently skipped resolution entirely, while the anaconda path consistently differentiates between None (use session defaults) and [] (use no packages). Switch to an explicit `is None` / `is not None` check so non-anaconda behavior matches the anaconda path. --- src/snowflake/snowpark/_internal/udf_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snowflake/snowpark/_internal/udf_utils.py b/src/snowflake/snowpark/_internal/udf_utils.py index 7f92e9fc63..d38d8bbe8e 100644 --- a/src/snowflake/snowpark/_internal/udf_utils.py +++ b/src/snowflake/snowpark/_internal/udf_utils.py @@ -1247,7 +1247,7 @@ def resolve_imports_and_packages( if artifact_repository != _ANACONDA_SHARED_REPOSITORY: # Non-conda artifact repository - skip conda-based package resolution resolved_packages = [] - if not packages and session: + if packages is None and session: resolved_packages = list( session._resolve_packages( [], @@ -1256,7 +1256,7 @@ def resolve_imports_and_packages( include_pandas=is_pandas_udf, ) ) - elif packages: + elif packages is not None: if not all(isinstance(package, str) for package in packages): raise TypeError( "Non-conda artifact repository requires that all packages be passed as str." From 283061f8bfed095c99b2327f80feb22ef20c4ed7 Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Fri, 17 Apr 2026 11:04:14 -0700 Subject: [PATCH 02/14] dynamic dbapi package resolution --- .../data_source/drivers/base_driver.py | 10 ++- .../snowpark/_internal/data_source/utils.py | 69 ++++++++++++++++++- src/snowflake/snowpark/context.py | 2 + 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py b/src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py index 15edc2b5a8..272924faa8 100644 --- a/src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py +++ b/src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py @@ -167,7 +167,13 @@ def udtf_ingestion( statement_params: Optional[Dict[str, str]] = None, _emit_ast: bool = True, ) -> "snowflake.snowpark.DataFrame": - from snowflake.snowpark._internal.data_source.utils import UDTF_PACKAGE_MAP + from snowflake.snowpark._internal.data_source.utils import ( + resolve_udtf_packages, + ) + + resolved_packages = packages or resolve_udtf_packages( + self.dbms_type, session._get_default_artifact_repository() + ) udtf_name = random_name_for_temp_object(TempObjectType.FUNCTION) with measure_time() as udtf_register_time: @@ -186,7 +192,7 @@ def udtf_ingestion( ] ), external_access_integrations=[external_access_integrations], - packages=packages or UDTF_PACKAGE_MAP.get(self.dbms_type), + packages=resolved_packages, imports=imports, artifact_repository=artifact_repository, statement_params=statement_params, diff --git a/src/snowflake/snowpark/_internal/data_source/utils.py b/src/snowflake/snowpark/_internal/data_source/utils.py index fee4a6c818..1390789df3 100644 --- a/src/snowflake/snowpark/_internal/data_source/utils.py +++ b/src/snowflake/snowpark/_internal/data_source/utils.py @@ -34,7 +34,11 @@ from snowflake.snowpark._internal.data_source import DataSourceReader from snowflake.snowpark._internal.type_utils import convert_sp_to_sf_type from snowflake.snowpark._internal.utils import get_temp_type_for_object -from snowflake.snowpark.exceptions import SnowparkDataframeReaderException +from snowflake.snowpark.context import _PYPI_SHARED_REPOSITORY +from snowflake.snowpark.exceptions import ( + SnowparkClientException, + SnowparkDataframeReaderException, +) from snowflake.snowpark.types import StructType from typing import TYPE_CHECKING @@ -98,7 +102,11 @@ class DRIVER_TYPE(str, Enum): DRIVER_TYPE.PYMYSQL: PymysqlDriver, } -UDTF_PACKAGE_MAP = { +# Default UDTF package list, suitable for Snowflake's Anaconda shared +# repository. The Snowflake Anaconda channel ships conda builds of these +# packages with the necessary native libraries (e.g., libpq for psycopg2, +# msodbcsql for pyodbc) bundled, so the source distribution names work. +_ANACONDA_UDTF_PACKAGE_MAP = { DBMS_TYPE.ORACLE_DB: ["oracledb>=2.0.0,<4.0.0", "snowflake-snowpark-python"], DBMS_TYPE.SQLITE_DB: ["snowflake-snowpark-python"], DBMS_TYPE.SQL_SERVER_DB: [ @@ -114,6 +122,63 @@ class DRIVER_TYPE(str, Enum): DBMS_TYPE.MYSQL_DB: ["pymysql>=1.0.0,<2.0.0", "snowflake-snowpark-python"], } +# UDTF package list when using the PyPI shared repository, which is the +# default on Python 3.14+. Differences from the Anaconda map: +# - Postgres uses ``psycopg2-binary`` because PyPI's ``psycopg2`` is sdist +# only and requires ``pg_config`` to compile, which is not available in +# the server-side install sandbox. +# - SQL Server has no PyPI-installable equivalent of ``msodbcsql`` (it is +# Microsoft's ODBC driver, distributed as a system package), so the +# UDTF path cannot work on PyPI today. +# - Databricks depends on ``databricks-sql-connector``, which transitively +# requires ``thrift<0.21``; no version of ``thrift`` publishes a Python +# 3.14 wheel on PyPI, and the server refuses to compile sdists. +_PYPI_UDTF_PACKAGE_MAP = { + DBMS_TYPE.ORACLE_DB: ["oracledb>=2.0.0,<4.0.0", "snowflake-snowpark-python"], + DBMS_TYPE.SQLITE_DB: ["snowflake-snowpark-python"], + DBMS_TYPE.POSTGRES_DB: [ + "psycopg2-binary>=2.0.0,<3.0.0", + "snowflake-snowpark-python", + ], + DBMS_TYPE.MYSQL_DB: ["pymysql>=1.0.0,<2.0.0", "snowflake-snowpark-python"], + # SQL_SERVER_DB and DATABRICKS_DB intentionally omitted - see + # resolve_udtf_packages for the user-facing error. +} + +# Backwards-compatible alias for callers (and external code) that imported +# the old map. New code should use :func:`resolve_udtf_packages`. +UDTF_PACKAGE_MAP = _ANACONDA_UDTF_PACKAGE_MAP + + +def resolve_udtf_packages( + dbms_type: "DBMS_TYPE", artifact_repository: Optional[str] +) -> List[str]: + """Return the default UDTF package list for ``dbms_type``. + + Picks the package list appropriate for ``artifact_repository``. When the + repository is the PyPI shared repository (the default on Python 3.14+), + some DBMSes have no working package set; this raises + :class:`SnowparkClientException` with guidance to switch repositories. + """ + if artifact_repository == _PYPI_SHARED_REPOSITORY: + packages = _PYPI_UDTF_PACKAGE_MAP.get(dbms_type) + if packages is None: + raise SnowparkClientException( + f"DataFrameReader.dbapi server-side UDTF ingestion for " + f"{dbms_type.value} is not supported on the PyPI artifact " + f"repository (the default on Python 3.14+). The required " + f"packages are not installable from PyPI in the server-side " + f"sandbox. To enable UDTF ingestion for this DBMS, run with " + f"the Anaconda artifact repository, e.g. by passing " + f"``udtf_configs={{'artifact_repository': " + f"'snowflake.snowpark.anaconda_shared_repository', ...}}`` " + f"to ``session.read.dbapi`` (note: the Anaconda repository " + f"requires a Python runtime version that the Snowflake " + f"Anaconda channel publishes builds for)." + ) + return packages + return _ANACONDA_UDTF_PACKAGE_MAP.get(dbms_type) + def get_jdbc_dbms(jdbc_url: str) -> str: """ diff --git a/src/snowflake/snowpark/context.py b/src/snowflake/snowpark/context.py index fe8d36090f..46c2419c1f 100644 --- a/src/snowflake/snowpark/context.py +++ b/src/snowflake/snowpark/context.py @@ -168,6 +168,8 @@ # The fully qualified name of the Anaconda shared repository (conda channel). _ANACONDA_SHARED_REPOSITORY = "snowflake.snowpark.anaconda_shared_repository" +# The fully qualified name of the PyPI shared repository (pypi channel). +_PYPI_SHARED_REPOSITORY = "snowflake.snowpark.pypi_shared_repository" # In case of failures or the current default artifact repository is unset, we fallback to this _DEFAULT_ARTIFACT_REPOSITORY = _ANACONDA_SHARED_REPOSITORY From 6adf23f03b20cdc40d4aae22edddee7a326860a8 Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Fri, 17 Apr 2026 12:35:08 -0700 Subject: [PATCH 03/14] add data source unit test --- tests/unit/test_data_source.py | 66 +++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_data_source.py b/tests/unit/test_data_source.py index 2e3a4639b6..3063099ebf 100644 --- a/tests/unit/test_data_source.py +++ b/tests/unit/test_data_source.py @@ -7,7 +7,15 @@ from unittest.mock import Mock, patch from snowflake.snowpark._internal.data_source.drivers.base_driver import BaseDriver from snowflake.snowpark._internal.data_source.datasource_reader import DataSourceReader -from snowflake.snowpark._internal.data_source.utils import DBMS_TYPE +from snowflake.snowpark._internal.data_source.utils import ( + DBMS_TYPE, + resolve_udtf_packages, +) +from snowflake.snowpark.context import ( + _ANACONDA_SHARED_REPOSITORY, + _PYPI_SHARED_REPOSITORY, +) +from snowflake.snowpark.exceptions import SnowparkClientException from snowflake.snowpark.types import StructType, StructField, StringType @@ -115,3 +123,59 @@ def test_datasource_reader_close_error_handling(cursor_fails, conn_fails): mock_logger.debug.assert_called() args, kwargs = mock_logger.debug.call_args assert re.search(r"Failed to close", args[0]) + + +@pytest.mark.parametrize( + "dbms_type,expected_fragment", + [ + (DBMS_TYPE.ORACLE_DB, "oracledb"), + (DBMS_TYPE.POSTGRES_DB, "psycopg2-binary"), + (DBMS_TYPE.MYSQL_DB, "pymysql"), + (DBMS_TYPE.SQLITE_DB, "snowflake-snowpark-python"), + ], +) +def test_resolve_udtf_packages_pypi_supported(dbms_type, expected_fragment): + """PyPI-supported DBMSes return a package list with the expected dependency.""" + packages = resolve_udtf_packages(dbms_type, _PYPI_SHARED_REPOSITORY) + assert any(expected_fragment in p for p in packages), ( + f"Expected to find a package matching '{expected_fragment}' in " f"{packages}" + ) + + +@pytest.mark.parametrize( + "dbms_type", + [DBMS_TYPE.SQL_SERVER_DB, DBMS_TYPE.DATABRICKS_DB], +) +def test_resolve_udtf_packages_pypi_unsupported_raises(dbms_type): + """SQL Server and Databricks have no PyPI-installable package set, so + ``resolve_udtf_packages`` raises a ``SnowparkClientException`` pointing + the user to the Anaconda artifact repository.""" + with pytest.raises(SnowparkClientException) as exc_info: + resolve_udtf_packages(dbms_type, _PYPI_SHARED_REPOSITORY) + + message = str(exc_info.value) + assert dbms_type.value in message + + +@pytest.mark.parametrize( + "dbms_type,expected_fragment", + [ + (DBMS_TYPE.ORACLE_DB, "oracledb"), + # On Anaconda we ship plain ``psycopg2`` (the conda build bundles + # libpq), in contrast to the PyPI variant which uses + # ``psycopg2-binary``. + (DBMS_TYPE.POSTGRES_DB, "psycopg2>="), + (DBMS_TYPE.MYSQL_DB, "pymysql"), + (DBMS_TYPE.SQLITE_DB, "snowflake-snowpark-python"), + (DBMS_TYPE.SQL_SERVER_DB, "msodbcsql"), + (DBMS_TYPE.DATABRICKS_DB, "databricks-sql-connector"), + ], +) +def test_resolve_udtf_packages_anaconda(dbms_type, expected_fragment): + """On the Anaconda repository every DBMS has a working package list, + including SQL Server and Databricks.""" + packages = resolve_udtf_packages(dbms_type, _ANACONDA_SHARED_REPOSITORY) + assert packages is not None + assert any(expected_fragment in p for p in packages), ( + f"Expected to find a package matching '{expected_fragment}' in " f"{packages}" + ) From 88133ac720672f3a409d628876040e2f70029f81 Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Fri, 17 Apr 2026 12:42:37 -0700 Subject: [PATCH 04/14] clarify PyPI package resolution error The PyPI gaps in UDTF package resolution (SQL Server, Databricks) are independent of the Python version - they apply to any session whose default artifact repository is PyPI, not just Python 3.14+. Update the comment and the SnowparkClientException message to reflect that, and note that on Python <3.14 a PyPI default can still occur via an account/database/schema-level override. --- .../snowpark/_internal/data_source/utils.py | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/snowflake/snowpark/_internal/data_source/utils.py b/src/snowflake/snowpark/_internal/data_source/utils.py index 1390789df3..c975c21166 100644 --- a/src/snowflake/snowpark/_internal/data_source/utils.py +++ b/src/snowflake/snowpark/_internal/data_source/utils.py @@ -122,17 +122,21 @@ class DRIVER_TYPE(str, Enum): DBMS_TYPE.MYSQL_DB: ["pymysql>=1.0.0,<2.0.0", "snowflake-snowpark-python"], } -# UDTF package list when using the PyPI shared repository, which is the -# default on Python 3.14+. Differences from the Anaconda map: -# - Postgres uses ``psycopg2-binary`` because PyPI's ``psycopg2`` is sdist -# only and requires ``pg_config`` to compile, which is not available in -# the server-side install sandbox. +# UDTF package list when using the PyPI shared repository. The server-side +# UDTF install sandbox refuses to compile source distributions (sdists), so +# every package here must be wheel-installable from PyPI. Differences from +# the Anaconda map: +# - Postgres uses ``psycopg2-binary`` because ``psycopg2`` on PyPI is +# sdist-only; ``psycopg2-binary`` is the wheel-packaged equivalent. # - SQL Server has no PyPI-installable equivalent of ``msodbcsql`` (it is # Microsoft's ODBC driver, distributed as a system package), so the # UDTF path cannot work on PyPI today. # - Databricks depends on ``databricks-sql-connector``, which transitively -# requires ``thrift<0.21``; no version of ``thrift`` publishes a Python -# 3.14 wheel on PyPI, and the server refuses to compile sdists. +# requires ``thrift``; ``thrift`` on PyPI is sdist-only for every +# version, so the server cannot install it. +# These PyPI gaps are independent of the Python version; they apply to any +# session whose default artifact repository is PyPI (most commonly Python +# 3.14+, where PyPI is the global default). _PYPI_UDTF_PACKAGE_MAP = { DBMS_TYPE.ORACLE_DB: ["oracledb>=2.0.0,<4.0.0", "snowflake-snowpark-python"], DBMS_TYPE.SQLITE_DB: ["snowflake-snowpark-python"], @@ -156,8 +160,9 @@ def resolve_udtf_packages( """Return the default UDTF package list for ``dbms_type``. Picks the package list appropriate for ``artifact_repository``. When the - repository is the PyPI shared repository (the default on Python 3.14+), - some DBMSes have no working package set; this raises + repository is the PyPI shared repository, some DBMSes have no working + package set (their dependencies are not wheel-installable from PyPI, and + the server-side UDTF sandbox refuses to compile sdists); this raises :class:`SnowparkClientException` with guidance to switch repositories. """ if artifact_repository == _PYPI_SHARED_REPOSITORY: @@ -165,16 +170,15 @@ def resolve_udtf_packages( if packages is None: raise SnowparkClientException( f"DataFrameReader.dbapi server-side UDTF ingestion for " - f"{dbms_type.value} is not supported on the PyPI artifact " - f"repository (the default on Python 3.14+). The required " - f"packages are not installable from PyPI in the server-side " - f"sandbox. To enable UDTF ingestion for this DBMS, run with " - f"the Anaconda artifact repository, e.g. by passing " - f"``udtf_configs={{'artifact_repository': " - f"'snowflake.snowpark.anaconda_shared_repository', ...}}`` " - f"to ``session.read.dbapi`` (note: the Anaconda repository " - f"requires a Python runtime version that the Snowflake " - f"Anaconda channel publishes builds for)." + f"{dbms_type.value} is not supported when the session's " + f"default artifact repository is PyPI: the required " + f"packages are not wheel-installable from PyPI, and the " + f"server-side UDTF install sandbox refuses to compile " + f"source distributions. Switch to the Anaconda artifact " + f"repository (on Python 3.14+, PyPI is the client-side " + f"default; on older Python versions, Anaconda is the " + f"default but may have been overridden at the account, " + f"database, or schema level)." ) return packages return _ANACONDA_UDTF_PACKAGE_MAP.get(dbms_type) From 5dc2dff82699ea75742cf58956e038205dbc576c Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Tue, 21 Apr 2026 10:35:36 -0700 Subject: [PATCH 05/14] use specified artifact repo is provided --- .../snowpark/_internal/data_source/drivers/base_driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py b/src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py index 272924faa8..88a65e8825 100644 --- a/src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py +++ b/src/snowflake/snowpark/_internal/data_source/drivers/base_driver.py @@ -172,7 +172,8 @@ def udtf_ingestion( ) resolved_packages = packages or resolve_udtf_packages( - self.dbms_type, session._get_default_artifact_repository() + self.dbms_type, + artifact_repository or session._get_default_artifact_repository(), ) udtf_name = random_name_for_temp_object(TempObjectType.FUNCTION) From 48c75086827764ab7821b77136989f7b89b6e462 Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Wed, 22 Apr 2026 15:46:58 -0700 Subject: [PATCH 06/14] ci: enable data source tests for Python 3.14 The dbapi UDTF package resolution fix in this branch (dynamic package map based on the effective artifact repository) is sufficient to unblock the data source tests on Python 3.14, whose default artifact repository is PyPI. Databricks and SQL Server UDTF ingestion still fail on PyPI due to upstream package availability, but those tests already skip on IS_PY314. --- .github/workflows/precommit.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml index 823a901a68..6f98cde48e 100644 --- a/.github/workflows/precommit.yml +++ b/.github/workflows/precommit.yml @@ -222,8 +222,7 @@ jobs: shell: bash - name: Run data source tests # psycopg2 is not supported on macos 3.9 - # TODO: enable datasource tests for 3.14 - if: ${{ !(matrix.os == 'macos-latest' && matrix.python-version == '3.9') && !(matrix.python-version == '3.14') }} + if: ${{ !(matrix.os == 'macos-latest' && matrix.python-version == '3.9') }} run: python -m tox -e datasource env: PYTHON_VERSION: ${{ matrix.python-version }} From 66eaedce1cd7daf4149f575fc94dfa842c91e612 Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Wed, 22 Apr 2026 15:47:28 -0700 Subject: [PATCH 07/14] test: skip Databricks and SQL Server UDTF tests on Python 3.14 These tests rely on server-side UDTF package installation from the default artifact repository. On Python 3.14 that default is PyPI, where: - databricks-sql-connector pulls in thrift, which has no 3.14 wheel and fails the server's sdist-compile policy. - msodbcsql is not published to PyPI at all. Skip the affected tests on 3.14 until the Snowflake Anaconda channel ships 3.14 builds or a PyPI-resolvable alternative exists. --- tests/integ/datasource/test_databricks.py | 10 ++++- tests/integ/datasource/test_sql_server.py | 45 ++++++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/tests/integ/datasource/test_databricks.py b/tests/integ/datasource/test_databricks.py index 42cffb9506..e95598e672 100644 --- a/tests/integ/datasource/test_databricks.py +++ b/tests/integ/datasource/test_databricks.py @@ -42,7 +42,7 @@ databricks_unicode_schema, databricks_double_quoted_schema, ) -from tests.utils import IS_IN_STORED_PROC, IS_MACOS, Utils +from tests.utils import IS_IN_STORED_PROC, IS_MACOS, Utils, IS_PY314 DEPENDENCIES_PACKAGE_UNAVAILABLE = True try: @@ -177,6 +177,10 @@ def test_double_quoted_column_databricks(session, custom_schema): [("table", TEST_TABLE_NAME), ("query", f"(SELECT * FROM {TEST_TABLE_NAME})")], ) @pytest.mark.udf +@pytest.mark.skipif( + IS_PY314, + reason="databricks-sql-connector's thrift dependency has no Python 3.14 wheel on PyPI; server-side UDTF install fails on the default repo.", +) def test_udtf_ingestion_databricks(session, input_type, input_value, caplog): # we define here to avoid test_databricks.py to be pickled and unpickled in UDTF def local_create_databricks_connection(): @@ -286,6 +290,10 @@ def test_session_init(session): ) +@pytest.mark.skipif( + IS_PY314, + reason="databricks-sql-connector's thrift dependency has no Python 3.14 wheel on PyPI; server-side UDTF install fails on the default repo.", +) def test_session_init_udtf(session): udtf_configs = { "external_access_integration": DATABRICKS_TEST_EXTERNAL_ACCESS_INTEGRATION diff --git a/tests/integ/datasource/test_sql_server.py b/tests/integ/datasource/test_sql_server.py index 8056bd397e..667753fc6a 100644 --- a/tests/integ/datasource/test_sql_server.py +++ b/tests/integ/datasource/test_sql_server.py @@ -7,7 +7,14 @@ from snowflake.snowpark._internal.data_source.utils import DBMS_TYPE from tests.parameters import SQL_SERVER_CONNECTION_PARAMETERS -from tests.utils import IS_IN_STORED_PROC, Utils, IS_WINDOWS, IS_MACOS, RUNNING_ON_GH +from tests.utils import ( + IS_IN_STORED_PROC, + Utils, + IS_WINDOWS, + IS_MACOS, + RUNNING_ON_GH, + IS_PY314, +) from tests.resources.test_data_source_dir.test_sql_server_data import ( SQL_SERVER_TABLE_NAME, EXPECTED_TEST_DATA, @@ -145,6 +152,10 @@ def test_sql_server_ingestion( ), ], ) +@pytest.mark.skipif( + IS_PY314, + reason="msodbcsql is not available on PyPI, so the server-side UDTF install fails on Python 3.14's default PyPI artifact repository.", +) def test_sql_server_udtf_ingestion( session, input_type, table_name, expected_data, expected_schema, apply_order ): @@ -182,13 +193,13 @@ def local_create_connection_sql_server(): [ ("table", "NONEXISTTABLE", "Invalid object name", None), ("query", "SELEC ** FORM TABLE", "Incorrect syntax near", None), - ( + pytest.param( "table", "NONEXISTTABLE", "Invalid object name", SQL_SERVER_TEST_EXTERNAL_ACCESS_INTEGRATION, ), - ( + pytest.param( "query", "SELEC ** FORM TABLE", "Incorrect syntax near", @@ -196,6 +207,10 @@ def local_create_connection_sql_server(): ), ], ) +@pytest.mark.skipif( + IS_PY314, + reason="msodbcsql is not available on PyPI, so the server-side UDTF install fails on Python 3.14's default PyPI artifact repository.", +) def test_error_case(session, input_type, input_value, error_message, udtf_configs): # Use local connection function when udtf_configs is provided if udtf_configs: @@ -229,9 +244,15 @@ def connection_func(): "udtf_configs", [ None, - SQL_SERVER_TEST_EXTERNAL_ACCESS_INTEGRATION, + pytest.param( + SQL_SERVER_TEST_EXTERNAL_ACCESS_INTEGRATION, + ), ], ) +@pytest.mark.skipif( + IS_PY314, + reason="msodbcsql is not available on PyPI, so the server-side UDTF install fails on Python 3.14's default PyPI artifact repository.", +) def test_partitions_and_predicates(session, udtf_configs): # Use local connection function when udtf_configs is provided if udtf_configs: @@ -298,9 +319,15 @@ def connection_func(): "udtf_configs", [ None, - SQL_SERVER_TEST_EXTERNAL_ACCESS_INTEGRATION, + pytest.param( + SQL_SERVER_TEST_EXTERNAL_ACCESS_INTEGRATION, + ), ], ) +@pytest.mark.skipif( + IS_PY314, + reason="msodbcsql is not available on PyPI, so the server-side UDTF install fails on Python 3.14's default PyPI artifact repository.", +) def test_session_init_statement(session, udtf_configs): # Use local connection function when udtf_configs is provided if udtf_configs: @@ -360,9 +387,15 @@ def test_pyodbc_driver_class_builder(): "udtf_configs", [ None, - SQL_SERVER_TEST_EXTERNAL_ACCESS_INTEGRATION, + pytest.param( + SQL_SERVER_TEST_EXTERNAL_ACCESS_INTEGRATION, + ), ], ) +@pytest.mark.skipif( + IS_PY314, + reason="msodbcsql is not available on PyPI, so the server-side UDTF install fails on Python 3.14's default PyPI artifact repository.", +) def test_sql_server_with_connection_parameters(session, udtf_configs): """Test connection_parameters with local/default ingestion and UDTF ingestion.""" From c6ce7b650eaa6f48fc6e34fcd0ed3b6407af80cf Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Wed, 22 Apr 2026 15:56:47 -0700 Subject: [PATCH 08/14] update conftest too --- tests/integ/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integ/conftest.py b/tests/integ/conftest.py index fc1835e923..f670c6964b 100644 --- a/tests/integ/conftest.py +++ b/tests/integ/conftest.py @@ -362,6 +362,10 @@ def session( session.sql("alter session set ENABLE_ROW_ACCESS_POLICY=true").collect() if sys.version_info.major == 3 and sys.version_info.minor == 14: session.sql("alter session set ENABLE_PYTHON_3_14=true").collect() + # TODO: delete once pypi becomes default for 3.14 + session.sql( + "alter session set DEFAULT_PYTHON_ARTIFACT_REPOSITORY=snowflake.snowpark.pypi_shared_repository" + ).collect() try: yield session From 050984af5adcfb7f6ae8fdd0073aaccfc06c44be Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Wed, 22 Apr 2026 16:51:17 -0700 Subject: [PATCH 09/14] disbale rerun --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fec74cd6a1..5884aee624 100644 --- a/tox.ini +++ b/tox.ini @@ -129,7 +129,7 @@ commands = notudf: {env:SNOWFLAKE_PYTEST_CMD} -m "{env:SNOWFLAKE_TEST_TYPE} and not udf" {posargs:} {env:RERUN_FLAGS} src/snowflake/snowpark tests udf: {env:SNOWFLAKE_PYTEST_CMD} -m "{env:SNOWFLAKE_TEST_TYPE} or udf" {posargs:} {env:RERUN_FLAGS} src/snowflake/snowpark tests notdoctest: {env:SNOWFLAKE_PYTEST_CMD} -m "{env:SNOWFLAKE_TEST_TYPE} or udf" {posargs:} {env:RERUN_FLAGS} tests - notudfdoctest: {env:SNOWFLAKE_PYTEST_CMD} -m "{env:SNOWFLAKE_TEST_TYPE} and not udf" {posargs:} {env:RERUN_FLAGS} tests + notudfdoctest: {env:SNOWFLAKE_PYTEST_CMD} -m "{env:SNOWFLAKE_TEST_TYPE} and not udf" {posargs:} tests local: {env:SNOWFLAKE_PYTEST_CMD} --local_testing_mode -m "integ or unit or mock" {posargs:} tests dailynotdoctest: {env:SNOWFLAKE_PYTEST_DAILY_CMD} -m "{env:SNOWFLAKE_TEST_TYPE} or udf" {posargs:} tests dailynotdoctestnotudf: {env:SNOWFLAKE_PYTEST_DAILY_CMD} -m "{env:SNOWFLAKE_TEST_TYPE} and not udf" {posargs:} tests From 3c3d7bc5be9e701eb64d90e381238e6633cda6fb Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Thu, 23 Apr 2026 10:22:24 -0700 Subject: [PATCH 10/14] fix conf --- tests/integ/conftest.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/integ/conftest.py b/tests/integ/conftest.py index f670c6964b..504cbcdaf6 100644 --- a/tests/integ/conftest.py +++ b/tests/integ/conftest.py @@ -286,6 +286,14 @@ def test_schema(connection, local_testing_mode) -> None: cursor.execute( f"GRANT ALL PRIVILEGES ON SCHEMA {TEST_SCHEMA} TO ROLE PUBLIC" ) + # TODO: delete once pypi becomes default for 3.14 + if sys.version_info.major == 3 and sys.version_info.minor == 14: + cursor.execute( + "alter session set ENABLE_DEFAULT_PYTHON_ARTIFACT_REPOSITORY=true" + ) + cursor.execute( + "alter schema set DEFAULT_PYTHON_ARTIFACT_REPOSITORY=snowflake.snowpark.pypi_shared_repository" + ) yield cursor.execute(f"DROP SCHEMA IF EXISTS {TEST_SCHEMA}") @@ -362,10 +370,6 @@ def session( session.sql("alter session set ENABLE_ROW_ACCESS_POLICY=true").collect() if sys.version_info.major == 3 and sys.version_info.minor == 14: session.sql("alter session set ENABLE_PYTHON_3_14=true").collect() - # TODO: delete once pypi becomes default for 3.14 - session.sql( - "alter session set DEFAULT_PYTHON_ARTIFACT_REPOSITORY=snowflake.snowpark.pypi_shared_repository" - ).collect() try: yield session From 88776557c500996903be0ca0b3742cab5ed97783 Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Thu, 23 Apr 2026 13:34:23 -0700 Subject: [PATCH 11/14] more --- tests/integ/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/integ/conftest.py b/tests/integ/conftest.py index 504cbcdaf6..fed59e720b 100644 --- a/tests/integ/conftest.py +++ b/tests/integ/conftest.py @@ -442,6 +442,14 @@ def temp_schema(connection, session, local_testing_mode) -> None: cursor.execute( f"GRANT ALL PRIVILEGES ON SCHEMA {temp_schema_name} TO ROLE PUBLIC" ) + # TODO: delete once pypi becomes default for 3.14 + if sys.version_info.major == 3 and sys.version_info.minor == 14: + cursor.execute( + "alter session set ENABLE_DEFAULT_PYTHON_ARTIFACT_REPOSITORY=true" + ) + cursor.execute( + "alter schema set DEFAULT_PYTHON_ARTIFACT_REPOSITORY=snowflake.snowpark.pypi_shared_repository" + ) yield temp_schema_name cursor.execute(f"DROP SCHEMA IF EXISTS {temp_schema_name}") From 0190094b45e01e67fa79a03995a34e3f67f5c268 Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Thu, 23 Apr 2026 14:46:18 -0700 Subject: [PATCH 12/14] try --- .github/workflows/precommit.yml | 50 ++++++++++++++++----------------- tests/integ/conftest.py | 6 ++++ 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml index 6f98cde48e..711f16bd47 100644 --- a/.github/workflows/precommit.yml +++ b/.github/workflows/precommit.yml @@ -187,31 +187,31 @@ jobs: # Specify SNOWFLAKE_IS_PYTHON_RUNTIME_TEST: 1 when adding >= python3.13 with no server-side support # For example, see https://github.com/snowflakedb/snowpark-python/pull/681 shell: bash - # do not run other tests for macos - - if: ${{ matrix.os != 'macos-latest' && matrix.python-version != '3.14' }} - name: Run tests (excluding doctests) - run: python -m tox -e "py${PYTHON_VERSION/\./}-notdoctest-ci" - env: - PYTHON_VERSION: ${{ matrix.python-version }} - cloud_provider: ${{ matrix.cloud-provider }} - PYTEST_ADDOPTS: --color=yes --tb=short - TOX_PARALLEL_NO_SPINNER: 1 - SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }} - SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }} - shell: bash - # TODO: Remove the test below and run udf tests for 3.14 - # for 3.14, skip udf, doctest - - if: ${{ matrix.os != 'macos-latest' && matrix.python-version == '3.14' }} - name: Run tests (excluding udf, doctests) - run: python -m tox -e "py${PYTHON_VERSION/\./}-notudfdoctest-ci" - env: - PYTHON_VERSION: ${{ matrix.python-version }} - cloud_provider: ${{ matrix.cloud-provider }} - PYTEST_ADDOPTS: --color=yes --tb=short - TOX_PARALLEL_NO_SPINNER: 1 - SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }} - SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }} - shell: bash + # # do not run other tests for macos + # - if: ${{ matrix.os != 'macos-latest' && matrix.python-version != '3.14' }} + # name: Run tests (excluding doctests) + # run: python -m tox -e "py${PYTHON_VERSION/\./}-notdoctest-ci" + # env: + # PYTHON_VERSION: ${{ matrix.python-version }} + # cloud_provider: ${{ matrix.cloud-provider }} + # PYTEST_ADDOPTS: --color=yes --tb=short + # TOX_PARALLEL_NO_SPINNER: 1 + # SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }} + # SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }} + # shell: bash + # # TODO: Remove the test below and run udf tests for 3.14 + # # for 3.14, skip udf, doctest + # - if: ${{ matrix.os != 'macos-latest' && matrix.python-version == '3.14' }} + # name: Run tests (excluding udf, doctests) + # run: python -m tox -e "py${PYTHON_VERSION/\./}-notudfdoctest-ci" + # env: + # PYTHON_VERSION: ${{ matrix.python-version }} + # cloud_provider: ${{ matrix.cloud-provider }} + # PYTEST_ADDOPTS: --color=yes --tb=short + # TOX_PARALLEL_NO_SPINNER: 1 + # SNOWPARK_PYTHON_API_TEST_BUCKET_PATH: ${{ secrets.SNOWPARK_PYTHON_API_TEST_BUCKET_PATH }} + # SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION: ${{ vars.SNOWPARK_PYTHON_API_S3_STORAGE_INTEGRATION }} + # shell: bash - name: Install MS ODBC Driver (Ubuntu only) if: ${{ contains(matrix.os, 'ubuntu') }} run: | diff --git a/tests/integ/conftest.py b/tests/integ/conftest.py index fed59e720b..f45156e8be 100644 --- a/tests/integ/conftest.py +++ b/tests/integ/conftest.py @@ -370,6 +370,12 @@ def session( session.sql("alter session set ENABLE_ROW_ACCESS_POLICY=true").collect() if sys.version_info.major == 3 and sys.version_info.minor == 14: session.sql("alter session set ENABLE_PYTHON_3_14=true").collect() + session.sql( + "alter session set ENABLE_DEFAULT_PYTHON_ARTIFACT_REPOSITORY=true" + ).collect() + session.sql( + "alter schema set DEFAULT_PYTHON_ARTIFACT_REPOSITORY=snowflake.snowpark.pypi_shared_repository" + ).collect() try: yield session From 0a5c328619efcebcdb99659c4ba7f050fcd735a1 Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Thu, 23 Apr 2026 14:51:40 -0700 Subject: [PATCH 13/14] update default --- src/snowflake/snowpark/context.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/snowflake/snowpark/context.py b/src/snowflake/snowpark/context.py index 46c2419c1f..a111839050 100644 --- a/src/snowflake/snowpark/context.py +++ b/src/snowflake/snowpark/context.py @@ -5,6 +5,7 @@ """Context module for Snowpark.""" import logging +import sys from typing import Callable, Optional import snowflake.snowpark @@ -170,8 +171,12 @@ _ANACONDA_SHARED_REPOSITORY = "snowflake.snowpark.anaconda_shared_repository" # The fully qualified name of the PyPI shared repository (pypi channel). _PYPI_SHARED_REPOSITORY = "snowflake.snowpark.pypi_shared_repository" -# In case of failures or the current default artifact repository is unset, we fallback to this -_DEFAULT_ARTIFACT_REPOSITORY = _ANACONDA_SHARED_REPOSITORY +# In case of failures and for routing to the right session package store, we use this +_DEFAULT_ARTIFACT_REPOSITORY = ( + _ANACONDA_SHARED_REPOSITORY + if sys.version_info < (3, 14) + else _PYPI_SHARED_REPOSITORY +) def configure_development_features( From 18a59c12490929fd7547111b11eb940adc861ca8 Mon Sep 17 00:00:00 2001 From: Ben Kogan Date: Thu, 23 Apr 2026 14:53:33 -0700 Subject: [PATCH 14/14] cleanup conf --- tests/integ/conftest.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/integ/conftest.py b/tests/integ/conftest.py index f45156e8be..504cbcdaf6 100644 --- a/tests/integ/conftest.py +++ b/tests/integ/conftest.py @@ -370,12 +370,6 @@ def session( session.sql("alter session set ENABLE_ROW_ACCESS_POLICY=true").collect() if sys.version_info.major == 3 and sys.version_info.minor == 14: session.sql("alter session set ENABLE_PYTHON_3_14=true").collect() - session.sql( - "alter session set ENABLE_DEFAULT_PYTHON_ARTIFACT_REPOSITORY=true" - ).collect() - session.sql( - "alter schema set DEFAULT_PYTHON_ARTIFACT_REPOSITORY=snowflake.snowpark.pypi_shared_repository" - ).collect() try: yield session @@ -448,14 +442,6 @@ def temp_schema(connection, session, local_testing_mode) -> None: cursor.execute( f"GRANT ALL PRIVILEGES ON SCHEMA {temp_schema_name} TO ROLE PUBLIC" ) - # TODO: delete once pypi becomes default for 3.14 - if sys.version_info.major == 3 and sys.version_info.minor == 14: - cursor.execute( - "alter session set ENABLE_DEFAULT_PYTHON_ARTIFACT_REPOSITORY=true" - ) - cursor.execute( - "alter schema set DEFAULT_PYTHON_ARTIFACT_REPOSITORY=snowflake.snowpark.pypi_shared_repository" - ) yield temp_schema_name cursor.execute(f"DROP SCHEMA IF EXISTS {temp_schema_name}")