diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml index 20a2d5274a2..0d8ca77dac3 100644 --- a/.github/workflows/precommit.yml +++ b/.github/workflows/precommit.yml @@ -85,7 +85,7 @@ jobs: matrix: os: [macos-latest, windows-latest-64-cores, ubuntu-latest-64-cores] python-version: ["3.9", "3.10", "3.11"] - cloud-provider: [aws, azure, gcp] + cloud-provider: [aws, gcp] # TODO: SNOW-1643374 add azure back exclude: # only run macos with aws py3.9 for doctest - os: macos-latest @@ -309,7 +309,7 @@ jobs: matrix: os: [macos-latest, windows-latest-64-cores, ubuntu-latest-64-cores] python-version: [ "3.9", "3.10", "3.11" ] - cloud-provider: [aws, azure, gcp] + cloud-provider: [aws, gcp] # TODO: SNOW-1643374 add azure back exclude: # only run macos with aws py3.9 for doctest - os: macos-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a4c9b8d5ee..1cf5845439c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,14 @@ ### Snowpark Python API Updates +### New Features + +- Added following new functions in `snowflake.snowpark.functions`: + - `array_remove` + - `ln` + #### Improvements -- Added support for function `functions.ln` - Added support for specifying the following to `DataFrameWriter.save_as_table`: - `enable_schema_evolution` - `data_retention_time` @@ -45,13 +50,15 @@ #### New Features -- Added limited support for the `Timedelta` type, including +- Added limited support for the `Timedelta` type, including the following features. Snowpark pandas will raise `NotImplementedError` for unsupported `Timedelta` use cases. - supporting tracking the Timedelta type through `copy`, `cache_result`, `shift`, `sort_index`. - - converting non-timedelta to timedelta via `astype`. + - converting non-timedelta to timedelta via `astype`. - `NotImplementedError` will be raised for the rest of methods that do not support `Timedelta`. - support for subtracting two timestamps to get a Timedelta. - - support indexing with Timedelta data columns. + - support indexing with Timedelta data columns. - support for adding or subtracting timestamps and `Timedelta`. + - support for binary arithmetic between two `Timedelta` values. + - support for lazy `TimedeltaIndex`. - Added support for index's arithmetic and comparison operators. - Added support for `Series.dt.round`. - Added documentation pages for `DatetimeIndex`. diff --git a/docs/source/modin/indexing.rst b/docs/source/modin/indexing.rst index fa4f0538890..80ceba61bec 100644 --- a/docs/source/modin/indexing.rst +++ b/docs/source/modin/indexing.rst @@ -220,3 +220,43 @@ DatetimeIndex DatetimeIndex.mean DatetimeIndex.std + +.. _api.timedeltaindex: + +TimedeltaIndex +-------------- + +.. autosummary:: + :toctree: pandas_api/ + + TimedeltaIndex + +.. rubric:: `TimedeltaIndex` Components + +.. autosummary:: + :toctree: pandas_api/ + + TimedeltaIndex.days + TimedeltaIndex.seconds + TimedeltaIndex.microseconds + TimedeltaIndex.nanoseconds + TimedeltaIndex.components + TimedeltaIndex.inferred_freq + +.. rubric:: `TimedeltaIndex` Conversion + +.. autosummary:: + :toctree: pandas_api/ + + TimedeltaIndex.as_unit + TimedeltaIndex.to_pytimedelta + TimedeltaIndex.round + TimedeltaIndex.floor + TimedeltaIndex.ceil + +.. rubric:: `TimedeltaIndex` Methods + +.. autosummary:: + :toctree: pandas_api/ + + TimedeltaIndex.mean diff --git a/docs/source/modin/supported/dataframe_supported.rst b/docs/source/modin/supported/dataframe_supported.rst index 1855eb314b3..6bb214e3bd6 100644 --- a/docs/source/modin/supported/dataframe_supported.rst +++ b/docs/source/modin/supported/dataframe_supported.rst @@ -98,8 +98,8 @@ Methods +-----------------------------+---------------------------------+----------------------------------+----------------------------------------------------+ | ``assign`` | Y | | | +-----------------------------+---------------------------------+----------------------------------+----------------------------------------------------+ -| ``astype`` | P | | ``N``: from string to datetime or ``errors == | -| | | | "ignore"`` | +| ``astype`` | P | | ``N`` if from string to datetime/timedelta or | +| | | | ``errors == "ignore"`` | +-----------------------------+---------------------------------+----------------------------------+----------------------------------------------------+ | ``at_time`` | N | | | +-----------------------------+---------------------------------+----------------------------------+----------------------------------------------------+ diff --git a/docs/source/modin/supported/index.rst b/docs/source/modin/supported/index.rst index 97202d69290..2d7999c4954 100644 --- a/docs/source/modin/supported/index.rst +++ b/docs/source/modin/supported/index.rst @@ -16,6 +16,7 @@ To view the docs for the most recent release, check that you’re viewing the st dataframe_supported index_supported datetime_index_supported + timedelta_index_supported window_supported groupby_supported resampling_supported diff --git a/docs/source/modin/supported/series_supported.rst b/docs/source/modin/supported/series_supported.rst index ea78a3a0e68..331be4d0298 100644 --- a/docs/source/modin/supported/series_supported.rst +++ b/docs/source/modin/supported/series_supported.rst @@ -105,8 +105,8 @@ Methods +-----------------------------+---------------------------------+----------------------------------+----------------------------------------------------+ | ``asof`` | N | | | +-----------------------------+---------------------------------+----------------------------------+----------------------------------------------------+ -| ``astype`` | P | | ``N``: from string to datetime or ``errors == | -| | | | "ignore"`` | +| ``astype`` | P | | ``N`` if from string to datetime/timedelta or | +| | | | ``errors == "ignore"`` | +-----------------------------+---------------------------------+----------------------------------+----------------------------------------------------+ | ``at_time`` | N | | | +-----------------------------+---------------------------------+----------------------------------+----------------------------------------------------+ diff --git a/docs/source/modin/supported/timedelta_index_supported.rst b/docs/source/modin/supported/timedelta_index_supported.rst new file mode 100644 index 00000000000..73abe530fd7 --- /dev/null +++ b/docs/source/modin/supported/timedelta_index_supported.rst @@ -0,0 +1,48 @@ +``pd.TimedeltaIndex`` supported APIs +==================================== + +The following table is structured as follows: The first column contains the method name. +The second column is a flag for whether or not there is an implementation in Snowpark for +the method in the left column. + +.. note:: + ``Y`` stands for yes, i.e., supports distributed implementation, ``N`` stands for no and API simply errors out, + ``P`` stands for partial (meaning some parameters may not be supported yet), and ``D`` stands for defaults to single + node pandas execution via UDF/Sproc. + +Attributes + ++-----------------------------+---------------------------------+----------------------------------------------------+ +| TimedeltaIndex attribute | Snowpark implemented? (Y/N/P/D) | Notes for current implementation | ++-----------------------------+---------------------------------+----------------------------------------------------+ +| ``days`` | N | | ++-----------------------------+---------------------------------+----------------------------------------------------+ +| ``seconds`` | N | | ++-----------------------------+---------------------------------+----------------------------------------------------+ +| ``microseconds`` | N | | ++-----------------------------+---------------------------------+----------------------------------------------------+ +| ``nanoseconds`` | N | | ++-----------------------------+---------------------------------+----------------------------------------------------+ +| ``components`` | N | | ++-----------------------------+---------------------------------+----------------------------------------------------+ +| ``inferred_freq`` | N | | ++-----------------------------+---------------------------------+----------------------------------------------------+ + + +Methods + ++-----------------------------+---------------------------------+----------------------------------+-------------------------------------------+ +| DataFrame method | Snowpark implemented? (Y/N/P/D) | Missing parameters | Notes for current implementation | ++-----------------------------+---------------------------------+----------------------------------+-------------------------------------------+ +| ``as_unit`` | N | | | ++-----------------------------+---------------------------------+----------------------------------+-------------------------------------------+ +| ``to_pytimedelta`` | N | | | ++-----------------------------+---------------------------------+----------------------------------+-------------------------------------------+ +| ``round`` | N | | | ++-----------------------------+---------------------------------+----------------------------------+-------------------------------------------+ +| ``floor`` | N | | | ++-----------------------------+---------------------------------+----------------------------------+-------------------------------------------+ +| ``ceil`` | N | | | ++-----------------------------+---------------------------------+----------------------------------+-------------------------------------------+ +| ``mean`` | N | | | ++-----------------------------+---------------------------------+----------------------------------+-------------------------------------------+ diff --git a/docs/source/snowpark/functions.rst b/docs/source/snowpark/functions.rst index 100cb5470fc..9a381e5046a 100644 --- a/docs/source/snowpark/functions.rst +++ b/docs/source/snowpark/functions.rst @@ -43,6 +43,7 @@ Functions array_min array_position array_prepend + array_remove array_size array_slice array_sort diff --git a/src/snowflake/snowpark/_internal/analyzer/snowflake_plan.py b/src/snowflake/snowpark/_internal/analyzer/snowflake_plan.py index 83c0eebb9c7..aad369a8b83 100644 --- a/src/snowflake/snowpark/_internal/analyzer/snowflake_plan.py +++ b/src/snowflake/snowpark/_internal/analyzer/snowflake_plan.py @@ -1608,6 +1608,9 @@ def with_query_block( new_query = project_statement([], name) + # note we do not propagate the query parameter of the child here, + # the query parameter will be propagate along with the definition during + # query generation stage. queries = child.queries[:-1] + [Query(sql=new_query)] # propagate the cte table referenced_ctes = {name}.union(child.referenced_ctes) diff --git a/src/snowflake/snowpark/_internal/compiler/query_generator.py b/src/snowflake/snowpark/_internal/compiler/query_generator.py index fdf8af9d4dd..d9220424097 100644 --- a/src/snowflake/snowpark/_internal/compiler/query_generator.py +++ b/src/snowflake/snowpark/_internal/compiler/query_generator.py @@ -64,7 +64,7 @@ def __init__( # NOTE: the dict used here is an ordered dict, all with query block definition is recorded in the # order of when the with query block is visited. The order is important to make sure the dependency # between the CTE definition is satisfied. - self.resolved_with_query_block: Dict[str, str] = {} + self.resolved_with_query_block: Dict[str, Query] = {} def generate_queries( self, logical_plans: List[LogicalPlan] @@ -209,7 +209,7 @@ def do_resolve_with_resolved_children( if logical_plan.name not in self.resolved_with_query_block: self.resolved_with_query_block[ logical_plan.name - ] = resolved_child.queries[-1].sql + ] = resolved_child.queries[-1] resolved_plan = self.plan_builder.with_query_block( logical_plan.name, diff --git a/src/snowflake/snowpark/_internal/compiler/utils.py b/src/snowflake/snowpark/_internal/compiler/utils.py index 579c3b8e5d6..273ebe0440a 100644 --- a/src/snowflake/snowpark/_internal/compiler/utils.py +++ b/src/snowflake/snowpark/_internal/compiler/utils.py @@ -227,6 +227,9 @@ def update_resolvable_node( # re-calculation of the sql query and snowflake plan node._sql_query = None node._snowflake_plan = None + # make sure we also clean up the cached _projection_in_str, so that + # the projection expression can be re-analyzed during code generation + node._projection_in_str = None node.analyzer = query_generator # update the pre_actions and post_actions for the select statement @@ -267,12 +270,26 @@ def update_resolvable_node( update_resolvable_node(node.snowflake_plan, query_generator) node.analyzer = query_generator + node.pre_actions = node._snowflake_plan.queries[:-1] + node.post_actions = node._snowflake_plan.post_actions + node._api_calls = node._snowflake_plan.api_calls + + if isinstance(node, SelectSnowflakePlan): + node.expr_to_alias.update(node._snowflake_plan.expr_to_alias) + node.df_aliased_col_name_to_real_col_name.update( + node._snowflake_plan.df_aliased_col_name_to_real_col_name + ) + node._query_params = [] + for query in node._snowflake_plan.queries: + if query.params: + node._query_params.extend(query.params) + elif isinstance(node, Selectable): node.analyzer = query_generator def get_snowflake_plan_queries( - plan: SnowflakePlan, resolved_with_query_blocks: Dict[str, str] + plan: SnowflakePlan, resolved_with_query_blocks: Dict[str, Query] ) -> Dict[PlanQueryType, List[Query]]: from snowflake.snowpark._internal.analyzer.analyzer_utils import cte_statement @@ -286,12 +303,16 @@ def get_snowflake_plan_queries( post_action_queries = copy.deepcopy(plan.post_actions) table_names = [] definition_queries = [] + final_query_params = [] for name, definition_query in resolved_with_query_blocks.items(): if name in plan.referenced_ctes: table_names.append(name) - definition_queries.append(definition_query) + definition_queries.append(definition_query.sql) + final_query_params.extend(definition_query.params) with_query = cte_statement(definition_queries, table_names) plan_queries[-1].sql = with_query + plan_queries[-1].sql + final_query_params.extend(plan_queries[-1].params) + plan_queries[-1].params = final_query_params return { PlanQueryType.QUERIES: plan_queries, diff --git a/src/snowflake/snowpark/functions.py b/src/snowflake/snowpark/functions.py index 8f8b156132c..58c2ab8518c 100644 --- a/src/snowflake/snowpark/functions.py +++ b/src/snowflake/snowpark/functions.py @@ -5333,6 +5333,56 @@ def array_append(array: ColumnOrName, element: ColumnOrName) -> Column: return builtin("array_append")(a, e) +def array_remove(array: ColumnOrName, element: ColumnOrLiteral) -> Column: + """Given a source ARRAY, returns an ARRAY with elements of the specified value removed. + + Args: + array: name of column containing array. + element: element to be removed from the array. If the element is a VARCHAR, it needs + to be casted into VARIANT data type. + + Examples:: + >>> from snowflake.snowpark.types import VariantType + >>> df = session.create_dataframe([([1, '2', 3.1, 1, 1],)], ['data']) + >>> df.select(array_remove(df.data, 1).alias("objects")).show() + ------------- + |"OBJECTS" | + ------------- + |[ | + | "2", | + | 3.1 | + |] | + ------------- + + + >>> df.select(array_remove(df.data, lit('2').cast(VariantType())).alias("objects")).show() + ------------- + |"OBJECTS" | + ------------- + |[ | + | 1, | + | 3.1, | + | 1, | + | 1 | + |] | + ------------- + + + >>> df.select(array_remove(df.data, None).alias("objects")).show() + ------------- + |"OBJECTS" | + ------------- + |NULL | + ------------- + + + See Also: + - `ARRAY `_ for more details on semi-structured arrays. + """ + a = _to_col_if_str(array, "array_remove") + return builtin("array_remove")(a, element) + + def array_cat(array1: ColumnOrName, array2: ColumnOrName) -> Column: """Returns the concatenation of two ARRAYs. diff --git a/src/snowflake/snowpark/modin/pandas/__init__.py b/src/snowflake/snowpark/modin/pandas/__init__.py index 975289684cf..c4eb07d9589 100644 --- a/src/snowflake/snowpark/modin/pandas/__init__.py +++ b/src/snowflake/snowpark/modin/pandas/__init__.py @@ -63,7 +63,6 @@ SparseDtype, StringDtype, Timedelta, - TimedeltaIndex, Timestamp, UInt8Dtype, UInt16Dtype, @@ -156,6 +155,7 @@ from snowflake.snowpark.modin.plugin.extensions.pd_overrides import ( # isort: skip # noqa: E402,F401 Index, DatetimeIndex, + TimedeltaIndex, ) import snowflake.snowpark.modin.plugin.extensions.base_overrides # isort: skip # noqa: E402,F401 import snowflake.snowpark.modin.plugin.extensions.dataframe_extensions # isort: skip # noqa: E402,F401 diff --git a/src/snowflake/snowpark/modin/plugin/_internal/binary_op_utils.py b/src/snowflake/snowpark/modin/plugin/_internal/binary_op_utils.py index 7d03940b7e0..a0ca357c59b 100644 --- a/src/snowflake/snowpark/modin/plugin/_internal/binary_op_utils.py +++ b/src/snowflake/snowpark/modin/plugin/_internal/binary_op_utils.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from types import MappingProxyType +import numpy as np import pandas as native_pd from pandas._typing import Callable, Scalar @@ -236,10 +237,34 @@ def _compute_subtraction_between_snowpark_timestamp_columns( "rmul": "mul", "rsub": "sub", "rmod": "mod", + "__rand__": "__and__", + "__ror__": "__or__", } ) +def _op_is_between_two_timedeltas_or_timedelta_and_null( + first_datatype: DataType, second_datatype: DataType +) -> bool: + """ + Whether the binary operation is between two timedeltas, or between timedelta and null. + + Args: + first_datatype: First datatype + second_datatype: Second datatype + + Returns: + bool: Whether op is between two timedeltas or between timedelta and null. + """ + return ( + isinstance(first_datatype, TimedeltaType) + and isinstance(second_datatype, (TimedeltaType, NullType)) + ) or ( + isinstance(first_datatype, (TimedeltaType, NullType)) + and isinstance(second_datatype, TimedeltaType) + ) + + def compute_binary_op_between_snowpark_columns( op: str, first_operand: SnowparkColumn, @@ -274,6 +299,7 @@ def compute_binary_op_between_snowpark_columns( ) binary_op_result_column = None + snowpark_pandas_type = None # some operators and the data types have to be handled specially to align with pandas # However, it is difficult to fail early if the arithmetic operator is not compatible @@ -290,7 +316,18 @@ def compute_binary_op_between_snowpark_columns( and isinstance(second_datatype(), TimestampType) ): binary_op_result_column = dateadd("ns", first_operand, second_operand) - elif op == "add" and ( + elif op in ( + "add", + "sub", + "eq", + "ne", + "gt", + "ge", + "lt", + "le", + "floordiv", + "truediv", + ) and ( ( isinstance(first_datatype(), TimedeltaType) and isinstance(second_datatype(), NullType) @@ -315,16 +352,66 @@ def compute_binary_op_between_snowpark_columns( # Timedelta - Timestamp doesn't make sense. Raise the same error # message as pandas. raise TypeError("bad operand type for unary -: 'DatetimeArray'") - elif isinstance(first_datatype(), TimedeltaType) or isinstance( - second_datatype(), TimedeltaType + elif op == "mod" and _op_is_between_two_timedeltas_or_timedelta_and_null( + first_datatype(), second_datatype() + ): + binary_op_result_column = compute_modulo_between_snowpark_columns( + first_operand, first_datatype(), second_operand, second_datatype() + ) + snowpark_pandas_type = TimedeltaType() + elif op == "pow" and _op_is_between_two_timedeltas_or_timedelta_and_null( + first_datatype(), second_datatype() + ): + raise TypeError("unsupported operand type for **: Timedelta") + elif op == "__or__" and _op_is_between_two_timedeltas_or_timedelta_and_null( + first_datatype(), second_datatype() + ): + raise TypeError("unsupported operand type for |: Timedelta") + elif op == "__and__" and _op_is_between_two_timedeltas_or_timedelta_and_null( + first_datatype(), second_datatype() + ): + raise TypeError("unsupported operand type for &: Timedelta") + elif ( + op in ("add", "sub") + and isinstance(first_datatype(), TimedeltaType) + and isinstance(second_datatype(), TimedeltaType) + ): + snowpark_pandas_type = TimedeltaType() + elif op == "mul" and _op_is_between_two_timedeltas_or_timedelta_and_null( + first_datatype(), second_datatype() + ): + raise np.core._exceptions._UFuncBinaryResolutionError( # type: ignore[attr-defined] + np.multiply, (np.dtype("timedelta64[ns]"), np.dtype("timedelta64[ns]")) + ) + elif _op_is_between_two_timedeltas_or_timedelta_and_null( + first_datatype(), second_datatype() + ) and op in ("eq", "ne", "gt", "ge", "lt", "le", "truediv"): + # These operations, when done between timedeltas, work without any + # extra handling in `snowpark_pandas_type` or `binary_op_result_column`. + # They produce outputs that are not timedeltas (e.g. numbers for floordiv + # and truediv, and bools for the comparisons). + pass + elif ( + # equal_null and floordiv for timedelta also work without special + # handling, but we need to exclude them from the above case so we catch + # them in an `elif` clause further down. + op not in ("equal_null", "floordiv") + and ( + ( + isinstance(first_datatype(), TimedeltaType) + and not isinstance(second_datatype(), TimedeltaType) + ) + or ( + not isinstance(first_datatype(), TimedeltaType) + and isinstance(second_datatype(), TimedeltaType) + ) + ) ): # We don't support these cases yet. - # TODO(SNOW-1637101, SNOW-1637102): Support these cases. + # TODO(SNOW-1637102): Support this case. ErrorMessage.not_implemented( - f"Snowpark pandas does not yet support the binary operation {op} with timedelta types." + f"Snowpark pandas does not yet support the binary operation {op} with a Timedelta column and a non-Timedelta column." ) - elif op == "truediv": - binary_op_result_column = first_operand / second_operand elif op == "floordiv": binary_op_result_column = floor(first_operand / second_operand) elif op == "mod": @@ -335,9 +422,9 @@ def compute_binary_op_between_snowpark_columns( binary_op_result_column = compute_power_between_snowpark_columns( first_operand, second_operand ) - elif op in ["__or__", "__ror__"]: + elif op == "__or__": binary_op_result_column = first_operand | second_operand - elif op in ["__and__", "__rand__"]: + elif op == "__and__": binary_op_result_column = first_operand & second_operand elif ( op == "add" @@ -370,6 +457,8 @@ def compute_binary_op_between_snowpark_columns( pandas_lit(""), ) elif op == "equal_null": + # TODO(SNOW-1641716): In Snowpark pandas, generally use this equal_null + # with type checking intead of snowflake.snowpark.functions.equal_null. if not are_equal_types(first_datatype(), second_datatype()): binary_op_result_column = pandas_lit(False) else: @@ -409,7 +498,7 @@ def compute_binary_op_between_snowpark_columns( return SnowparkPandasColumn( snowpark_column=binary_op_result_column, - snowpark_pandas_type=None, + snowpark_pandas_type=snowpark_pandas_type, ) @@ -423,6 +512,10 @@ def are_equal_types(type1: DataType, type2: DataType) -> bool: Returns: True if given types are equal, False otherwise. """ + if isinstance(type1, TimedeltaType) and not isinstance(type2, TimedeltaType): + return False + if isinstance(type2, TimedeltaType) and not isinstance(type1, TimedeltaType): + return False if isinstance(type1, _IntegralType) and isinstance(type2, _IntegralType): return True if isinstance(type1, _FractionalType) and isinstance(type2, _FractionalType): diff --git a/src/snowflake/snowpark/modin/plugin/_internal/type_utils.py b/src/snowflake/snowpark/modin/plugin/_internal/type_utils.py index 1fd252bf7e0..261903be0f7 100644 --- a/src/snowflake/snowpark/modin/plugin/_internal/type_utils.py +++ b/src/snowflake/snowpark/modin/plugin/_internal/type_utils.py @@ -267,6 +267,8 @@ def to_pandas(cls, s: DataType) -> Union[np.dtype, ExtensionDtype]: return np.dtype("int64") if s.scale == 0 else np.dtype("float64") if isinstance(s, TimestampType): return np.dtype("datetime64[ns]") + if isinstance(s, TimedeltaType): + return np.dtype("timedelta64[ns]") # We also need to treat parameterized types correctly if isinstance(s, (StringType, ArrayType, MapType, GeographyType)): return np.dtype(np.object_) @@ -316,12 +318,12 @@ def column_astype( isinstance(from_sf_type, TimestampType) and from_sf_type.tz == TimestampTimeZone.LTZ ): - # treat TIMESTAMPT_LTZ columns as same as TIMESTAMPT_TZ + # treat TIMESTAMP_LTZ columns as same as TIMESTAMP_TZ curr_col = builtin("to_timestamp_tz")(curr_col) if isinstance(to_sf_type, TimestampType): assert to_sf_type.tz != TimestampTimeZone.LTZ, ( - "Cast to TIMESTAMPT_LTZ is not supported in astype since " + "Cast to TIMESTAMP_LTZ is not supported in astype since " "Snowpark pandas API maps tz aware datetime to TIMESTAMP_TZ" ) # convert to timestamp diff --git a/src/snowflake/snowpark/modin/plugin/compiler/snowflake_query_compiler.py b/src/snowflake/snowpark/modin/plugin/compiler/snowflake_query_compiler.py index 31904a58b65..7e6336c397e 100644 --- a/src/snowflake/snowpark/modin/plugin/compiler/snowflake_query_compiler.py +++ b/src/snowflake/snowpark/modin/plugin/compiler/snowflake_query_compiler.py @@ -1730,7 +1730,6 @@ def set_index_from_series( Returns: The new SnowflakeQueryCompiler after the set_index operation """ - self._raise_not_implemented_error_for_timedelta() assert ( len(key._modin_frame.data_column_pandas_labels) == 1 @@ -1761,6 +1760,7 @@ def set_index_from_series( new_index_ids = result_column_mapper.map_right_quoted_identifiers( other_frame.data_column_snowflake_quoted_identifiers ) + new_index_snowpark_types = other_frame.cached_data_column_snowpark_pandas_types if append: new_index_labels = ( new_internal_frame.index_column_pandas_labels + new_index_labels @@ -1769,6 +1769,10 @@ def set_index_from_series( new_internal_frame.index_column_snowflake_quoted_identifiers + new_index_ids ) + new_index_snowpark_types = ( + self_frame.cached_index_column_snowpark_pandas_types + + new_index_snowpark_types + ) new_internal_frame = InternalFrame.create( ordered_dataframe=new_internal_frame.ordered_dataframe, data_column_pandas_labels=self_frame.data_column_pandas_labels, @@ -1778,8 +1782,8 @@ def set_index_from_series( ), index_column_pandas_labels=new_index_labels, index_column_snowflake_quoted_identifiers=new_index_ids, - data_column_types=None, - index_column_types=None, + data_column_types=self_frame.cached_data_column_snowpark_pandas_types, + index_column_types=new_index_snowpark_types, ) return SnowflakeQueryCompiler(new_internal_frame) @@ -5882,7 +5886,6 @@ def set_index_from_columns( Returns: A new QueryCompiler instance with updated index. """ - self._raise_not_implemented_error_for_timedelta() index_column_pandas_labels = keys index_column_snowflake_quoted_identifiers = [] @@ -5910,6 +5913,16 @@ def set_index_from_columns( self._modin_frame.data_column_snowflake_quoted_identifiers ) + id_to_type = ( + self._modin_frame.snowflake_quoted_identifier_to_snowpark_pandas_type + ) + index_column_snowpark_pandas_types = [ + id_to_type.get(id) for id in index_column_snowflake_quoted_identifiers + ] + data_column_snowpark_pandas_types = [ + id_to_type.get(id) for id in data_column_snowflake_quoted_identifiers + ] + # Generate aliases for new index columns if # 1. 'keys' are also kept as data columns, or # 2. 'keys' have duplicates. @@ -5944,6 +5957,10 @@ def set_index_from_columns( self._modin_frame.index_column_snowflake_quoted_identifiers + index_column_snowflake_quoted_identifiers ) + index_column_snowpark_pandas_types = ( + self._modin_frame.cached_index_column_snowpark_pandas_types + + index_column_snowpark_pandas_types + ) frame = InternalFrame.create( ordered_dataframe=ordered_dataframe, @@ -5952,8 +5969,8 @@ def set_index_from_columns( data_column_pandas_index_names=self._modin_frame.data_column_pandas_index_names, data_column_pandas_labels=data_column_pandas_labels, data_column_snowflake_quoted_identifiers=data_column_snowflake_quoted_identifiers, - data_column_types=None, - index_column_types=None, + data_column_types=data_column_snowpark_pandas_types, + index_column_types=index_column_snowpark_pandas_types, ) return SnowflakeQueryCompiler(frame) @@ -8778,7 +8795,15 @@ def astype( to_sf_type = TypeMapper.to_snowflake(to_dtype) from_dtype = col_dtypes_curr[label] from_sf_type = self._modin_frame.get_snowflake_type(id) - if is_astype_type_error(from_sf_type, to_sf_type): + if isinstance(from_sf_type, StringType) and isinstance( + to_sf_type, TimedeltaType + ): + # Raise NotImplementedError as there is no Snowflake SQL function converting + # string (e.g. 1 day, 3 hours, 2 minutes) to Timedelta + ErrorMessage.not_implemented( + f"dtype {pandas_dtype(from_dtype)} cannot be converted to {pandas_dtype(to_dtype)}" + ) + elif is_astype_type_error(from_sf_type, to_sf_type): raise TypeError( f"dtype {pandas_dtype(from_dtype)} cannot be converted to {pandas_dtype(to_dtype)}" ) @@ -10255,13 +10280,16 @@ def drop( data_column_labels = [] data_column_identifiers = [] - for label, identifiers in zip( + data_column_snowpark_pandas_types = [] + for label, identifiers, type in zip( frame.data_column_pandas_labels, frame.data_column_snowflake_quoted_identifiers, + frame.cached_data_column_snowpark_pandas_types, ): if label not in data_column_labels_to_drop: data_column_labels.append(label) data_column_identifiers.append(identifiers) + data_column_snowpark_pandas_types.append(type) frame = InternalFrame.create( ordered_dataframe=frame.ordered_dataframe, @@ -10270,8 +10298,8 @@ def drop( data_column_pandas_labels=data_column_labels, data_column_snowflake_quoted_identifiers=data_column_identifiers, data_column_pandas_index_names=frame.data_column_pandas_index_names, - data_column_types=None, - index_column_types=None, + data_column_types=data_column_snowpark_pandas_types, + index_column_types=frame.cached_index_column_snowpark_pandas_types, ) frame = frame.select_active_columns() @@ -17257,17 +17285,27 @@ def compare( other._modin_frame.data_column_snowflake_quoted_identifiers, self._modin_frame.data_column_pandas_labels, ): + left_identiifer = result_column_mapper.left_quoted_identifiers_map[ + left_identifier + ] + right_identifier = result_column_mapper.right_quoted_identifiers_map[ + right_identifier + ] + op_result = compute_binary_op_between_snowpark_columns( + op="equal_null", + first_operand=col(left_identifier), + first_datatype=functools.partial( + lambda col: result_frame.get_snowflake_type(col), left_identiifer + ), + second_operand=col(right_identifier), + second_datatype=functools.partial( + lambda col: result_frame.get_snowflake_type(col), right_identifier + ), + ) binary_op_result = binary_op_result.append_column( str(left_pandas_label) + "_comparison_result", - col( - result_column_mapper.left_quoted_identifiers_map[left_identifier] - ).equal_null( - col( - result_column_mapper.right_quoted_identifiers_map[ - right_identifier - ] - ) - ), + op_result.snowpark_column, + op_result.snowpark_pandas_type, ) """ >>> SnowflakeQueryCompiler(binary_op_result).to_pandas() @@ -17345,28 +17383,48 @@ def compare( new_pandas_labels = [] new_values = [] column_index_tuples = [] + column_types = [] for ( pandas_column_value, pandas_label, left_identifier, right_identifier, column_only_contains_matches, + left_type, + right_type, ) in zip( self.columns, filtered_binary_op_result.data_column_pandas_labels, self._modin_frame.data_column_snowflake_quoted_identifiers, other._modin_frame.data_column_snowflake_quoted_identifiers, all_rows_match_frame.iloc[:, 0].values, + self._modin_frame.cached_data_column_snowpark_pandas_types, + other._modin_frame.cached_data_column_snowpark_pandas_types, ): # Drop columns that only contain matches. if column_only_contains_matches: continue - cols_equal = col( - result_column_mapper.left_quoted_identifiers_map[left_identifier] - ).equal_null( - col(result_column_mapper.right_quoted_identifiers_map[right_identifier]) - ) + left_mappped_identifier = result_column_mapper.left_quoted_identifiers_map[ + left_identifier + ] + right_mapped_identifier = result_column_mapper.right_quoted_identifiers_map[ + right_identifier + ] + + cols_equal = compute_binary_op_between_snowpark_columns( + op="equal_null", + first_operand=col(left_mappped_identifier), + first_datatype=functools.partial( + lambda col: result_frame.get_snowflake_type(col), + left_mappped_identifier, + ), + second_operand=col(right_mapped_identifier), + second_datatype=functools.partial( + lambda col: result_frame.get_snowflake_type(col), + right_mapped_identifier, + ), + ).snowpark_column # Add a column containing the values from `self`, but replace # matching values with null. @@ -17375,11 +17433,7 @@ def compare( iff( condition=cols_equal, expr1=pandas_lit(np.nan), - expr2=col( - result_column_mapper.left_quoted_identifiers_map[ - left_identifier - ] - ), + expr2=col(left_mappped_identifier), ) ) @@ -17390,11 +17444,7 @@ def compare( iff( condition=cols_equal, expr1=pandas_lit(np.nan), - expr2=col( - result_column_mapper.right_quoted_identifiers_map[ - right_identifier - ] - ), + expr2=col(right_mapped_identifier), ) ) @@ -17408,8 +17458,13 @@ def compare( column_index_tuples.append((pandas_column_value, "self")) column_index_tuples.append((pandas_column_value, "other")) + column_types.append(left_type) + column_types.append(right_type) + result = SnowflakeQueryCompiler( - filtered_binary_op_result.project_columns(new_pandas_labels, new_values) + filtered_binary_op_result.project_columns( + new_pandas_labels, new_values, column_types + ) ).set_columns( # TODO(SNOW-1510921): fix the levels and inferred_type of the # result's MultiIndex once we can pass the levels correctly through diff --git a/src/snowflake/snowpark/modin/plugin/extensions/index.py b/src/snowflake/snowpark/modin/plugin/extensions/index.py index 21692a228ec..bbd415536af 100644 --- a/src/snowflake/snowpark/modin/plugin/extensions/index.py +++ b/src/snowflake/snowpark/modin/plugin/extensions/index.py @@ -42,6 +42,7 @@ is_integer_dtype, is_numeric_dtype, is_object_dtype, + is_timedelta64_dtype, pandas_dtype, ) from pandas.core.dtypes.inference import is_hashable @@ -112,21 +113,30 @@ def __new__( from snowflake.snowpark.modin.plugin.extensions.datetime_index import ( DatetimeIndex, ) + from snowflake.snowpark.modin.plugin.extensions.timedelta_index import ( + TimedeltaIndex, + ) if query_compiler: dtype = query_compiler.index_dtypes[0] - if dtype == np.dtype("datetime64[ns]"): + if is_datetime64_any_dtype(dtype): return DatetimeIndex(query_compiler=query_compiler) + if is_timedelta64_dtype(dtype): + return TimedeltaIndex(query_compiler=query_compiler) elif isinstance(data, BasePandasDataset): if data.ndim != 1: raise ValueError("Index data must be 1 - dimensional") dtype = data.dtype - if dtype == np.dtype("datetime64[ns]"): - return DatetimeIndex(data, dtype, copy, name, tupleize_cols) + if is_datetime64_any_dtype(dtype): + return DatetimeIndex(data, dtype=dtype, copy=copy, name=name) + if is_timedelta64_dtype(dtype): + return TimedeltaIndex(data, dtype=dtype, copy=copy, name=name) else: index = native_pd.Index(data, dtype, copy, name, tupleize_cols) if isinstance(index, native_pd.DatetimeIndex): return DatetimeIndex(data) + if isinstance(index, native_pd.TimedeltaIndex): + return TimedeltaIndex(data) return object.__new__(cls) def __init__( @@ -252,9 +262,13 @@ def __getattr__(self, key: str) -> Any: def _binary_ops(self, method: str, other: Any) -> Index: if isinstance(other, Index): other = other.to_series().reset_index(drop=True) - return self.__constructor__( - self.to_series().reset_index(drop=True).__getattr__(method)(other) - ) + series = self.to_series().reset_index(drop=True).__getattr__(method)(other) + qc = series._query_compiler + qc = qc.set_index_from_columns(qc.columns, include_index=False) + # Use base constructor to ensure that the correct type is returned. + idx = Index(query_compiler=qc) + idx.name = series.name + return idx def _unary_ops(self, method: str) -> Index: return self.__constructor__( diff --git a/src/snowflake/snowpark/modin/plugin/extensions/pd_overrides.py b/src/snowflake/snowpark/modin/plugin/extensions/pd_overrides.py index 3515baaee3a..5d61bc95694 100644 --- a/src/snowflake/snowpark/modin/plugin/extensions/pd_overrides.py +++ b/src/snowflake/snowpark/modin/plugin/extensions/pd_overrides.py @@ -44,9 +44,13 @@ DatetimeIndex, ) from snowflake.snowpark.modin.plugin.extensions.index import Index # noqa: F401 +from snowflake.snowpark.modin.plugin.extensions.timedelta_index import ( # noqa: F401 + TimedeltaIndex, +) register_pd_accessor("Index")(Index) register_pd_accessor("DatetimeIndex")(DatetimeIndex) +register_pd_accessor("TimedeltaIndex")(TimedeltaIndex) @_inherit_docstrings(native_pd.read_csv, apilink="pandas.read_csv") diff --git a/src/snowflake/snowpark/modin/plugin/extensions/timedelta_index.py b/src/snowflake/snowpark/modin/plugin/extensions/timedelta_index.py new file mode 100644 index 00000000000..7facf4acefd --- /dev/null +++ b/src/snowflake/snowpark/modin/plugin/extensions/timedelta_index.py @@ -0,0 +1,388 @@ +# +# Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved. +# + +# Licensed to Modin Development Team under one or more contributor license agreements. +# See the NOTICE file distributed with this work for additional information regarding +# copyright ownership. The Modin Development Team licenses this file to you under the +# Apache License, Version 2.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific language +# governing permissions and limitations under the License. + +# Code in this file may constitute partial or total reimplementation, or modification of +# existing code originally distributed by the Modin project, under the Apache License, +# Version 2.0. + +""" +Module houses ``TimedeltaIndex`` class, that is distributed version of +``pandas.TimedeltaIndex``. +""" + +from __future__ import annotations + +import numpy as np +import pandas as native_pd +from pandas._libs import lib +from pandas._typing import ArrayLike, AxisInt, Dtype, Frequency, Hashable +from pandas.core.dtypes.common import is_timedelta64_dtype + +from snowflake.snowpark.modin.pandas import DataFrame, Series +from snowflake.snowpark.modin.plugin.compiler.snowflake_query_compiler import ( + SnowflakeQueryCompiler, +) +from snowflake.snowpark.modin.plugin.extensions.index import Index +from snowflake.snowpark.modin.plugin.utils.error_message import ( + timedelta_index_not_implemented, +) + +_CONSTRUCTOR_DEFAULTS = { + "unit": lib.no_default, + "freq": lib.no_default, + "dtype": None, + "copy": False, + "name": None, +} + + +class TimedeltaIndex(Index): + + # Equivalent index type in native pandas + _NATIVE_INDEX_TYPE = native_pd.TimedeltaIndex + + def __new__(cls, *args, **kwargs): + """ + Create new instance of TimedeltaIndex. This overrides behavior of Index.__new__. + Args: + *args: arguments. + **kwargs: keyword arguments. + + Returns: + New instance of TimedeltaIndex. + """ + return object.__new__(cls) + + def __init__( + self, + data: ArrayLike | native_pd.Index | Series | None = None, + unit: str | lib.NoDefault = _CONSTRUCTOR_DEFAULTS["unit"], + freq: Frequency | lib.NoDefault = _CONSTRUCTOR_DEFAULTS["freq"], + dtype: Dtype | None = _CONSTRUCTOR_DEFAULTS["dtype"], + copy: bool = _CONSTRUCTOR_DEFAULTS["copy"], + name: Hashable | None = _CONSTRUCTOR_DEFAULTS["name"], + query_compiler: SnowflakeQueryCompiler = None, + ) -> None: + """ + Immutable Index of timedelta64 data. + + Represented internally as int64, and scalars returned Timedelta objects. + + Parameters + ---------- + data : array-like (1-dimensional), optional + Optional timedelta-like data to construct index with. + unit : {'D', 'h', 'm', 's', 'ms', 'us', 'ns'}, optional + The unit of ``data``. + + .. deprecated:: 2.2.0 + Use ``pd.to_timedelta`` instead. + + freq : str or pandas offset object, optional + One of pandas date offset strings or corresponding objects. The string + ``'infer'`` can be passed in order to set the frequency of the index as + the inferred frequency upon creation. + dtype : numpy.dtype or str, default None + Valid ``numpy`` dtypes are ``timedelta64[ns]``, ``timedelta64[us]``, + ``timedelta64[ms]``, and ``timedelta64[s]``. + copy : bool + Make a copy of input array. + name : object + Name to be stored in the index. + + Examples + -------- + >>> pd.TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days']) + TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'], dtype='timedelta64[ns]') + + We can also let pandas infer the frequency when possible. + + >>> pd.TimedeltaIndex(np.arange(5) * 24 * 3600 * 1e9, freq='infer') + TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'], dtype='timedelta64[ns]') + """ + if query_compiler: + # Raise error if underlying type is not a Timedelta type. + current_dtype = query_compiler.index_dtypes[0] + if not is_timedelta64_dtype(current_dtype): + raise ValueError( + f"TimedeltaIndex can only be created from a query compiler with TimedeltaType, found {current_dtype}" + ) + kwargs = { + "unit": unit, + "freq": freq, + "dtype": dtype, + "copy": copy, + "name": name, + } + self._init_index(data, _CONSTRUCTOR_DEFAULTS, query_compiler, **kwargs) + + @timedelta_index_not_implemented() + @property + def days(self) -> Index: + """ + Number of days for each element. + + Returns + ------- + An Index with the days component of the timedelta. + + Examples + -------- + >>> idx = pd.to_timedelta(["0 days", "10 days", "20 days"]) # doctest: +SKIP + >>> idx # doctest: +SKIP + TimedeltaIndex(['0 days', '10 days', '20 days'], + dtype='timedelta64[ns]', freq=None) + >>> idx.days # doctest: +SKIP + Index([0, 10, 20], dtype='int64') + """ + + @timedelta_index_not_implemented() + @property + def seconds(self) -> Index: + """ + Number of seconds (>= 0 and less than 1 day) for each element. + + Returns + ------- + An Index with seconds component of the timedelta. + + Examples + -------- + >>> idx = pd.to_timedelta([1, 2, 3], unit='s') # doctest: +SKIP + >>> idx # doctest: +SKIP + TimedeltaIndex(['0 days 00:00:01', '0 days 00:00:02', '0 days 00:00:03'], + dtype='timedelta64[ns]', freq=None) + >>> idx.seconds # doctest: +SKIP + Index([1, 2, 3], dtype='int32') + """ + + @timedelta_index_not_implemented() + @property + def microseconds(self) -> Index: + """ + Number of microseconds (>= 0 and less than 1 second) for each element. + + Returns + ------- + An Index with microseconds component of the timedelta. + + Examples + -------- + >>> idx = pd.to_timedelta([1, 2, 3], unit='us') # doctest: +SKIP + >>> idx # doctest: +SKIP + TimedeltaIndex(['0 days 00:00:00.000001', '0 days 00:00:00.000002', + '0 days 00:00:00.000003'], + dtype='timedelta64[ns]', freq=None) + >>> idx.microseconds # doctest: +SKIP + Index([1, 2, 3], dtype='int32') + """ + + @timedelta_index_not_implemented() + @property + def nanoseconds(self) -> Index: + """ + Number of nonoseconds (>= 0 and less than 1 microsecond) for each element. + + Returns + ------- + An Index with nanoseconds compnent of the timedelta. + + Examples + -------- + >>> idx = pd.to_timedelta([1, 2, 3], unit='ns') # doctest: +SKIP + >>> idx # doctest: +SKIP + TimedeltaIndex(['0 days 00:00:00.000000001', '0 days 00:00:00.000000002', + '0 days 00:00:00.000000003'], + dtype='timedelta64[ns]', freq=None) + >>> idx.nanoseconds # doctest: +SKIP + Index([1, 2, 3], dtype='int32') + """ + + @timedelta_index_not_implemented() + @property + def components(self) -> DataFrame: + """ + Return a DataFrame of the individual resolution components of the Timedeltas. + + The components (days, hours, minutes seconds, milliseconds, microseconds, + nanoseconds) are returned as columns in a DataFrame. + + Returns + ------- + A DataFrame + + Examples + -------- + >>> idx = pd.to_timedelta(['1 day 3 min 2 us 42 ns']) # doctest: +SKIP + >>> idx # doctest: +SKIP + TimedeltaIndex(['1 days 00:03:00.000002042'], + dtype='timedelta64[ns]', freq=None) + >>> idx.components # doctest: +SKIP + days hours minutes seconds milliseconds microseconds nanoseconds + 0 1 0 3 0 0 2 42 + """ + + @timedelta_index_not_implemented() + @property + def inferred_freq(self) -> str | None: + """ + Tries to return a string representing a frequency generated by infer_freq. + + Returns None if it can't autodetect the frequency. + + Examples + -------- + >>> idx = pd.to_timedelta(["0 days", "10 days", "20 days"]) # doctest: +SKIP + >>> idx # doctest: +SKIP + TimedeltaIndex(['0 days', '10 days', '20 days'], + dtype='timedelta64[ns]', freq=None) + >>> idx.inferred_freq # doctest: +SKIP + '10D' + """ + + @timedelta_index_not_implemented() + def round(self, freq: Frequency) -> TimedeltaIndex: + """ + Perform round operation on the data to the specified `freq`. + + Parameters + ---------- + freq : str or Offset + The frequency level to round the index to. Must be a fixed + frequency like 'S' (second) not 'ME' (month end). See + frequency aliases for a list of possible `freq` values. + + Returns + ------- + TimedeltaIndex with round values. + + Raises + ------ + ValueError if the `freq` cannot be converted. + """ + + @timedelta_index_not_implemented() + def floor(self, freq: Frequency) -> TimedeltaIndex: + """ + Perform floor operation on the data to the specified `freq`. + + Parameters + ---------- + freq : str or Offset + The frequency level to floor the index to. Must be a fixed + frequency like 'S' (second) not 'ME' (month end). See + frequency aliases for a list of possible `freq` values. + + Returns + ------- + TimedeltaIndex with floor values. + + Raises + ------ + ValueError if the `freq` cannot be converted. + """ + + @timedelta_index_not_implemented() + def ceil(self, freq: Frequency) -> TimedeltaIndex: + """ + Perform ceil operation on the data to the specified `freq`. + + Parameters + ---------- + freq : str or Offset + The frequency level to ceil the index to. Must be a fixed + frequency like 'S' (second) not 'ME' (month end). See + frequency aliases for a list of possible `freq` values. + + Returns + ------- + TimedeltaIndex with ceil values. + + Raises + ------ + ValueError if the `freq` cannot be converted. + """ + + @timedelta_index_not_implemented() + def to_pytimedelta(self) -> np.ndarray: + """ + Return an ndarray of datetime.timedelta objects. + + Returns + ------- + numpy.ndarray + + Examples + -------- + >>> idx = pd.to_timedelta([1, 2, 3], unit='D') # doctest: +SKIP + >>> idx # doctest: +SKIP + TimedeltaIndex(['1 days', '2 days', '3 days'], + dtype='timedelta64[ns]', freq=None) + >>> idx.to_pytimedelta() # doctest: +SKIP + array([datetime.timedelta(days=1), datetime.timedelta(days=2), + datetime.timedelta(days=3)], dtype=object) + """ + + @timedelta_index_not_implemented() + def mean( + self, *, skipna: bool = True, axis: AxisInt | None = 0 + ) -> native_pd.Timestamp: + """ + Return the mean value of the Array. + + Parameters + ---------- + skipna : bool, default True + Whether to ignore any NaT elements. + axis : int, optional, default 0 + + Returns + ------- + scalar Timestamp + + See Also + -------- + numpy.ndarray.mean : Returns the average of array elements along a given axis. + Series.mean : Return the mean value in a Series. + + Notes + ----- + mean is only defined for Datetime and Timedelta dtypes, not for Period. + """ + + @timedelta_index_not_implemented() + def as_unit(self, unit: str) -> TimedeltaIndex: + """ + Convert to a dtype with the given unit resolution. + + Parameters + ---------- + unit : {'s', 'ms', 'us', 'ns'} + + Returns + ------- + DatetimeIndex + + Examples + -------- + >>> idx = pd.to_timedelta(['1 day 3 min 2 us 42 ns']) # doctest: +SKIP + >>> idx # doctest: +SKIP + TimedeltaIndex(['1 days 00:03:00.000002042'], + dtype='timedelta64[ns]', freq=None) + >>> idx.as_unit('s') # doctest: +SKIP + TimedeltaIndex(['1 days 00:03:00'], dtype='timedelta64[s]', freq=None) + """ diff --git a/src/snowflake/snowpark/modin/plugin/utils/error_message.py b/src/snowflake/snowpark/modin/plugin/utils/error_message.py index 1e832450579..7fc86152c63 100644 --- a/src/snowflake/snowpark/modin/plugin/utils/error_message.py +++ b/src/snowflake/snowpark/modin/plugin/utils/error_message.py @@ -143,6 +143,10 @@ def raise_not_implemented_method_error( decorating_functions=False, attribute_prefix="DatetimeIndex" ) +timedelta_index_not_implemented = _make_not_implemented_decorator( + decorating_functions=False, attribute_prefix="TimedeltaIndex" +) + pandas_module_level_function_not_implemented = _make_not_implemented_decorator( decorating_functions=True, attribute_prefix="pd" ) diff --git a/tests/integ/modin/binary/test_timedelta.py b/tests/integ/modin/binary/test_timedelta.py index 632243664d7..d9fa20b1a40 100644 --- a/tests/integ/modin/binary/test_timedelta.py +++ b/tests/integ/modin/binary/test_timedelta.py @@ -11,8 +11,10 @@ import pytest import snowflake.snowpark.modin.plugin # noqa: F401 +from snowflake.snowpark.exceptions import SnowparkSQLException from tests.integ.modin.sql_counter import sql_count_checker from tests.integ.modin.utils import ( + assert_series_equal, assert_snowpark_pandas_equals_to_pandas_without_dtypecheck, create_test_dfs, create_test_series, @@ -109,12 +111,37 @@ def timestamp_scalar(request): return request.param +@pytest.fixture( + params=[ + pd.Timedelta("10 days 23:59:59.123456789"), + datetime.timedelta(microseconds=1), + datetime.timedelta(microseconds=2), + pd.Timedelta(nanoseconds=1), + pd.Timedelta(nanoseconds=2), + pd.Timedelta(nanoseconds=3), + pd.Timedelta(days=1), + pd.Timedelta(days=1, hours=1), + pd.Timedelta(days=10), + ] +) +def timedelta_scalar_positive(request): + return request.param + + @pytest.fixture( params=[ pd.Timedelta("10 days 23:59:59.123456789"), pd.Timedelta("-10 days 23:59:59.123456789"), datetime.timedelta(days=-10, hours=23), datetime.timedelta(microseconds=1), + datetime.timedelta(microseconds=2), + pd.Timedelta(nanoseconds=1), + pd.Timedelta(nanoseconds=2), + pd.Timedelta(nanoseconds=3), + pd.Timedelta(days=1), + pd.Timedelta(days=1, hours=1), + pd.Timedelta(days=10), + pd.Timedelta(days=-1), ] ) def timedelta_scalar(request): @@ -134,6 +161,36 @@ def timedelta_dataframes_1() -> tuple[pd.DataFrame, native_pd.DataFrame]: ) +@pytest.fixture +def timedelta_dataframes_postive_no_nulls_1_2x2() -> tuple[ + pd.DataFrame, native_pd.DataFrame +]: + return create_test_dfs( + [ + [pd.Timedelta(days=1), pd.Timedelta(days=4)], + [ + pd.Timedelta(days=2), + pd.Timedelta(days=3), + ], + ] + ) + + +@pytest.fixture +def timedelta_dataframes_with_negatives_no_nulls_1_2x2() -> tuple[ + pd.DataFrame, native_pd.DataFrame +]: + return create_test_dfs( + [ + [pd.Timedelta(days=1), pd.Timedelta(days=4)], + [ + pd.Timedelta(days=2), + pd.Timedelta(days=-3), + ], + ] + ) + + @pytest.fixture def timedelta_series_1() -> tuple[pd.Series, native_pd.Series]: return create_test_series( @@ -148,10 +205,332 @@ def timedelta_series_1() -> tuple[pd.Series, native_pd.Series]: ) +@pytest.fixture +def timedelta_series_positive_no_nulls_1_length_6() -> tuple[ + pd.Series, native_pd.Series +]: + return create_test_series( + [ + pd.Timedelta(days=1), + pd.Timedelta(days=2), + pd.Timedelta(days=3), + pd.Timedelta(days=4), + pd.Timedelta(days=5), + pd.Timedelta(days=6), + ] + ) + + +@pytest.fixture +def timedelta_series_no_nulls_2_length_6() -> tuple[pd.Series, native_pd.Series]: + return create_test_series( + [ + pd.Timedelta(microseconds=7), + pd.Timedelta(hours=6, minutes=5), + pd.Timedelta(hours=4, minutes=3), + pd.Timedelta(hours=2, minutes=1), + pd.Timedelta(hours=8, minutes=9), + pd.Timedelta(hours=9, minutes=8), + ] + ) + + +@pytest.fixture +def timedelta_series_no_nulls_3_length_2() -> tuple[pd.Series, native_pd.Series]: + return create_test_series( + [ + pd.Timedelta(microseconds=7), + pd.Timedelta(hours=6, minutes=5), + ] + ) + + +@pytest.fixture( + params=[ + "sub", + "rsub", + "add", + "radd", + "div", + "rdiv", + "truediv", + "rtruediv", + "floordiv", + "rfloordiv", + "mod", + "rmod", + "eq", + "ne", + "gt", + "lt", + "ge", + "le", + ] +) +def op_between_timedeltas(request) -> list[str]: + """Valid operations between timedeltas.""" + return request.param + + +class TestInvalid: + """ + Test invalid binary operations, e.g. subtracting a timestamp from a timedelta. + + For simplicity, check these cases for operations between dataframes and + scalars only. + """ + + @sql_count_checker(query_count=0) + def test_timedelta_scalar_minus_timestamp_dataframe(self): + eval_snowpark_pandas_result( + *create_test_dfs([datetime.datetime(year=2024, month=8, day=21)]), + lambda df: pd.Timedelta(1) - df, + expect_exception=True, + expect_exception_type=TypeError, + expect_exception_match=re.escape( + "bad operand type for unary -: 'DatetimeArray" + ), + ) + + @sql_count_checker(query_count=0) + @pytest.mark.parametrize( + "operation,error_symbol", + [("__or__", "|"), ("__ror__", "|"), ("__and__", "&"), ("__rand__", "&")], + ) + def test_timedelta_dataframe_bitwise_operation_with_timedelta_scalar( + self, operation, timedelta_dataframes_1, error_symbol + ): + eval_snowpark_pandas_result( + *timedelta_dataframes_1, + lambda df: getattr(df, operation)(pd.Timedelta(2)), + expect_exception=True, + # pandas exception depends on the input types and is something like + # "unsupported operand type(s) for &: 'Timedelta' and 'TimedeltaArray'", + # but Snowpwark pandas always gives the same exception. + assert_exception_equal=False, + expect_exception_type=TypeError, + expect_exception_match=re.escape( + f"unsupported operand type for {error_symbol}: Timedelta" + ), + ) + + @sql_count_checker(query_count=0) + @pytest.mark.parametrize("operation", ["pow", "rpow"]) + def test_timedelta_dataframe_exponentiation_with_timedelta_scalar( + self, operation, timedelta_dataframes_1 + ): + eval_snowpark_pandas_result( + *timedelta_dataframes_1, + lambda df: getattr(df, operation)(pd.Timedelta(2)), + expect_exception=True, + # pandas exception depends on the input types and is something + # like "cannot perform __rpow__ with this index type: + # TimedeltaArray", but Snowpwark pandas always gives the same + # exception. + assert_exception_equal=False, + expect_exception_type=TypeError, + expect_exception_match=re.escape( + "unsupported operand type for **: Timedelta" + ), + ) + + @sql_count_checker(query_count=0) + @pytest.mark.parametrize("operation", ["mul", "rmul"]) + def test_timedelta_dataframe_multiplied_by_timedelta_scalar_invalid( + self, operation, timedelta_dataframes_1 + ): + eval_snowpark_pandas_result( + *timedelta_dataframes_1, + lambda df: getattr(df, operation)(pd.Timedelta(2)), + expect_exception=True, + expect_exception_type=np.core._exceptions._UFuncBinaryResolutionError, + expect_exception_match=re.escape( + "ufunc 'multiply' cannot use operands with types dtype(' native_pd.DataFrame: class TestDefaultParameters: - @sql_count_checker(query_count=QUERY_COUNT, join_count=JOIN_COUNT) + @sql_count_checker( + query_count=QUERY_COUNT_MULTI_LEVEL_INDEX, + join_count=JOIN_COUNT_MULTI_LEVEL_INDEX, + ) def test_no_diff(self, base_df): other_df = base_df.copy() eval_snowpark_pandas_result( @@ -72,6 +85,56 @@ def test_no_diff(self, base_df): # In snowpark pandas, the column index of the empty resulting frame # has the correct values and names, but the incorrect inferred_type # for some of its levels. Ignore that bug for now. + # TODO(SNOW-1510921): fix the bug. + check_index_type=False, + check_column_type=False, + ) + + @sql_count_checker( + # no joins because we can skip the joins when comparing df to df.copy() + query_count=QUERY_COUNT_SINGLE_LEVEL_INDEX, + join_count=0, + ) + def test_no_diff_timedelta(self): + eval_snowpark_pandas_result( + *create_test_dfs([pd.Timedelta(1)]), + lambda df: df.compare(df.copy()), + check_index_type=False, + check_column_type=False, + ) + + @sql_count_checker( + query_count=QUERY_COUNT_SINGLE_LEVEL_INDEX, + join_count=JOIN_COUNT_SINGLE_LEVEL_INDEX, + ) + def test_one_diff_timedelta(self): + base_snow_df, base_pandas_df = create_test_dfs( + [[pd.Timedelta(1), pd.Timedelta(2)]] + ) + other_snow_df, other_pandas_df = create_test_dfs( + [[pd.Timedelta(1), pd.Timedelta(3)]] + ) + eval_snowpark_pandas_result( + (base_snow_df, other_snow_df), + (base_pandas_df, other_pandas_df), + lambda t: t[0].compare(t[1]), + check_index_type=False, + check_column_type=False, + ) + + @sql_count_checker( + query_count=QUERY_COUNT_SINGLE_LEVEL_INDEX, + join_count=JOIN_COUNT_SINGLE_LEVEL_INDEX, + ) + def test_timedelta_compared_with_int(self): + base_snow_df, base_pandas_df = create_test_dfs([[pd.Timedelta(1), 2]]) + other_snow_df, other_pandas_df = create_test_dfs( + [[pd.Timedelta(1), pd.Timedelta(2)]] + ) + eval_snowpark_pandas_result( + (base_snow_df, other_snow_df), + (base_pandas_df, other_pandas_df), + lambda t: t[0].compare(t[1]), check_index_type=False, check_column_type=False, ) @@ -86,7 +149,10 @@ def test_no_diff(self, base_df): ((3, 4), [201]), ], ) - @sql_count_checker(query_count=QUERY_COUNT, join_count=JOIN_COUNT) + @sql_count_checker( + query_count=QUERY_COUNT_MULTI_LEVEL_INDEX, + join_count=JOIN_COUNT_MULTI_LEVEL_INDEX, + ) def test_single_value_diff(self, base_df, position, new_value): # check that we are changing a value, so the test case is meaningful. assert not ( @@ -122,7 +188,10 @@ def test_default_index_on_both_axes(self, base_df): ), ) - @sql_count_checker(query_count=QUERY_COUNT, join_count=JOIN_COUNT) + @sql_count_checker( + query_count=QUERY_COUNT_MULTI_LEVEL_INDEX, + join_count=JOIN_COUNT_MULTI_LEVEL_INDEX, + ) def test_different_value_in_every_column_and_row(self, base_df): other_df = base_df.copy() other_df.iloc[0, 0] = "c" diff --git a/tests/integ/modin/frame/test_equals.py b/tests/integ/modin/frame/test_equals.py index e57c4180231..95b6b8ffd6f 100644 --- a/tests/integ/modin/frame/test_equals.py +++ b/tests/integ/modin/frame/test_equals.py @@ -15,6 +15,12 @@ "lhs, rhs, expected", [ ([1, 2, 3], [1, 2, 3], True), + pytest.param( + [pd.Timedelta(1), pd.Timedelta(2), pd.Timedelta(3)], + [pd.Timedelta(1), pd.Timedelta(2), pd.Timedelta(3)], + True, + id="timedelta", + ), ([1, 2, 3], [1, 2, 4], False), # different values ([1, 2, None], [1, 2, None], True), # nulls are considered equal ([1, 2, 3], [1.0, 2.0, 3.0], False), # float and integer types are not equal @@ -58,6 +64,8 @@ def test_equals_column_labels(lhs, rhs, expected): (np.float64, np.float32, True), (np.int16, "object", False), (np.int16, np.float16, False), + ("timedelta64[ns]", int, False), + ("timedelta64[ns]", float, False), ], ) @sql_count_checker(query_count=2, join_count=2) diff --git a/tests/integ/modin/index/conftest.py b/tests/integ/modin/index/conftest.py index bc26872380e..3c6362dd83c 100644 --- a/tests/integ/modin/index/conftest.py +++ b/tests/integ/modin/index/conftest.py @@ -16,6 +16,10 @@ data={"col1": [1, 2, 3], "col2": [3, 4, 5]}, index=native_pd.DatetimeIndex(["2024-01-01", "2024-02-01", "2024-03-01"]), ), + native_pd.DataFrame( + data={"col1": [1, 2, 3], "col2": [3, 4, 5]}, + index=native_pd.TimedeltaIndex(["0 days", "1 days", "3 days"]), + ), ] NATIVE_INDEX_TEST_DATA = [ @@ -36,6 +40,8 @@ tz="America/Los_Angeles", ), native_pd.DatetimeIndex([1262347200000000000, 1262347400000000000]), + native_pd.TimedeltaIndex(["0 days", "1 days", "3 days"]), + native_pd.TimedeltaIndex([100, 200, 300]), ] NATIVE_INDEX_UNIQUE_TEST_DATA = [ diff --git a/tests/integ/modin/index/test_timedelta_index_methods.py b/tests/integ/modin/index/test_timedelta_index_methods.py new file mode 100644 index 00000000000..1baafed24d2 --- /dev/null +++ b/tests/integ/modin/index/test_timedelta_index_methods.py @@ -0,0 +1,65 @@ +# +# Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved. +# + +import modin.pandas as pd +import pandas as native_pd +import pytest + +import snowflake.snowpark.modin.plugin # noqa: F401 +from tests.integ.modin.sql_counter import sql_count_checker + + +@sql_count_checker(query_count=3) +def test_timedelta_index_construction(): + # create from native pandas timedelta index. + index = native_pd.TimedeltaIndex(["1 days", "2 days", "3 days"]) + snow_index = pd.Index(index) + assert isinstance(snow_index, pd.TimedeltaIndex) + + # create from query compiler with timedelta type. + df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, index=index) + snow_index = df.index + assert isinstance(snow_index, pd.TimedeltaIndex) + + # create from snowpark pandas timedelta index. + snow_index = pd.Index(pd.TimedeltaIndex([123])) + assert isinstance(snow_index, pd.TimedeltaIndex) + + # create by subtracting datetime index from another. + date_range1 = pd.date_range("2000-01-01", periods=10, freq="h") + date_range2 = pd.date_range("2001-05-01", periods=10, freq="h") + snow_index = date_range2 - date_range1 + assert isinstance(snow_index, pd.TimedeltaIndex) + + +@sql_count_checker(query_count=0) +@pytest.mark.parametrize( + "kwargs", + [ + {"unit": "ns"}, + {"freq": "M"}, + {"dtype": "int"}, + {"copy": True}, + {"name": "abc"}, + ], +) +def test_non_default_args(kwargs): + idx = pd.TimedeltaIndex(["1 days"]) + + name = list(kwargs.keys())[0] + value = list(kwargs.values())[0] + msg = f"Non-default argument '{name}={value}' when constructing Index with query compiler" + with pytest.raises(AssertionError, match=msg): + pd.TimedeltaIndex(query_compiler=idx._query_compiler, **kwargs) + + +@pytest.mark.parametrize( + "property", ["days", "seconds", "microseconds", "nanoseconds", "inferred_freq"] +) +@sql_count_checker(query_count=0) +def test_property_not_implemented(property): + snow_index = pd.TimedeltaIndex(["1 days", "2 days"]) + msg = f"Snowpark pandas does not yet support the property TimedeltaIndex.{property}" + with pytest.raises(NotImplementedError, match=msg): + getattr(snow_index, property) diff --git a/tests/integ/modin/series/test_astype.py b/tests/integ/modin/series/test_astype.py index ff65c677c05..9c00e9a675d 100644 --- a/tests/integ/modin/series/test_astype.py +++ b/tests/integ/modin/series/test_astype.py @@ -407,8 +407,8 @@ def test_python_datetime_astype_DatetimeTZDtype(seed): @sql_count_checker(query_count=1) @pytest.mark.parametrize( "data", - [[12345678, 9], [12345678, 2.6], [True, False], [1, "2"], ["1", "2"]], - ids=["int", "float", "boolean", "object", "string"], + [[12345678, 9], [12345678, 2.6], [True, False], [1, "2"]], + ids=["int", "float", "boolean", "object"], ) def test_astype_to_timedelta(data): native_series = native_pd.Series(data) @@ -419,24 +419,31 @@ def test_astype_to_timedelta(data): @sql_count_checker(query_count=2) -def test_astype_datetime_to_timedelta_negative(): - native_series = native_pd.Series( +def test_astype_to_timedelta_negative(): + native_datetime_series = native_pd.Series( data=[pd.to_datetime("2000-01-01"), pd.to_datetime("2001-01-01")] ) - snow_series = pd.Series(native_series) + snow_datetime_series = pd.Series(native_datetime_series) with SqlCounter(query_count=0): with pytest.raises( TypeError, match=re.escape("Cannot cast DatetimeArray to dtype timedelta64[ns]"), ): - native_series.astype("timedelta64[ns]") + native_datetime_series.astype("timedelta64[ns]") with pytest.raises( TypeError, match=re.escape( "dtype datetime64[ns] cannot be converted to timedelta64[ns]" ), ): - snow_series.astype("timedelta64[ns]") + snow_datetime_series.astype("timedelta64[ns]") + with SqlCounter(query_count=0): + snow_string_series = pd.Series(data=["2 days, 3 minutes"]) + with pytest.raises( + NotImplementedError, + match=re.escape("dtype object cannot be converted to timedelta64[ns]"), + ): + snow_string_series.astype("timedelta64[ns]") @sql_count_checker(query_count=0) diff --git a/tests/integ/modin/series/test_equals.py b/tests/integ/modin/series/test_equals.py index b0f7af34b19..912726ff2f9 100644 --- a/tests/integ/modin/series/test_equals.py +++ b/tests/integ/modin/series/test_equals.py @@ -15,6 +15,12 @@ "lhs, rhs, expected", [ ([1, 2, 3], [1, 2, 3], True), + pytest.param( + [pd.Timedelta(1), pd.Timedelta(2), pd.Timedelta(3)], + [pd.Timedelta(1), pd.Timedelta(2), pd.Timedelta(3)], + True, + id="timedelta", + ), ([1, 2, None], [1, 2, None], True), # nulls are considered equal ([1, 2, 3], [1.0, 2.0, 3.0], False), # float and integer types are not equal ([1, 2, 3], ["1", "2", "3"], False), # integer and string types are not equal @@ -37,6 +43,8 @@ def test_equals_series(lhs, rhs, expected): (np.float64, np.float32, True), (np.int16, "object", False), (np.int16, np.float16, False), + ("timedelta64[ns]", int, False), + ("timedelta64[ns]", float, False), ], ) @sql_count_checker(query_count=2, join_count=2) diff --git a/tests/integ/modin/test_timedelta_ops.py b/tests/integ/modin/test_timedelta_ops.py index 2d38c1e372f..c60b91b3273 100644 --- a/tests/integ/modin/test_timedelta_ops.py +++ b/tests/integ/modin/test_timedelta_ops.py @@ -26,8 +26,8 @@ } -@sql_count_checker(query_count=0) -def test_td_case1_negative(): +@sql_count_checker(query_count=1) +def test_insert_datetime_difference_in_days(): data = TIME_DATA1 snow_df = pd.DataFrame(data) native_df = native_pd.DataFrame(data) @@ -41,80 +41,20 @@ def test_td_case1_negative(): ) / np.timedelta64(1, "D") ).round() - # TODO SNOW-1635620: remove Exception raised when TimeDelta is implemented - with pytest.raises(NotImplementedError): - snow_df["month_lag"] = ( - ( - pd.to_datetime(snow_df["CREATED_AT"], format="%Y-%m-%d %H:%M:%S") - - pd.to_datetime( - snow_df["REPORTING_DATE"], format="%Y-%m-%d", errors="coerce" - ) - ) - / np.timedelta64(1, "D") - ).round() - assert_series_equal(snow_df["month_lag"], native_df["open_lag"]) - - -@sql_count_checker(query_count=0) -def test_td_case2_negative(): - data = TIME_DATA1 - snow_df = pd.DataFrame(data) - native_df = native_pd.DataFrame(data) - native_df["open_lag"] = ( - ( - native_pd.to_datetime(native_df["CREATED_AT"], format="%Y-%m-%d %H:%M:%S") - - native_pd.to_datetime( - native_df["OPEN_DATE"], format="%Y-%m-%d", errors="coerce" - ) - ) - / np.timedelta64(1, "D") - ).round() - # TODO SNOW-1635620: remove Exception raised when TimeDelta is implemented - with pytest.raises(NotImplementedError): - snow_df["open_lag"] = ( - ( - pd.to_datetime(snow_df["CREATED_AT"], format="%Y-%m-%d %H:%M:%S") - - pd.to_datetime( - snow_df["OPEN_DATE"], format="%Y-%m-%d", errors="coerce" - ) - ) - / np.timedelta64(1, "D") - ).round() - assert_series_equal(snow_df["open_lag"], native_df["open_lag"]) - - -@sql_count_checker(query_count=0) -def test_td_case3_negative(): - data = TIME_DATA1 - snow_df = pd.DataFrame(data) - native_df = native_pd.DataFrame(data) - - native_df["close_lag"] = ( + snow_df["month_lag"] = ( ( - native_pd.to_datetime(native_df["CREATED_AT"], format="%Y-%m-%d %H:%M:%S") - - native_pd.to_datetime( - native_df["CLOSED_DATE"], format="%Y-%m-%d", errors="coerce" + pd.to_datetime(snow_df["CREATED_AT"], format="%Y-%m-%d %H:%M:%S") + - pd.to_datetime( + snow_df["REPORTING_DATE"], format="%Y-%m-%d", errors="coerce" ) ) / np.timedelta64(1, "D") ).round() - # TODO SNOW-1635620: remove Exception raised when TimeDelta is implemented - with pytest.raises(NotImplementedError): - snow_df["close_lag"] = ( - ( - pd.to_datetime(snow_df["CREATED_AT"], format="%Y-%m-%d %H:%M:%S") - - pd.to_datetime( - snow_df["CLOSED_DATE"], format="%Y-%m-%d", errors="coerce" - ) - ) - / np.timedelta64(1, "D") - ).round() - - assert_series_equal(snow_df["close_lag"], native_df["close_lag"]) + assert_series_equal(snow_df["month_lag"], native_df["month_lag"]) @sql_count_checker(query_count=1) -def test_td_case4(): +def test_insert_datetime_difference(): data = { "bl_start_ts": [Timestamp("2017-03-01T12")], "green_light_ts": [Timestamp("2017-01-07T12")], @@ -131,7 +71,7 @@ def test_td_case4(): @sql_count_checker(query_count=0) -def test_td_case5_negative(): +def test_diff_timestamp_column_to_get_timedelta_negative(): data = { "Country": ["A", "B", "C", "D", "E"], "Agreement Signing Date": [ @@ -144,7 +84,7 @@ def test_td_case5_negative(): } snow_df = pd.DataFrame(data) native_df = native_pd.DataFrame(data) - # TODO SNOW-1635620: remove Exception raised when TimeDelta is implemented + # TODO SNOW-1641729: remove Exception raised when TimeDelta is implemented with pytest.raises(SnowparkSQLException): eval_snowpark_pandas_result( snow_df, diff --git a/tests/integ/scala/test_function_suite.py b/tests/integ/scala/test_function_suite.py index a322c7d34b8..98b2bdbfeef 100644 --- a/tests/integ/scala/test_function_suite.py +++ b/tests/integ/scala/test_function_suite.py @@ -36,6 +36,7 @@ array_intersection, array_position, array_prepend, + array_remove, array_size, array_slice, array_to_string, @@ -2823,6 +2824,34 @@ def test_array_append(session): ) +@pytest.mark.skipif( + "config.getoption('local_testing_mode', default=False)", + reason="array_remove is not yet supported in local testing mode.", +) +def test_array_remove(session): + Utils.check_answer( + [ + Row("[\n 2,\n 3\n]"), + Row("[\n 6,\n 7\n]"), + ], + TestData.array1(session).select( + array_remove(array_remove(col("arr1"), lit(1)), lit(8)) + ), + sort=False, + ) + + Utils.check_answer( + [ + Row("[\n 2,\n 3\n]"), + Row("[\n 6,\n 7\n]"), + ], + TestData.array1(session).select( + array_remove(array_remove(col("arr1"), 1), lit(8)) + ), + sort=False, + ) + + @pytest.mark.skipif( "config.getoption('local_testing_mode', default=False)", reason="array_cat is not yet supported in local testing mode.", diff --git a/tests/integ/scala/test_snowflake_plan_suite.py b/tests/integ/scala/test_snowflake_plan_suite.py index 4b2f538ea40..3d9f2e22b24 100644 --- a/tests/integ/scala/test_snowflake_plan_suite.py +++ b/tests/integ/scala/test_snowflake_plan_suite.py @@ -175,9 +175,8 @@ def check_plan_queries( # the cte optimization is not kicking in when sql simplifier disabled, because # the cte_optimization_enabled is set to False when constructing the plan for df2, # and place_holder is not propogated. - # TODO (SNOW-1541096): revisit this test once the cte optimization is switched to the - # new compilation infra. - cte_applied=session.sql_simplifier_enabled, + cte_applied=session.sql_simplifier_enabled + or session._query_compilation_stage_enabled, exec_queries=df2._plan.execution_queries, ) diff --git a/tests/integ/test_cte.py b/tests/integ/test_cte.py index 6aa115afcc2..87a91deab0e 100644 --- a/tests/integ/test_cte.py +++ b/tests/integ/test_cte.py @@ -10,6 +10,7 @@ from snowflake.connector.options import installed_pandas from snowflake.snowpark import Window from snowflake.snowpark._internal.analyzer import analyzer +from snowflake.snowpark._internal.analyzer.snowflake_plan import PlanQueryType from snowflake.snowpark._internal.utils import ( TEMP_OBJECT_NAME_PREFIX, TempObjectType, @@ -35,6 +36,16 @@ ) ] +binary_operations = [ + lambda x, y: x.union_all(y), + lambda x, y: x.select("a").union(y.select("a")), + lambda x, y: x.except_(y), + lambda x, y: x.select("a").intersect(y.select("a")), + lambda x, y: x.join(y.select("a", "b"), rsuffix="_y"), + lambda x, y: x.select("a").join(y, how="outer", rsuffix="_y"), + lambda x, y: x.join(y.select("a"), how="left", rsuffix="_y"), +] + WITH = "WITH" @@ -104,18 +115,7 @@ def test_unary(session, action): check_result(session, df_action.union_all(df_action), expect_cte_optimized=True) -@pytest.mark.parametrize( - "action", - [ - lambda x, y: x.union_all(y), - lambda x, y: x.select("a").union(y.select("a")), - lambda x, y: x.except_(y), - lambda x, y: x.select("a").intersect(y.select("a")), - lambda x, y: x.join(y.select("a", "b"), rsuffix="_y"), - lambda x, y: x.select("a").join(y, how="outer", rsuffix="_y"), - lambda x, y: x.join(y.select("a"), how="left", rsuffix="_y"), - ], -) +@pytest.mark.parametrize("action", binary_operations) def test_binary(session, action): df = session.create_dataframe([[1, 2], [3, 4]], schema=["a", "b"]) check_result(session, action(df, df), expect_cte_optimized=True) @@ -138,6 +138,67 @@ def test_binary(session, action): assert len(plan_queries["post_actions"]) == 1 +@pytest.mark.parametrize("action", binary_operations) +def test_variable_binding_binary(session, action): + df1 = session.sql( + "select $1 as a, $2 as b from values (?, ?), (?, ?)", params=[1, "a", 2, "b"] + ) + df2 = session.sql( + "select $1 as a, $2 as b from values (?, ?), (?, ?)", params=[1, "c", 3, "d"] + ) + df3 = session.sql( + "select $1 as a, $2 as b from values (?, ?), (?, ?)", params=[1, "a", 2, "b"] + ) + + check_result(session, action(df1, df3), expect_cte_optimized=True) + check_result(session, action(df1, df2), expect_cte_optimized=False) + + +def test_variable_binding_multiple(session): + if not session._query_compilation_stage_enabled: + pytest.skip( + "CTE query generation without the new query generation doesn't work correctly" + ) + + df1 = session.sql( + "select $1 as a, $2 as b from values (?, ?), (?, ?)", params=[1, "a", 2, "b"] + ) + df2 = session.sql( + "select $1 as a, $2 as b from values (?, ?), (?, ?)", params=[1, "c", 3, "d"] + ) + + df_res = df1.union(df1).union(df2) + check_result(session, df_res, expect_cte_optimized=True) + plan_queries = df_res._plan.execution_queries + + assert plan_queries[PlanQueryType.QUERIES][-1].params == [ + 1, + "a", + 2, + "b", + 1, + "c", + 3, + "d", + ] + + df_res = df2.union(df1).union(df2).union(df1) + check_result(session, df_res, expect_cte_optimized=True) + plan_queries = df_res._plan.execution_queries + + assert plan_queries[PlanQueryType.QUERIES][-1].params == [ + 1, + "a", + 2, + "b", + 1, + "c", + 3, + "d", + ] + assert plan_queries[PlanQueryType.QUERIES][-1].sql.count(WITH) == 1 + + @pytest.mark.parametrize( "action", [ diff --git a/tests/integ/test_df_to_snowpark_pandas.py b/tests/integ/test_df_to_snowpark_pandas.py index 05d51b1b38a..ede9b10e85c 100644 --- a/tests/integ/test_df_to_snowpark_pandas.py +++ b/tests/integ/test_df_to_snowpark_pandas.py @@ -5,6 +5,8 @@ # Tests behavior of to_snowpark_pandas() without explicitly initializing Snowpark pandas. +import sys + import pytest from snowflake.snowpark._internal.utils import TempObjectType @@ -47,9 +49,14 @@ def test_to_snowpark_pandas_no_modin(session, tmp_table_basic): # TODO: SNOW-1552497: after upgrading to modin 0.30.1, Snowpark pandas will support # all pandas 2.2.x, and this function call will raise a ModuleNotFoundError since # modin is not installed. + match = ( + "Snowpark pandas does not support Python 3.8. Please update to Python 3.9 or later" + if sys.version_info.major == 3 and sys.version_info.minor == 8 + else "does not match the supported pandas version in Snowpark pandas" + ) with pytest.raises( RuntimeError, - match="does not match the supported pandas version in Snowpark pandas", + match=match, ): snowpark_df.to_snowpark_pandas() else: diff --git a/tests/notebooks/modin/MIMICHealthcareDemo.ipynb b/tests/notebooks/modin/MIMICHealthcareDemo.ipynb index 40e82c78d6b..3f1849e52cd 100644 --- a/tests/notebooks/modin/MIMICHealthcareDemo.ipynb +++ b/tests/notebooks/modin/MIMICHealthcareDemo.ipynb @@ -392,10 +392,17 @@ "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:snowflake.snowpark.modin.plugin.utils.warning_message:Snowpark pandas support for Timedelta is not currently available.\n" + ] + } + ], "source": [ - "# TODO SNOW-1635620: uncomment when TimeDelta is implemented\n", - "# df[\"length_of_stay\"] = (df[\"outtime\"]-df[\"intime\"])/pd.Timedelta('1 hour')" + "df[\"length_of_stay\"] = (df[\"outtime\"]-df[\"intime\"])/pd.Timedelta('1 hour')" ] }, { @@ -405,8 +412,7 @@ "metadata": {}, "outputs": [], "source": [ - "# TODO SNOW-1635620: uncomment when TimeDelta is implemented\n", - "# df[\"age\"] = df[\"intime\"].dt.year-df[\"dob\"].dt.year" + "df[\"age\"] = df[\"intime\"].dt.year-df[\"dob\"].dt.year" ] }, { @@ -426,8 +432,7 @@ }, "outputs": [], "source": [ - "# TODO SNOW-1635620: uncomment when TimeDelta is implemented\n", - "# df = df[df[\"age\"]<100]" + "df = df[df[\"age\"]<100]" ] }, { @@ -447,10 +452,37 @@ "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:snowflake.snowpark.modin.plugin.utils.warning_message:DataFrame.plot materializes data to the local machine for plotting.\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "# TODO SNOW-1635620: uncomment when TimeDelta is implemented\n", - "# df.plot(\"age\",\"length_of_stay\",kind=\"scatter\")" + "df.plot(\"age\",\"length_of_stay\",kind=\"scatter\")" ] }, { @@ -518,8 +550,8 @@ }, "outputs": [], "source": [ - "# TODO SNOW-1635620: uncomment when TimeDelta is implemented\n", - "# df[\"pre_icu_length_of_stay\"]= (df[\"intime\"]-df[\"admittime\"])/pd.Timedelta('1 day')" + "df[\"admittime\"] = pd.to_datetime(df[\"admittime\"])\n", + "df[\"pre_icu_length_of_stay\"]= (df[\"intime\"]-df[\"admittime\"])/pd.Timedelta('1 day')" ] }, { @@ -541,7 +573,7 @@ }, "outputs": [], "source": [ - "# TODO SNOW-1635620: uncomment when TimeDelta is implemented\n", + "# TODO(https://snowflakecomputing.atlassian.net/browse/SNOW-1640617): Implement Series.hist\n", "# df[\"pre_icu_length_of_stay\"].hist()" ] }, @@ -552,11 +584,18 @@ "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Percentage of ICU admissions within 1 day: 81.10%\n" + ] + } + ], "source": [ - "# TODO SNOW-1635620: uncomment when TimeDelta is implemented\n", - "# print(f\"Percentage of ICU admissions within 1 day: \\\n", - "# {len(df[df['pre_icu_length_of_stay']<1])/len(df)*100:.2f}%\")" + "print(f\"Percentage of ICU admissions within 1 day: \\\n", + " {len(df[df['pre_icu_length_of_stay']<1])/len(df)*100:.2f}%\")" ] }, { @@ -592,12 +631,12 @@ "3 HUMERAL FRACTURE\n", "4 ALCOHOLIC HEPATITIS\n", " ... \n", - "131 PERICARDIAL EFFUSION\n", - "132 ALTERED MENTAL STATUS\n", - "133 ACUTE RESPIRATORY DISTRESS SYNDROME;ACUTE RENA...\n", - "134 BRADYCARDIA\n", - "135 CHOLANGITIS\n", - "Name: diagnosis, Length: 136, dtype: object" + "122 SHORTNESS OF BREATH\n", + "123 PERICARDIAL EFFUSION\n", + "124 ACUTE RESPIRATORY DISTRESS SYNDROME;ACUTE RENA...\n", + "125 BRADYCARDIA\n", + "126 CHOLANGITIS\n", + "Name: diagnosis, Length: 127, dtype: object" ] }, "execution_count": 16, @@ -636,12 +675,12 @@ "3 HUMERAL FRACTURE\n", "4 ALCOHOLIC HEPATITIS\n", " ... \n", - "131 PERICARDIAL EFFUSION\n", - "132 ALTERED MENTAL STATUS\n", - "133 ACUTE RESPIRATORY DISTRESS SYNDROME ACUTE RENA...\n", - "134 BRADYCARDIA\n", - "135 CHOLANGITIS\n", - "Name: diagnosis, Length: 136, dtype: object" + "122 SHORTNESS OF BREATH\n", + "123 PERICARDIAL EFFUSION\n", + "124 ACUTE RESPIRATORY DISTRESS SYNDROME ACUTE RENA...\n", + "125 BRADYCARDIA\n", + "126 CHOLANGITIS\n", + "Name: diagnosis, Length: 127, dtype: object" ] }, "execution_count": 17, @@ -669,8 +708,8 @@ { "data": { "text/plain": [ - "[('SEPSIS', 10),\n", - " ('PNEUMONIA', 8),\n", + "[('SEPSIS', 9),\n", + " ('PNEUMONIA', 7),\n", " ('CONGESTIVE HEART FAILURE', 5),\n", " ('FEVER', 4),\n", " ('SHORTNESS OF BREATH', 4)]" @@ -769,8 +808,8 @@ "data": { "text/plain": [ "hospital_expire_flag\n", - "0 90\n", - "1 46\n", + "0 85\n", + "1 42\n", "Name: count, dtype: int64" ] }, @@ -892,15 +931,15 @@ " ...\n", " \n", " \n", - " 131\n", - " 0\n", + " 122\n", " 0\n", " 0\n", " 0\n", " 0\n", + " 1\n", " \n", " \n", - " 132\n", + " 123\n", " 0\n", " 0\n", " 0\n", @@ -908,7 +947,7 @@ " 0\n", " \n", " \n", - " 133\n", + " 124\n", " 0\n", " 0\n", " 0\n", @@ -916,7 +955,7 @@ " 0\n", " \n", " \n", - " 134\n", + " 125\n", " 0\n", " 0\n", " 0\n", @@ -924,7 +963,7 @@ " 0\n", " \n", " \n", - " 135\n", + " 126\n", " 0\n", " 0\n", " 0\n", @@ -933,7 +972,7 @@ " \n", " \n", "\n", - "

