Repository: mars-project/mars

Issue:
[BUG] Filtering failed when scaler is pd.Timestamp 
<!--
Thank you for your contribution!

Please review https://github.com/mars-project/mars/blob/master/CONTRIBUTING.rst before opening an issue.
-->

**Describe the bug**

Filtering failed when scaler is pd.Timestamp.

**To Reproduce**
To help us reproducing this bug, please provide information below:
1. Your Python version
2. The version of Mars you use
3. Versions of crucial packages, such as numpy, scipy and pandas
4. Full stack of the error.
5. Minimized code to reproduce the error.

```
In [1]: import mars.dataframe as md

In [2]: df = md.DataFrame({'a': ['a', 'b', 'c'], 'b': [md.Timestamp('2022-1-1'),
   ...:  md.Timestamp('2022-1-2'), md.Timestamp('2022-1-3')]})

In [3]: df[df.b < md.Timestamp('2022-1-3')].execute()
Unexpected error happens in <function TaskProcessor.optimize at 0x7feea8b979d0>
Traceback (most recent call last):
  File "/Users/xuyeqin/Workspace/mars/mars/services/task/supervisor/processor.py", line 57, in inner
    return await func(processor, *args, **kwargs)
  File "/Users/xuyeqin/Workspace/mars/mars/services/task/supervisor/processor.py", line 131, in optimize
    return await asyncio.to_thread(self._preprocessor.optimize)
  File "/Users/xuyeqin/Workspace/mars/mars/lib/aio/_threads.py", line 36, in to_thread
    return await loop.run_in_executor(None, func_call)
  File "/Users/xuyeqin/miniconda3/envs/mars3.8/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/Users/xuyeqin/Workspace/mars/mars/services/task/supervisor/preprocessor.py", line 127, in optimize
    self.tileable_optimization_records = optimize_tileable_graph(
  File "/Users/xuyeqin/Workspace/mars/mars/optimization/logical/tileable/core.py", line 36, in optimize
    return TileableOptimizer.optimize(tileable_graph)
  File "/Users/xuyeqin/Workspace/mars/mars/core/mode.py", line 77, in _inner
    return func(*args, **kwargs)
  File "/Users/xuyeqin/Workspace/mars/mars/optimization/logical/core.py", line 265, in optimize
    if rule.match(op):
  File "/Users/xuyeqin/Workspace/mars/mars/optimization/logical/tileable/arithmetic_query.py", line 65, in match
    _, expr = self._extract_eval_expression(op.outputs[0])
  File "/Users/xuyeqin/Workspace/mars/mars/optimization/logical/tileable/arithmetic_query.py", line 111, in _extract_eval_expression
    result = cls._extract_binary(tileable)
  File "/Users/xuyeqin/Workspace/mars/mars/optimization/logical/tileable/arithmetic_query.py", line 148, in _extract_binary
    rhs_tileable, rhs_expr = cls._extract_eval_expression(op.rhs)
  File "/Users/xuyeqin/Workspace/mars/mars/optimization/logical/tileable/arithmetic_query.py", line 105, in _extract_eval_expression
    elif isinstance(tileable.op, DataFrameUnaryUfunc):
AttributeError: 'Timestamp' object has no attribute 'op'
  0%|                                                   | 0/100 [00:00<?, ?it/s]
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-3-a7387f7228a9> in <module>
----> 1 df[df.b < md.Timestamp('2022-1-3')].execute()

~/Workspace/mars/mars/core/entity/tileables.py in execute(self, session, **kw)
    460 
    461     def execute(self, session=None, **kw):
--> 462         result = self.data.execute(session=session, **kw)
    463         if isinstance(result, TILEABLE_TYPE):
    464             return self

~/Workspace/mars/mars/core/entity/executable.py in execute(self, session, **kw)
     96 
     97         session = _get_session(self, session)
---> 98         return execute(self, session=session, **kw)
     99 
    100     def _check_session(self, session: SessionType, action: str):

~/Workspace/mars/mars/deploy/oscar/session.py in execute(tileable, session, wait, new_session_kwargs, show_progress, progress_update_interval, *tileables, **kwargs)
   1771         session = get_default_or_create(**(new_session_kwargs or dict()))
   1772     session = _ensure_sync(session)
-> 1773     return session.execute(
   1774         tileable,
   1775         *tileables,

~/Workspace/mars/mars/deploy/oscar/session.py in execute(self, tileable, show_progress, *tileables, **kwargs)
   1571         fut = asyncio.run_coroutine_threadsafe(coro, self._loop)
   1572         try:
-> 1573             execution_info: ExecutionInfo = fut.result(
   1574                 timeout=self._isolated_session.timeout
   1575             )

~/miniconda3/envs/mars3.8/lib/python3.8/concurrent/futures/_base.py in result(self, timeout)
    437                 raise CancelledError()
    438             elif self._state == FINISHED:
--> 439                 return self.__get_result()
    440             else:
    441                 raise TimeoutError()

~/miniconda3/envs/mars3.8/lib/python3.8/concurrent/futures/_base.py in __get_result(self)
    386     def __get_result(self):
    387         if self._exception:
--> 388             raise self._exception
    389         else:
    390             return self._result

~/Workspace/mars/mars/deploy/oscar/session.py in _execute(session, wait, show_progress, progress_update_interval, cancelled, *tileables, **kwargs)
   1722                 while not cancelled.is_set():
   1723                     try:
-> 1724                         await asyncio.wait_for(
   1725                             asyncio.shield(execution_info), progress_update_interval
   1726                         )

~/miniconda3/envs/mars3.8/lib/python3.8/asyncio/tasks.py in wait_for(fut, timeout, loop)
    481 
    482         if fut.done():
--> 483             return fut.result()
    484         else:
    485             fut.remove_done_callback(cb)

~/miniconda3/envs/mars3.8/lib/python3.8/asyncio/tasks.py in _wrap_awaitable(awaitable)
    682     that will later be wrapped in a Task by ensure_future().
    683     """
--> 684     return (yield from awaitable.__await__())
    685 
    686 _wrap_awaitable._is_coroutine = _is_coroutine

~/Workspace/mars/mars/deploy/oscar/session.py in wait()
    100 
    101             async def wait():
--> 102                 return await self._aio_task
    103 
    104             self._future_local.future = fut = asyncio.run_coroutine_threadsafe(

~/Workspace/mars/mars/deploy/oscar/session.py in _run_in_background(self, tileables, task_id, progress, profiling)
    901                     )
    902                 if task_result.error:
--> 903                     raise task_result.error.with_traceback(task_result.traceback)
    904             if cancelled:
    905                 return

~/Workspace/mars/mars/services/task/supervisor/processor.py in inner(processor, *args, **kwargs)
     55     async def inner(processor: "TaskProcessor", *args, **kwargs):
     56         try:
---> 57             return await func(processor, *args, **kwargs)
     58         except:  # noqa: E722  # nosec  # pylint: disable=bare-except  # pragma: no cover
     59             if log_when_error:

~/Workspace/mars/mars/services/task/supervisor/processor.py in optimize(self)
    129         # optimization, run it in executor,
    130         # since optimization may be a CPU intensive operation
--> 131         return await asyncio.to_thread(self._preprocessor.optimize)
    132 
    133     @_record_error

~/Workspace/mars/mars/lib/aio/_threads.py in to_thread(func, *args, **kwargs)
     34     ctx = contextvars.copy_context()
     35     func_call = functools.partial(ctx.run, func, *args, **kwargs)
---> 36     return await loop.run_in_executor(None, func_call)

~/miniconda3/envs/mars3.8/lib/python3.8/concurrent/futures/thread.py in run(self)
     55 
     56         try:
---> 57             result = self.fn(*self.args, **self.kwargs)
     58         except BaseException as exc:
     59             self.future.set_exception(exc)

~/Workspace/mars/mars/services/task/supervisor/preprocessor.py in optimize(self)
    125         if self._config.optimize_tileable_graph:
    126             # enable optimization
--> 127             self.tileable_optimization_records = optimize_tileable_graph(
    128                 self.tileable_graph
    129             )

~/Workspace/mars/mars/optimization/logical/tileable/core.py in optimize(tileable_graph)
     34 
     35 def optimize(tileable_graph: TileableGraph) -> OptimizationRecords:
---> 36     return TileableOptimizer.optimize(tileable_graph)

~/Workspace/mars/mars/core/mode.py in _inner(*args, **kwargs)
     75             def _inner(*args, **kwargs):
     76                 with enter_mode(**mode_name_to_value):
---> 77                     return func(*args, **kwargs)
     78 
     79         else:

~/Workspace/mars/mars/optimization/logical/core.py in optimize(cls, graph)
    263                     # maybe removed during optimization
    264                     continue
--> 265                 if rule.match(op):
    266                     optimized = True
    267                     rule.apply(op)

~/Workspace/mars/mars/optimization/logical/tileable/arithmetic_query.py in match(self, op)
     63     @implements(OptimizationRule.match)
     64     def match(self, op: OperandType) -> bool:
---> 65         _, expr = self._extract_eval_expression(op.outputs[0])
     66         return expr is not None
     67 

~/Workspace/mars/mars/optimization/logical/tileable/arithmetic_query.py in _extract_eval_expression(cls, tileable)
    109                 result = None, None
    110             else:
--> 111                 result = cls._extract_binary(tileable)
    112         else:
    113             result = None, None

~/Workspace/mars/mars/optimization/logical/tileable/arithmetic_query.py in _extract_binary(cls, tileable)
    146         if lhs_tileable is not None:
    147             cls._add_collapsable_predecessor(tileable, op.lhs)
--> 148         rhs_tileable, rhs_expr = cls._extract_eval_expression(op.rhs)
    149         if rhs_tileable is not None:
    150             cls._add_collapsable_predecessor(tileable, op.rhs)

~/Workspace/mars/mars/optimization/logical/tileable/arithmetic_query.py in _extract_eval_expression(cls, tileable)
    103         if cls._is_select_dataframe_column(tileable):
    104             result = cls._extract_column_select(tileable)
--> 105         elif isinstance(tileable.op, DataFrameUnaryUfunc):
    106             result = cls._extract_unary(tileable)
    107         elif isinstance(tileable.op, DataFrameBinopUfunc):

AttributeError: 'Timestamp' object has no attribute 'op'
```


