Repository: ibis-project/ibis

Issue:
OmniSciDB: Could not find signature for convert: <CategoricalDtype, String, Series>
Selecting a string column with GPU results gives an error.

Connect with `execution_type=2` for GPU/cudf results.

```python
t = con.table('omnisci_states')
t.info()
```

```
...
name      String(nullable=True)52        
```

```python
x = t['name']
x.compile()
```

```
'SELECT "name"\nFROM omnisci_states'
```

```python
x.execute()
```

```
...

/opt/conda/lib/python3.7/site-packages/ibis_framework-1.2.0-py3.7.egg/ibis/pandas/client.py in ibis_schema_apply_to(schema, df)
    331 
    332         if not_equal or dtype == dt.string:
--> 333             df[column] = convert(col_dtype, dtype, col)
    334 
    335     return df

opt/conda/lib/python3.7/site-packages/multipledispatch-0.6.0-py3.7.egg/multipledispatch/dispatcher.py in __call__(self, *args, **kwargs)
    273                 raise NotImplementedError(
    274                     'Could not find signature for %s: <%s>' %
--> 275                     (self.name, str_signature(types)))
    276             self._cache[types] = func
    277         try:

NotImplementedError: Could not find signature for convert: <CategoricalDtype, String, Series>
```

For contrast, this succeeds using the pymapd connection:

```python
c = con.con.select_ipc_gpu(x.compile())
c.to_pandas()[:5]
```



Discussion:
@mikehinchey does the error occur just after `x.execute()`?
@xmnlab yes, after `execute()` when it's converting the results.

Resolving patch:
diff --git a/ibis/client.py b/ibis/client.py
--- a/ibis/client.py
+++ b/ibis/client.py
@@ -39,7 +39,7 @@ def __init__(self, client, sql, **kwargs):
         self.result_wrapper = getattr(dml, 'result_handler', None)
         self.extra_options = kwargs
 
-    def execute(self):
+    def execute(self, **kwargs):
         """Execute a DML expression.
 
         Returns
@@ -50,7 +50,9 @@ def execute(self):
           Scalar expressions: Python scalar value
         """
         # synchronous by default
-        with self.client._execute(self.compiled_sql, results=True) as cur:
+        with self.client._execute(
+            self.compiled_sql, results=True, **kwargs
+        ) as cur:
             result = self._fetch(cur)
 
         return self._wrap_result(result)
@@ -144,7 +146,7 @@ def _fully_qualified_name(self, name, database):
         # XXX
         return name
 
-    def _execute(self, query, results=False):
+    def _execute(self, query, results=False, **kwargs):
         cur = self.con.execute(query)
         if results:
             return cur
diff --git a/ibis/omniscidb/api.py b/ibis/omniscidb/api.py
--- a/ibis/omniscidb/api.py
+++ b/ibis/omniscidb/api.py
@@ -1,10 +1,11 @@
 """OmniSciDB API module."""
+from typing import Optional
 
 import ibis
 import ibis.common.exceptions as com
 import ibis.config as cf
 from ibis.config import options
-from ibis.omniscidb.client import EXECUTION_TYPE_CURSOR, OmniSciDBClient
+from ibis.omniscidb.client import OmniSciDBClient  # noqa: F401
 from ibis.omniscidb.compiler import compiles, dialect, rewrites  # noqa: F401
 
 
