Repository: ibis-project/ibis

Issue:
Add an Expr.verify method that ensures that an expression is translatable
But does not execute. 



Discussion:

Resolving patch:
diff --git a/ibis/client.py b/ibis/client.py
--- a/ibis/client.py
+++ b/ibis/client.py
@@ -131,6 +131,18 @@ def execute(self, expr, params=None, limit=None):
 
         return output
 
+    def compile(self, expr, params=None, limit=None):
+        """
+        Translate expression to one or more queries according to backend target
+
+        Returns
+        -------
+        output : single query or list of queries
+        """
+        ast = self._build_ast_ensure_limit(expr, limit)
+        queries = [query.compile() for query in ast.queries]
+        return queries[0] if len(queries) == 1 else queries
+
     def _execute_and_fetch(self, compiled_query):
         with self._execute(compiled_query, results=True) as cur:
             return self._fetch_from_cursor(cur)
@@ -209,6 +221,11 @@ def execute(expr, limit=None):
     return backend.execute(expr, limit=limit)
 
 
+def compile(expr, limit=None):
+    backend = find_backend(expr)
+    return backend.compile(expr, limit=limit)
+
+
 def find_backend(expr):
     backends = []
 
diff --git a/ibis/expr/types.py b/ibis/expr/types.py
--- a/ibis/expr/types.py
+++ b/ibis/expr/types.py
@@ -129,6 +129,28 @@ def execute(self, limit=None):
         from ibis.client import execute
         return execute(self, limit=limit)
 
+    def compile(self, limit=None):
+        """
+        Compile expression to whatever execution target, to verify
+
+        Returns
+        -------
+        compiled : value or list
+           query representation or list thereof
+        """
+        from ibis.client import compile
+        return compile(self, limit=limit)
+
+    def verify(self):
+        """
+        Returns True if expression can be compiled to its attached client
+        """
+        try:
+            self.compile()
+            return True
+        except:
+            return False
+
     def equals(self, other):
         if type(self) != type(other):
             return False


Test patch:
diff --git a/ibis/expr/tests/mocks.py b/ibis/expr/tests/mocks.py
--- a/ibis/expr/tests/mocks.py
+++ b/ibis/expr/tests/mocks.py
@@ -356,6 +356,11 @@ def execute(self, expr, limit=None):
             self.executed_queries.append(query.compile())
         return None
 
+    def compile(self, expr, limit=None):
+        ast = self._build_ast_ensure_limit(expr, limit)
+        queries = [q.compile() for q in ast.queries]
+        return queries[0] if len(queries) == 1 else queries
+
 
 _all_types_schema = [
     ('a', 'int8'),
diff --git a/ibis/expr/tests/test_interactive.py b/ibis/expr/tests/test_interactive.py
--- a/ibis/expr/tests/test_interactive.py
+++ b/ibis/expr/tests/test_interactive.py
@@ -86,3 +86,8 @@ def test_histogram_repr_no_query_execute(self):
         with config.option_context('interactive', True):
             expr._repr()
         assert self.con.executed_queries == []
+
+    def test_compile_no_execute(self):
+        t = self.con.table('functional_alltypes')
+        t.double_col.sum().compile()
+        assert self.con.executed_queries == []
diff --git a/ibis/impala/tests/test_client.py b/ibis/impala/tests/test_client.py
--- a/ibis/impala/tests/test_client.py
+++ b/ibis/impala/tests/test_client.py
@@ -206,6 +206,13 @@ def test_sql_query_limits(self):
             assert table.count().execute() == 25
             assert table.count().execute(limit=10) == 25
 
+    def test_compile_verify(self):
+        table = self.db.functional_alltypes
+        expr = table.double_col.sum()
+
+        assert isinstance(expr.compile(), str)
+        assert expr.verify()
+
     def test_database_repr(self):
         assert self.test_data_db in repr(self.db)
 
diff --git a/ibis/sql/sqlite/tests/test_client.py b/ibis/sql/sqlite/tests/test_client.py
--- a/ibis/sql/sqlite/tests/test_client.py
+++ b/ibis/sql/sqlite/tests/test_client.py
@@ -49,3 +49,10 @@ def test_simple_aggregate_execute(self):
     def test_list_tables(self):
         assert len(self.con.list_tables()) > 0
         assert len(self.con.list_tables(like='functional')) == 1
+
+    def test_compile_verify(self):
+        unsupported_expr = self.alltypes.string_col.approx_nunique()
+        assert not unsupported_expr.verify()
+
+        supported_expr = self.alltypes.double_col.sum()
+        assert supported_expr.verify()