Discussion:

Resolving patch:
diff --git a/mars/optimization/logical/tileable/arithmetic_query.py b/mars/optimization/logical/tileable/arithmetic_query.py
--- a/mars/optimization/logical/tileable/arithmetic_query.py
+++ b/mars/optimization/logical/tileable/arithmetic_query.py
@@ -15,10 +15,10 @@
 import weakref
 from typing import Optional, Tuple
 
-import numpy as np
+from pandas.api.types import is_scalar
 
 from .... import dataframe as md
-from ....core import Tileable, get_output_types
+from ....core import Tileable, get_output_types, ENTITY_TYPE
 from ....dataframe.arithmetic.core import DataFrameUnaryUfunc, DataFrameBinopUfunc
 from ....dataframe.base.eval import DataFrameEval
 from ....dataframe.indexing.getitem import DataFrameIndex
@@ -94,9 +94,12 @@ def _is_select_dataframe_column(tileable) -> bool:
     def _extract_eval_expression(
         cls, tileable
     ) -> Tuple[Optional[Tileable], Optional[str]]:
-        if np.isscalar(tileable):
+        if is_scalar(tileable):
             return None, repr(tileable)
 
+        if not isinstance(tileable, ENTITY_TYPE):  # pragma: no cover
+            return None, None
+
         if tileable in _extract_result_cache:
             return _extract_result_cache[tileable]
 