136 rows × 5 columns

\n", + "

127 rows × 5 columns

\n", "" ], "text/plain": [ @@ -944,13 +983,13 @@ "3 0 0 0 0 0\n", "4 0 0 0 0 0\n", ".. ... ... ... ... ...\n", - "131 0 0 0 0 0\n", - "132 0 0 0 0 0\n", - "133 0 0 0 0 0\n", - "134 0 0 0 0 0\n", - "135 0 0 0 0 0\n", + "122 0 0 0 0 1\n", + "123 0 0 0 0 0\n", + "124 0 0 0 0 0\n", + "125 0 0 0 0 0\n", + "126 0 0 0 0 0\n", "\n", - "[136 rows x 5 columns]" + "[127 rows x 5 columns]" ] }, "execution_count": 23, @@ -979,12 +1018,12 @@ "3 0\n", "4 1\n", " ..\n", - "131 0\n", - "132 1\n", - "133 0\n", - "134 0\n", - "135 0\n", - "Name: hospital_expire_flag, Length: 136, dtype: int8" + "122 0\n", + "123 0\n", + "124 0\n", + "125 0\n", + "126 0\n", + "Name: hospital_expire_flag, Length: 127, dtype: int8" ] }, "execution_count": 24, @@ -1460,7 +1499,7 @@ " /* fitted */\n", " background-color: var(--sklearn-color-fitted-level-3);\n", "}\n", - "
GaussianNB()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + "
GaussianNB()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" ], "text/plain": [ "GaussianNB()" @@ -1508,7 +1547,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 29, @@ -1517,7 +1556,7 @@ }, { "data": { - "image/png": "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ZZKkN954wy85Sqfn2UPl/fff14gRIzRjxgwNHjxYaWlpWrp0qf7yl78EHeuCBQtq/U9DfHx8yGIF3IokjXqrcePG6tixY8Dfv+CCC7Rs2TK1aNGiVjVZo3Xr1vrggw902WWXSTpdMW7btk0XXHDBGb/frVs3maap9evXKzc3t9bPayp5r9frO5eTkyOPx6P9+/f/ZAXetWtX3yK4Glu2bDn7H/IHNm/erOzsbP3xj3/0nfviiy9qfW///v368ssvlZmZ6btPXFycOnfurJYtWyozM1N79+7ViBEjgro/gLNj4RjwvREjRqhZs2YaNmyYNm7cqOLiYq1bt0733nuvDhw4IEkaO3asHn74Ya1YsUK7du3S3Xff/bPPOLdr1055eXm67bbbtGLFCt81X3rpJUlSdna2DMPQypUrdezYMZWVlSklJUUTJkzQuHHj9MILL2jPnj3avn27nnjiCd9irDvvvFO7d+/W/fffr8LCQi1ZskQFBQVB/XnPPfdc7d+/X0uXLtWePXs0Z86cMy6CS0pKUl5enj7++GNt3LhR9957r2688Ua1atVKkjRjxgzl5+drzpw5+te//qVPPvlECxcu1KOPPhpUPABqI0kD32vUqJE2bNigtm3bavjw4eratatuv/12nTp1yldZ//73v9d///d/Ky8vT/369VNKSoquv/76n73uvHnz9Otf/1p33323unTpolGjRqm8vFySdM4552jGjBmaNGmSWrZsqTFjxkiSZs6cqSlTpig/P19du3bVlVdeqTfeeEPt27eXdHqe+JVXXtGKFSvUo0cPzZ8/X7NmzQrqz3vttddq3LhxGjNmjHr27KnNmzdrypQptb7XsWNHDR8+XFdddZUGDRqk7t27+z1idccdd+jZZ5/VwoUL1a1bN/Xv318FBQW+WAHUnWH91IoXAAAQVVTSAAA4FEkaAACHIkkDAOBQJGkAAByKJA0AgEORpAEAcCiSNAAADkWSBgDAoUjSAAA4FEkaAACHIkkDAOBQJGkAABzq/wPKjCmOMaod1gAAAABJRU5ErkJggg==", + "image/png": "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L6hLly5KSkpS586d9eyzzwYdC4DaSNJwjeTkZFVWVvo+r1u3TkVFRVq7dq1WrVqlqqoqDRkyRKmpqdq0aZP+9re/KSUlRddee61v3B/+8Afl5+frpZde0ubNm3XixAmtWLHiR+9766236k9/+pPmzp2rnTt36rnnnlNKSoqys7P1+uuvS5KKiop0+PBhPfXUU5KkvLw8LV68WAsWLNBnn32mCRMm6JZbbtGGDRsknfnHxIgRIzRs2DAVFhbqzjvv1OTJk4P+f5Kamqr8/Hz94x//0FNPPaWFCxfqySef9PtOcXGxXn31Vb311ltavXq1PvroI91zzz2+ny9ZskTTpk3TI488op07d2r27NmaOnWqXn755aDjAfA9FlAP5ebmWsOHD7csy7JM07TWrl1reTwea9KkSb6ft2jRwqqoqPCNeeWVV6xOnTpZpmn6zlVUVFjJycnWmjVrLMuyrFatWlmPPfaY7+dVVVVW69atffeyLMu66qqrrPHjx1uWZVlFRUWWJGvt2rXnjPP999+3JFlff/2179zp06ethg0bWlu2bPH77h133GGNHDnSsizLmjJlitW1a1e/nz/44IO1rvV9kqwVK1b84M8ff/xxq3fv3r7P06dPt+Lj462DBw/6zr3zzjtWXFycdfjwYcuyLOsnP/mJtXTpUr/rzJo1y+rXr59lWZa1b98+S5L10Ucf/eB9AZwbc9Kot1atWqWUlBRVVVXJNE39+te/1owZM3w/7969u9889Mcff6zi4mKlpqb6Xef06dPas2ePTp48qcOHD/ttz5mQkKA+ffrUannXKCwsVHx8vK666qqA4y4uLta3336ra665xu98ZWWlLr74YknSzp07a20T2q9fv4DvUWP58uWaO3eu9uzZo7KyMlVXVystLc3vO23atNEFF1zgdx/TNFVUVKTU1FTt2bNHd9xxh8aMGeP7TnV1tdLT04OOB4A/kjTqrYEDB2r+/PlKTExUVlaWEhL8/7g3atTI73NZWZl69+6tJUuW1LpWs2bN6hRDcnJy0GPKysokSX/5y1/8kqN0Zp49VLZu3apRo0Zp5syZGjJkiNLT07Vs2TL94Q9/CDrWhQsX1vpHQ3x8fMhiBdyKJI16q1GjRurQoUPA37/kkku0fPlyNW/evFY1WaNVq1b64IMPdOWVV0o6UzFu375dl1xyyTm/3717d5mmqQ0bNmjQoEG1fl5TyXu9Xt+5rl27yuPx6MCBAz9YgXfp0sW3CK7Gtm3bzv+b/I4tW7YoJydHDz30kO/c559/Xut7Bw4c0BdffKGsrCzffeLi4tSpUye1aNFCWVlZ2rt3r0aNGhXU/QGcHwvHgLNGjRqlpk2bavjw4dq0aZP27dun9evX67777tPBgwclSePHj9ejjz6qlStXateuXbrnnnt+9Bnntm3bKjc3V7fffrtWrlzpu+arr74qScrJyZFhGFq1apWOHz+usrIypaamatKkSZowYYJefvll7dmzRzt27NDTTz/tW4x11113affu3XrggQdUVFSkpUuXKj8/P6jf74UXXqgDBw5o2bJl2rNnj+bOnXvORXBJSUnKzc3Vxx9/rE2bNum+++7TzTffrJYtW0qSZs6cqby8PM2dO1f//Oc/9cknn2jRokV64okngooHQG0kaeCshg0bauPGjWrTpo1GjBihLl266I477tDp06d9lfV///d/6ze/+Y1yc3PVr18/paam6sYbb/zR686fP1+/+MUvdM8996hz584aM2aMysvLJUkXXHCBZs6cqcmTJ6tFixYaN26cJGnWrFmaOnWq8vLy1KVLF1177bX6y1/+onbt2kk6M0/8+uuva+XKlerZs6cWLFig2bNnB/X7vf766zVhwgSNGzdOvXr10pYtWzR16tRa3+vQoYNGjBih6667ToMHD1aPHj38HrG688479cILL2jRokXq3r27rrrqKuXn5/tiBVB3hvVDK14AAEBUUUkDAOBQJGkAAByKJA0AgEORpAEAcCiSNAAADkWSBgDAoUjSAAA4FEkaAACHIkkDAOBQJGkAAByKJA0AgEORpAEAcKj/D6AY4q/rnxBoAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -1546,7 +1585,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Accuracy of the binary classifier = 0.64\n" + "Accuracy of the binary classifier = 0.62\n" ] } ], @@ -1586,6 +1625,18 @@ "\n", "Snowpark pandas lets you seamlessly move between feature engineering, visualization, and machine learning — all within the Python data ecosystem, while operating directly on the data in your data warehouse. \n" ] + }, + { + "cell_type": "markdown", + "id": "4e78a2bc", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "id": "698086ae", + "metadata": {}, + "source": [] } ], "metadata": { diff --git a/tests/unit/modin/test_class.py b/tests/unit/modin/test_class.py index 1d4b3881b7b..29aa1037d47 100644 --- a/tests/unit/modin/test_class.py +++ b/tests/unit/modin/test_class.py @@ -49,7 +49,6 @@ def test_class_equivalence(): assert pd.SparseDtype is native_pd.SparseDtype assert pd.StringDtype is native_pd.StringDtype assert pd.Timedelta is native_pd.Timedelta - assert pd.TimedeltaIndex is native_pd.TimedeltaIndex assert pd.Timestamp is native_pd.Timestamp assert pd.UInt8Dtype is native_pd.UInt8Dtype assert pd.UInt16Dtype is native_pd.UInt16Dtype