@@ -47,29 +48,35 @@ def verify(expr: ibis.Expr, params: dict = None) -> bool:
 
 
 def connect(
-    uri=None,
-    user=None,
-    password=None,
-    host=None,
-    port=6274,
-    database=None,
-    protocol='binary',
-    session_id=None,
-    execution_type=EXECUTION_TYPE_CURSOR,
+    uri: Optional[str] = None,
+    user: Optional[str] = None,
+    password: Optional[str] = None,
+    host: Optional[str] = None,
+    port: Optional[int] = 6274,
+    database: Optional[str] = None,
+    protocol: str = 'binary',
+    session_id: Optional[str] = None,
+    ipc: Optional[bool] = None,
+    gpu_device: Optional[int] = None,
 ):
     """Create a client for OmniSciDB backend.
 
     Parameters
     ----------
-    uri : str
-    user : str
-    password : str
-    host : str
-    port : int
-    database : str
-    protocol : str
-    session_id : str
-    execution_type : int
+    uri : str, optional
+    user : str, optional
+    password : str, optional
+    host : str, optional
+    port : int, default 6274
+    database : str, optional
+    protocol : {'binary', 'http', 'https'}, default 'binary'
+    session_id: str, optional
+    ipc : bool, optional
+      Enable Inter Process Communication (IPC) execution type.
+      `ipc` default value is False when `gpu_device` is None, otherwise
+      its default value is True.
+    gpu_device : int, optional
+      GPU Device ID.
 
     Returns
     -------
@@ -84,7 +91,8 @@ def connect(
         database=database,
         protocol=protocol,
         session_id=session_id,
-        execution_type=execution_type,
+        ipc=ipc,
+        gpu_device=gpu_device,
     )
 
     if options.default_backend is None:
diff --git a/ibis/omniscidb/client.py b/ibis/omniscidb/client.py
--- a/ibis/omniscidb/client.py
+++ b/ibis/omniscidb/client.py
@@ -1,4 +1,6 @@
 """Ibis OmniSciDB Client."""
+from typing import Optional
+
 import pandas as pd
 import pkg_resources
 import pymapd
@@ -19,7 +21,7 @@
 from ibis.util import log
 
 try:
-    from cudf.dataframe.dataframe import DataFrame as GPUDataFrame
+    from cudf import DataFrame as GPUDataFrame
 except (ImportError, OSError):
     GPUDataFrame = None
 
@@ -33,9 +35,6 @@
 except ImportError:
     ...
 
-EXECUTION_TYPE_ICP = 1
-EXECUTION_TYPE_ICP_GPU = 2
-EXECUTION_TYPE_CURSOR = 3
 
 fully_qualified_re = re.compile(r"(.*)\.(?:`(.*)`|(.*))")
 
@@ -238,13 +237,14 @@ def to_df(self):
         dataframe : pandas.DataFrame
         """
         cursor = self.cursor
-        cursor_description = cursor.description
 
         if not isinstance(cursor, Cursor):
             if cursor is None:
                 return geopandas.GeoDataFrame([])
             return cursor
 
+        cursor_description = cursor.description
+
         col_names = [c.name for c in cursor_description]
         result = pd.DataFrame(cursor.fetchall(), columns=col_names)
 
@@ -276,12 +276,30 @@ def to_df(self):
         return result
 
 
+class OmniSciDBGPUCursor(OmniSciDBDefaultCursor):
+    """Cursor that exports result to GPU Dataframe."""
+
+    def to_df(self):
+        """
+        Return the result as a data frame.
+
+        Returns
+        -------
+        dataframe : cudf.DataFrame
+        """
+        return self.cursor
+
+
 class OmniSciDBQuery(Query):
-    """OmniSciDB Query class."""
+    """DML query execution to enable queries, progress, cancellation etc."""
 
     def _fetch(self, cursor):
-        # check if cursor is a pymapd cursor.Cursor
-        return self.schema().apply_to(cursor.to_df())
+        result = cursor.to_df()
+        # TODO: try to use `apply_to` for cudf.DataFrame using cudf 0.9
+        if GPUDataFrame is None or not isinstance(result, GPUDataFrame):
+            return self.schema().apply_to(result)
+        else:
+            return result
 
 
 class OmniSciDBTable(ir.TableExpr, DatabaseEntity):
@@ -472,17 +490,19 @@ class OmniSciDBClient(SQLClient):
 
     def __init__(
         self,
-        uri: str = None,
-        user: str = None,
-        password: str = None,
-        host: str = None,
-        port: str = 6274,
-        database: str = None,
+        uri: Optional[str] = None,
+        user: Optional[str] = None,
+        password: Optional[str] = None,
+        host: Optional[str] = None,
+        port: Optional[int] = 6274,
+        database: Optional[str] = None,
         protocol: str = 'binary',
-        session_id: str = None,
-        execution_type: str = EXECUTION_TYPE_CURSOR,
+        session_id: Optional[str] = None,
+        ipc: Optional[bool] = None,
+        gpu_device: Optional[int] = None,
     ):
-        """Initialize OmniSciDB Client.
+        """
+        Initialize OmniSciDB Client.
 
         Parameters
         ----------
