From c94bec48e500034fed1c9a116bfa2d78ce31baf0 Mon Sep 17 00:00:00 2001 From: Jianzhun Du Date: Fri, 19 Sep 2025 10:36:18 -0700 Subject: [PATCH 1/7] d --- .../snowpark/_internal/analyzer/analyzer.py | 12 +++++ .../_internal/analyzer/analyzer_utils.py | 9 ++++ .../snowpark/_internal/analyzer/expression.py | 23 +++++++++ src/snowflake/snowpark/functions.py | 48 +++++++++++++++++++ 4 files changed, 92 insertions(+) diff --git a/src/snowflake/snowpark/_internal/analyzer/analyzer.py b/src/snowflake/snowpark/_internal/analyzer/analyzer.py index 6749972980..5700ae2fc9 100644 --- a/src/snowflake/snowpark/_internal/analyzer/analyzer.py +++ b/src/snowflake/snowpark/_internal/analyzer/analyzer.py @@ -28,6 +28,7 @@ like_expression, list_agg, model_expression, + service_expression, named_arguments_function, order_expression, range_statement, @@ -74,6 +75,7 @@ ListAgg, Literal, ModelExpression, + ServiceExpression, MultipleExpression, NamedExpression, NamedFunctionExpression, @@ -430,6 +432,16 @@ def analyze( ], ) + if isinstance(expr, ServiceExpression): + return service_expression( + expr.service_name, + expr.method_name, + [ + self.to_sql_try_avoid_cast(c, df_aliased_col_name_to_real_col_name) + for c in expr.children + ], + ) + if isinstance(expr, FunctionExpression): if expr.api_call_source is not None: self.session._conn._telemetry_client.send_function_usage_telemetry( diff --git a/src/snowflake/snowpark/_internal/analyzer/analyzer_utils.py b/src/snowflake/snowpark/_internal/analyzer/analyzer_utils.py index 92f9ab2344..6baed2dd5d 100644 --- a/src/snowflake/snowpark/_internal/analyzer/analyzer_utils.py +++ b/src/snowflake/snowpark/_internal/analyzer/analyzer_utils.py @@ -209,6 +209,7 @@ TAB = " " UUID_COMMENT = "-- {}" MODEL = "MODEL" +SERVICE = "SERVICE" EXCLAMATION_MARK = "!" HAVING = " HAVING " STORAGE_INTEGRATION = " STORAGE_INTEGRATION " @@ -278,6 +279,14 @@ def model_expression( return f"{MODEL}{LEFT_PARENTHESIS}{model_args_str}{RIGHT_PARENTHESIS}{EXCLAMATION_MARK}{method_name}{LEFT_PARENTHESIS}{COMMA.join(children)}{RIGHT_PARENTHESIS}" +def service_expression( + service_name: str, + method_name: str, + children: List[str], +) -> str: + return f"{SERVICE}{LEFT_PARENTHESIS}{service_name}{RIGHT_PARENTHESIS}{EXCLAMATION_MARK}{method_name}{LEFT_PARENTHESIS}{COMMA.join(children)}{RIGHT_PARENTHESIS}" + + def function_expression(name: str, children: List[str], is_distinct: bool) -> str: return ( name diff --git a/src/snowflake/snowpark/_internal/analyzer/expression.py b/src/snowflake/snowpark/_internal/analyzer/expression.py index 05e8f9e672..d95dcdc95a 100644 --- a/src/snowflake/snowpark/_internal/analyzer/expression.py +++ b/src/snowflake/snowpark/_internal/analyzer/expression.py @@ -575,6 +575,29 @@ def plan_node_category(self) -> PlanNodeCategory: return PlanNodeCategory.FUNCTION +class ServiceExpression(Expression): + def __init__( + self, + service_name: str, + method_name: str, + arguments: List[Expression], + ) -> None: + super().__init__() + self.service_name = service_name + self.method_name = method_name + self.children = arguments + + def dependent_column_names(self) -> Optional[AbstractSet[str]]: + return derive_dependent_columns(*self.children) + + def dependent_column_names_with_duplication(self) -> List[str]: + return derive_dependent_columns_with_duplication(*self.children) + + @property + def plan_node_category(self) -> PlanNodeCategory: + return PlanNodeCategory.FUNCTION + + class FunctionExpression(Expression): def __init__( self, diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index b5a0f33ba4..00d1ee30a1 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -181,6 +181,7 @@ ListAgg, Literal, ModelExpression, + ServiceExpression, MultipleExpression, Star, NamedFunctionExpression, @@ -10746,6 +10747,30 @@ def _call_model( ) +def _call_service( + service_name: str, + method_name: str, + *args, + _emit_ast: bool = True, +) -> Column: + if _emit_ast: + _ast = build_function_expr("service", [service_name, method_name, *args]) + else: + _ast = None + + args_list = parse_positional_args_to_list(*args) + expressions = [Column._to_expr(arg) for arg in args_list] + return Column( + ServiceExpression( + service_name, + method_name, + expressions, + ), + _ast=_ast, + _emit_ast=_emit_ast, + ) + + @publicapi def model( model_name: str, @@ -10775,6 +10800,29 @@ def model( ) +@publicapi +def service( + service_name: str, + _emit_ast: bool = True, +) -> Callable: + """ + Creates a service function that can be used to call a service method. + + Args: + service_name: The name of the service to call. + + Example:: + + >>> df = session.table("MYDB.MYSCHEMA.MYTABLE") + >>> svc = service("MYDB.MYSCHEMA.MY_SERVICE") + >>> result_df = df.select(svc("predict")(col("A"), col("B"))) + >>> result_df.count() + """ + return lambda method_name: lambda *args: _call_service( + service_name, method_name, *args, _emit_ast=_emit_ast + ) + + # Add these alias for user code migration call_builtin = call_function collect_set = array_unique_agg From 094b45b588b22e01398208b8aa6a143de1557f58 Mon Sep 17 00:00:00 2001 From: Jianzhun Du Date: Tue, 23 Sep 2025 12:03:05 -0700 Subject: [PATCH 2/7] d --- docs/source/snowpark/functions.rst | 1 + .../_internal/analyzer/analyzer_utils.py | 3 +-- src/snowflake/snowpark/functions.py | 27 ++++++++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/source/snowpark/functions.rst b/docs/source/snowpark/functions.rst index baaf9beefe..b49e001954 100644 --- a/docs/source/snowpark/functions.rst +++ b/docs/source/snowpark/functions.rst @@ -414,6 +414,7 @@ Functions seq4 seq8 sequence + service sha1 sha2 sin diff --git a/src/snowflake/snowpark/_internal/analyzer/analyzer_utils.py b/src/snowflake/snowpark/_internal/analyzer/analyzer_utils.py index 6baed2dd5d..3b84ad61ee 100644 --- a/src/snowflake/snowpark/_internal/analyzer/analyzer_utils.py +++ b/src/snowflake/snowpark/_internal/analyzer/analyzer_utils.py @@ -209,7 +209,6 @@ TAB = " " UUID_COMMENT = "-- {}" MODEL = "MODEL" -SERVICE = "SERVICE" EXCLAMATION_MARK = "!" HAVING = " HAVING " STORAGE_INTEGRATION = " STORAGE_INTEGRATION " @@ -284,7 +283,7 @@ def service_expression( method_name: str, children: List[str], ) -> str: - return f"{SERVICE}{LEFT_PARENTHESIS}{service_name}{RIGHT_PARENTHESIS}{EXCLAMATION_MARK}{method_name}{LEFT_PARENTHESIS}{COMMA.join(children)}{RIGHT_PARENTHESIS}" + return f"{service_name}{EXCLAMATION_MARK}{method_name}{LEFT_PARENTHESIS}{COMMA.join(children)}{RIGHT_PARENTHESIS}" def function_expression(name: str, children: List[str], is_distinct: bool) -> str: diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 00d1ee30a1..a5f0b188d5 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -10813,10 +10813,29 @@ def service( Example:: - >>> df = session.table("MYDB.MYSCHEMA.MYTABLE") - >>> svc = service("MYDB.MYSCHEMA.MY_SERVICE") - >>> result_df = df.select(svc("predict")(col("A"), col("B"))) - >>> result_df.count() + >>> original_role = session.get_current_role() + >>> original_db = session.get_current_database() + >>> original_schema = session.get_current_schema() + >>> try: + ... session.use_role("test_role") + ... session.use_database("tutorial_db") + ... session.use_schema("data_schema") + ... svc = service("tutorial_2_job_service") + ... result_df = session.range(1).select(svc("SPCS_CANCEL_JOB")()) + ... result_df.show() + ... finally: + ... if original_role: + ... session.use_role(original_role) + ... if original_db: + ... session.use_database(original_db) + ... if original_schema: + ... session.use_schema(original_schema) + ------------------------------------------------------ + |"TUTORIAL_2_JOB_SERVICE!SPCS_CANCEL_JOB()" | + ------------------------------------------------------ + |Job TUTORIAL_2_JOB_SERVICE is already canceled ... | + ------------------------------------------------------ + """ return lambda method_name: lambda *args: _call_service( service_name, method_name, *args, _emit_ast=_emit_ast From 5cc8103b3777eb7774fb7f2e2c78adaa22fc65c6 Mon Sep 17 00:00:00 2001 From: Jianzhun Du Date: Tue, 23 Sep 2025 12:04:13 -0700 Subject: [PATCH 3/7] test --- src/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/conftest.py b/src/conftest.py index cb859391c1..acc91767af 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -97,6 +97,7 @@ def pytest_collection_modifyitems(config, items): disabled_doctests = [ "ai_classify", "model", + "service", ] # Add any test names that should be skipped for item in items: # identify doctest items From 16ad012f13dd1a938880f784008757e3e24d15cb Mon Sep 17 00:00:00 2001 From: Jianzhun Du Date: Fri, 3 Oct 2025 23:19:28 -0700 Subject: [PATCH 4/7] d --- src/snowflake/snowpark/functions.py | 36 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index a5f0b188d5..1817e2dc8f 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -10813,27 +10813,25 @@ def service( Example:: - >>> original_role = session.get_current_role() - >>> original_db = session.get_current_database() - >>> original_schema = session.get_current_schema() - >>> try: - ... session.use_role("test_role") - ... session.use_database("tutorial_db") - ... session.use_schema("data_schema") - ... svc = service("tutorial_2_job_service") - ... result_df = session.range(1).select(svc("SPCS_CANCEL_JOB")()) - ... result_df.show() - ... finally: - ... if original_role: - ... session.use_role(original_role) - ... if original_db: - ... session.use_database(original_db) - ... if original_schema: - ... session.use_schema(original_schema) + >>> service_instance = service("FORECAST_MODEL_SERVICE") + >>> # Prepare a DataFrame with the ten expected features + >>> df = session.create_dataframe( + ... [ + ... (0.038076, 0.050680, 0.061696, 0.021872, -0.044223, -0.034821, -0.043401, -0.002592, 0.019907, -0.017646), + ... (-0.001882, -0.044642, -0.051474, -0.026328, -0.008449, -0.019163, 0.074412, -0.039493, -0.068332, -0.092204), + ... ], + ... schema=["age", "sex", "bmi", "bp", "s1", "s2", "s3", "s4", "s5", "s6"], + ... ) + >>> # Invoke the model's predict method exposed by the service + >>> result_df = df.select( + ... service_instance("predict")(col("age"), col("sex"), col("bmi"), col("bp"), col("s1"), col("s2"), col("s3"), col("s4"), col("s5"), col("s6"))["output_feature_0"] + ... ) + >>> result_df.show() ------------------------------------------------------ - |"TUTORIAL_2_JOB_SERVICE!SPCS_CANCEL_JOB()" | + |"FORECAST_MODEL_SERVICE!PREDICT(""AGE"", ""SEX"... | ------------------------------------------------------ - |Job TUTORIAL_2_JOB_SERVICE is already canceled ... | + |82.31314086914062 | + |220.2223358154297 | ------------------------------------------------------ """ From f0ed3f3add63e6d89c940cb456e736387e4161da Mon Sep 17 00:00:00 2001 From: Jianzhun Du Date: Fri, 3 Oct 2025 23:23:47 -0700 Subject: [PATCH 5/7] changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a3ed0f16b..e439f8473a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Release History +## 1.41.0 (YYYY-MM-DD) + +### Snowpark Python API Updates + +#### New Features + +- Added a new function `service` in `snowflake.snowpark.functions` that allows users to create a callable representing a Snowpark Container Services (SPCS) service. + ## 1.40.0 (YYYY-MM-DD) ### Snowpark Python API Updates From dced1712c6442aff11087c6056ee0e2a20907f96 Mon Sep 17 00:00:00 2001 From: Jianzhun Du Date: Mon, 6 Oct 2025 13:42:45 -0700 Subject: [PATCH 6/7] d --- src/snowflake/snowpark/functions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 1817e2dc8f..d7c0956a9b 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -10818,7 +10818,6 @@ def service( >>> df = session.create_dataframe( ... [ ... (0.038076, 0.050680, 0.061696, 0.021872, -0.044223, -0.034821, -0.043401, -0.002592, 0.019907, -0.017646), - ... (-0.001882, -0.044642, -0.051474, -0.026328, -0.008449, -0.019163, 0.074412, -0.039493, -0.068332, -0.092204), ... ], ... schema=["age", "sex", "bmi", "bp", "s1", "s2", "s3", "s4", "s5", "s6"], ... ) @@ -10830,7 +10829,6 @@ def service( ------------------------------------------------------ |"FORECAST_MODEL_SERVICE!PREDICT(""AGE"", ""SEX"... | ------------------------------------------------------ - |82.31314086914062 | |220.2223358154297 | ------------------------------------------------------ From fb68e2d52ba3076390f21ccc09298d86b897e6ac Mon Sep 17 00:00:00 2001 From: Jianzhun Du Date: Mon, 6 Oct 2025 15:45:07 -0700 Subject: [PATCH 7/7] fix --- src/snowflake/snowpark/functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index d7c0956a9b..236d5c0ff8 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -10813,7 +10813,7 @@ def service( Example:: - >>> service_instance = service("FORECAST_MODEL_SERVICE") + >>> service_instance = service("TESTSCHEMA_SNOWPARK_PYTHON.FORECAST_MODEL_SERVICE") >>> # Prepare a DataFrame with the ten expected features >>> df = session.create_dataframe( ... [ @@ -10827,7 +10827,7 @@ def service( ... ) >>> result_df.show() ------------------------------------------------------ - |"FORECAST_MODEL_SERVICE!PREDICT(""AGE"", ""SEX"... | + |"TESTSCHEMA_SNOWPARK_PYTHON.FORECAST_MODEL_SERV... | ------------------------------------------------------ |220.2223358154297 | ------------------------------------------------------