Test patch:
diff --git a/mars/optimization/logical/tileable/tests/test_arithmetic_query.py b/mars/optimization/logical/tileable/tests/test_arithmetic_query.py
--- a/mars/optimization/logical/tileable/tests/test_arithmetic_query.py
+++ b/mars/optimization/logical/tileable/tests/test_arithmetic_query.py
@@ -109,3 +109,20 @@ def test_bool_eval_to_query(setup):
 
     r_df2, _r_col_a = fetch(execute(df2, df1["A"]))
     pd.testing.assert_frame_equal(r_df2, raw[(raw["A"] > 0.5) & (raw["C"] < 0.5)] + 1)
+
+    raw = pd.DataFrame(
+        {
+            "a": np.arange(100),
+            "b": [pd.Timestamp("2022-1-1") + pd.Timedelta(days=i) for i in range(100)],
+        }
+    )
+    df1 = md.DataFrame(raw, chunk_size=10)
+    df2 = df1[df1.b < pd.Timestamp("2022-3-20")]
+    graph = TileableGraph([df2.data])
+    next(TileableGraphBuilder(graph).build())
+    records = optimize(graph)
+    opt_df2 = records.get_optimization_result(df2.data)
+    assert opt_df2.op.expr == "(`b`) < (Timestamp('2022-03-20 00:00:00'))"
+
+    r_df2 = fetch(execute(df2))
+    pd.testing.assert_frame_equal(r_df2, raw[raw.b < pd.Timestamp("2022-3-20")])