@@ -492,11 +512,14 @@ def __init__(
         host : str, optional
         port : int, default 6274
         database : str, optional
-        protocol : {'binary', 'http', 'https'}, default binary
+        protocol : {'binary', 'http', 'https'}, default 'binary'
         session_id: str, optional
-        execution_type : {
-          EXECUTION_TYPE_ICP, EXECUTION_TYPE_ICP_GPU, EXECUTION_TYPE_CURSOR
-        }, default EXECUTION_TYPE_CURSOR
+        ipc : bool, optional, default None
+          Enable Inter Process Communication (IPC) execution type.
+          `ipc` default value when `gpu_device` is None is False, otherwise
+          its default value is True.
+        gpu_device : int, optional, default None
+          GPU Device ID.
 
         Raises
         ------
@@ -514,14 +537,10 @@ def __init__(
         self.protocol = protocol
         self.session_id = session_id
 
-        if execution_type not in (
-            EXECUTION_TYPE_ICP,
-            EXECUTION_TYPE_ICP_GPU,
-            EXECUTION_TYPE_CURSOR,
-        ):
-            raise Exception('Execution type defined not available.')
+        self._check_execution_type(ipc=ipc, gpu_device=gpu_device)
 
-        self.execution_type = execution_type
+        self.ipc = ipc
+        self.gpu_device = gpu_device
 
         if session_id:
             if self.version < pkg_resources.parse_version('0.12.0'):
@@ -586,6 +605,28 @@ def _build_ast(self, expr, context):
         result = build_ast(expr, context)
         return result
 
+    def _check_execution_type(
+        self, ipc: Optional[bool], gpu_device: Optional[int]
+    ):
+        """
+        Check if the execution type (ipc and gpu_device) is valid.
+
+        Parameters
+        ----------
+        ipc : bool, optional
+        gpu_device : int, optional
+
+        Raises
+        ------
+        com.IbisInputError
+            if "gpu_device" is not None and "ipc" is False
+        """
+        if gpu_device is not None and ipc is False:
+            raise com.IbisInputError(
+                'If GPU device is provided, IPC parameter should '
+                'be True or None (default).'
+            )
+
     def _fully_qualified_name(self, name, database):
         # OmniSciDB raises error sometimes with qualified names
         return name
@@ -633,31 +674,67 @@ def _get_table_schema(self, table_name, database=None):
             database, table_name = table_name_
         return self.get_schema(table_name, database)
 
-    def _execute(self, query, results=True):
-        """Execute a query.
+    def _execute_query(self, dml, **kwargs):
+        query = self.query_class(self, dml, **kwargs)
+        return query.execute(**kwargs)
+
+    def _execute(
+        self,
+        query: str,
+        results: bool = True,
+        ipc: Optional[bool] = None,
+        gpu_device: Optional[int] = None,
+        **kwargs,
+    ):
+        """
+        Compile and execute Ibis expression.
 
-        Paramters
-        ---------
-        query : DDL or DML or string
+        Return result in-memory in the appropriate object type.
+
+        Parameters
+        ----------
+        query : string
+          DML or DDL statement
+        results : boolean, default False
+          Pass True if the query as a result set
+        ipc : bool, optional, default None
+          Enable Inter Process Communication (IPC) execution type.
+          `ipc` default value (None) when `gpu_device` is None is interpreted
+           as False, otherwise it is interpreted as True.
+        gpu_device : int, optional, default None
+          GPU device ID.
 
         Returns
         -------
-        result : pandas.DataFrame
+        output : execution type dependent
+          If IPC is set as True and no GPU device is set:
+            ``pandas.DataFrame``
+          If IPC is set as True and GPU device is set: ``cudf.DataFrame``
+          If IPC is set as False and no GPU device is set:
+            pandas.DataFrame or
+            geopandas.GeoDataFrame (if it uses geospatial data)
 
         Raises
         ------
         Exception
             if execution method fails.
         """
+        # raise an Exception if kwargs is not empty:
+        if kwargs:
+            raise com.IbisInputError(
+                '"OmniSciDB.execute" method just support the follow parameter:'
+                ' "query", "results", "ipc" and "gpu_device". The follow extra'
+                ' parameters was given: "{}".'.format(', '.join(kwargs.keys()))
+            )
+
         if isinstance(query, (DDL, DML)):
             query = query.compile()
 
-        if self.execution_type == EXECUTION_TYPE_ICP:
-            execute = self.con.select_ipc
-        elif self.execution_type == EXECUTION_TYPE_ICP_GPU:
-            execute = self.con.select_ipc_gpu
-        else:
-            execute = self.con.cursor().execute
+        if ipc is None and gpu_device is None:
+            ipc = self.ipc
+            gpu_device = self.gpu_device
+
+        self._check_execution_type(ipc, gpu_device)
 
         cursor = (
             OmniSciDBGeoCursor
@@ -665,8 +742,19 @@ def _execute(self, query, results=True):
             else OmniSciDBDefaultCursor
         )
 
+        params = {}
+
+        if gpu_device is None and not ipc:
+            execute = self.con.cursor().execute
+        elif gpu_device is None and ipc:
+            execute = self.con.select_ipc
+        else:
+            params['device_id'] = gpu_device
+            execute = self.con.select_ipc_gpu
+            cursor = OmniSciDBGPUCursor
+
         try:
-            result = cursor(execute(query))
+            result = cursor(execute(query, **params))
         except Exception as e:
             raise Exception('{}: {}'.format(e, query))
 
@@ -973,7 +1061,8 @@ def database(self, name=None):
                 database=name,
                 protocol=self.protocol,
                 session_id=self.session_id,
-                execution_type=self.execution_type,
+                ipc=self.ipc,
+                device=self.device,
             )
             return self.database_class(name, new_client)
 


Test patch:
diff --git a/ibis/omniscidb/tests/test_client.py b/ibis/omniscidb/tests/test_client.py
--- a/ibis/omniscidb/tests/test_client.py
+++ b/ibis/omniscidb/tests/test_client.py
@@ -1,3 +1,6 @@
+from typing import Optional
+
+import mock
 import pandas as pd
 import pytest
 from pkg_resources import get_distribution, parse_version
@@ -7,8 +10,9 @@
 import ibis.expr.types as ir
 from ibis.tests.util import assert_equal
 
+pymapd = pytest.importorskip('pymapd')
+
 pytestmark = pytest.mark.omniscidb
-pytest.importorskip('pymapd')
 
 
 def test_table(alltypes):
@@ -148,3 +152,54 @@ def test_sql(con, sql):
 def test_explain(con, alltypes):
     # execute the expression using SQL query
     con.explain(alltypes)
+
+
+@pytest.mark.parametrize('ipc', [None, True, False])
+@pytest.mark.parametrize('gpu_device', [None, 1])
+def test_cpu_execution_type(
+    mocker, con, ipc: Optional[bool], gpu_device: Optional[int]
+):
+    """Test the combination of ipc and gpu_device parameters for connection."""
+    connection_info = {
+        'host': con.host,
+        'port': con.port,
+        'user': con.user,
+        'password': con.password,
+        'database': con.db_name,
+        'protocol': con.protocol,
+        'ipc': ipc,
+        'gpu_device': gpu_device,
+    }
+
+    if gpu_device and ipc is False:
+        # test exception
+        with pytest.raises(ibis.common.exceptions.IbisInputError):
+            ibis.omniscidb.connect(**connection_info)
+        return
+
+    mocked_methods = []
+
+    for mock_method_name in ('select_ipc', 'select_ipc_gpu'):
+        mocked_method = mock.patch.object(
+            pymapd.connection.Connection,
+            mock_method_name,
+            new=lambda *args, **kwargs: pd.DataFrame({'string_col': ['1']}),
+        )
+
+        mocked_method.start()
+        mocked_methods.append(mocked_method)
+
+    new_con = ibis.omniscidb.connect(**connection_info)
+    assert new_con is not None
+    assert new_con.ipc == ipc
+    assert new_con.gpu_device == gpu_device
+
+    expr = new_con.table('functional_alltypes')
+    expr = expr[['string_col']].limit(1)
+
+    assert expr.execute(ipc=True).shape[0] == 1
+    assert expr.execute(ipc=False).shape[0] == 1
+    assert expr.execute(ipc=None).shape[0] == 1
+
+    for mocked_method in mocked_methods:
+        mocked_method.stop()