Repository: databricks/koalas

Issue:
Allow querying DataFrame directly in sql method
While I really like the idea of @rxin's recent #256 PR, he uses an (in my opinion) over-simplistic example of `ks.sql("select * from range(10) where id > 7")`. I believe that the ability to query actual Koalas DataFrames through SQL can prove really valuable to many users. However, when trying to use `ks.sql` with a Koalas DataFrame, the following exception occurs:

```python
kdf = ks.DataFrame({'A': [1,2,3]})
>>> ks.sql("select * from kdf")
...
org.apache.spark.sql.AnalysisException: Table or view not found: kdf; line 1 pos 14
...
```

This is not surprising to someone with PySpark knowledge who knows that `kdf` has to be registered as a temporary table before being able to use it with SparkSQL. Unfortunately, (as I understand it) the target group of the Koalas library should not be expected to be Spark experts. To get the above example working, the following workaround is needed, which requires the usage of the lower-level (Py)Spark API, thus somewhat defeating the purpose of Koalas.

```python
>>> from pyspark import SparkContext
>>> from pyspark.sql import SQLContext
>>> sc = SparkContext.getOrCreate()
>>> sql = SQLContext(sc)
>>> sql.registerDataFrameAsTable(kdf._sdf, "kdf")
>>> ks.sql("select * from kdf")
   __index_level_0__  A                                                         
0                  0  1
1                  1  2
2                  2  3
# Optionally clean-up by dropping the temporary table
>>> sql.dropTempTable("kdf")
```

Wouldn't it be much more convenient if this "temporary table magic" would instead be handled by Koalas behind the scenes or are there any design objections against such an approach?


Discussion:
I think we shouldn't mix PySpark APIs and Koalas APIs. Maybe we should add a method called `to_spark(..)` and delegate everything to there.

As you said, I think `ks.sql`'s purpose is mainly to generate some data or read data from Spark source only. We maybe should document this limitation clearly.

So, when they want to switch to PySpark context, they should explicitly call `to_spark(...)`. Once we start to mix both, it might be difficult for Koalas to target its original goal, Pandas corresponding APIs.

I see the point of not mixing the two libraries' APIs. Then again, how does `ks.sql` relate to the pandas API where no `pandas.sql()` exists?

Also, the mentioned `to_spark()` method already exists but I don't quite understand how it relates to the `ks.sql` method.
> how does `ks.sql` relate to the pandas API where no pandas.sql() exists?

I was thinking adding some APIs non-existent in Pandas (with some strong reasons) might make sense (see https://github.com/databricks/koalas#unify-small-data-pandas-api-and-big-data-spark-api-but-pandas-first); however, if that brings a bunch of compatibility issues, I think we should restrict its usage rather than putting efforts to extend PySpark's API usage.

> Also, the mentioned `to_spark()` method already exists but I don't quite understand how it relates to the `ks.sql` method.

For instance, `sql.registerDataFrameAsTable(kdf.to_spark(), "kdf")` will work instead. We could additionally consider like a flag that drops index simply because PySpark users are not familiar with it anyway.
@floscha it is interesting that you mention that topic now. I was looking into how to port pandasql to koalas, taking into account python's normal syntax for string interpolation. As an example, this statement:

```py
date = pd.Timestamp('2012-05-01')
ids = [1,2,3]
kdf = ks.read_csv(...)
filtered_kdf = ks.sql(""" SELECT m.field1, m.field2 FROM {filtered_kdf}  m WHERE m.date >= {date} and m.ids in {ids} """)
```

This is equivalent to:

```py
kdf.asDF().registerTempView("_koalas_tmp_1")
filtered_kdf = ks.sql(""" SELECT m.field1, m.field2 FROM _koalas_tmp_1  m WHERE m.date >= '2012-...' and m.ids in (1,2,3) """)
```
The variables are not necessary as a start, but they are very convenient because I see a lot of error when doing string interpolation in general. The pandasql uses a regex that seems pretty good to get started with to extract the variables.

This syntax has the advantage that it is safe from injection attacks and it is familiar to any python user (it is the same as string.format()).

What are the thoughts about this design?

I forgot the link: https://github.com/yhat/pandasql 
They choose to not up brackets. The advantage of using brackets is that any python user will be familiar with the format, the IDE will probably have autocomplete (I see that github make the proper highlights) and it allows the interpolations of arbitrary values.
Hey @thunterdb, I also had *pandasql* in the back of my head but wasn't quite sure whether or not you consider it to be "actual" pandas. Injecting values from variables is definitely a cool feature to have but I think first of all there needs to be a decision if the functionality to run SQL queries should be part of Koalas or rather something external like KoalaSQL 😉

@HyukjinKwon I don't really see compatibility issues since there is no `pandas.sql()` method that could cause name conflicts. On the other hand, as soon as users start creating Spark/SQLContexts and temporary tables, I believe they are comfortable enough to use PySpark instead of Koalas anyway. So having code like `sql.registerDataFrameAsTable(kdf.to_spark(), "kdf")` seems quite counter-intuitive to me.
@floscha, what I mean by compatibility is between Koalas DataFrame and Spark DataFrame. Now we have to consider compatibility with Spark SQL <> Koalas DataFrame as well.

> On the other hand, as soon as users start creating Spark/SQLContexts and temporary tables, I believe they are comfortable enough to use PySpark instead of Koalas anyway. 

Yes, in that case, we should rather avoid to add Spark related APIs since we target users who want to use something different from PySpark. Proper fence is needed to distinguish sometimes.
@HyukjinKwon, what compatibility between Spark SQL and Koalas DataFrame do you mean? The project already has [several dependencies on Spark SQL](https://github.com/databricks/koalas/search?q=pyspark.sql) that require compatibility in some sense.

And again, `ks.sql()` is already a method that has no equivalent in pandas but rather directly translates to PySpark's `DataFrame.sql()` method. Since it defines itself as `Execute a SQL query and return the result as a Koalas DataFrame.`, I'd say the only two consistent options would be to also allow using Koalas DataFrames in such SQL queries or to drop the method altogether.

Maybe @rxin also has an opinion on this since he introduced the `ks.sql()` method in #256.
Great discussion. Here are my thoughts: my vision is that there is a nontrivial chance Koalas becomes the de facto standard "big data science" API over time.

That is, when data scientists work with big data, they use Koalas. They know it runs on Spark (or maybe even some other thing in the future), but they don't necessarily know the PySpark API because that's all they have learned (when they transition from pandas over to Koalas).

To accomplish that, it's necessary to make sure from a functionality point of view, Koalas API is a superset of PySpark's (at least when it comes to the structured APIs; I'd not consider RDD or MLlib here).

A lot of production data sits in data lakes (S3, Azure blob store, HDFS, etc). If a data scientist wants to work with some production data, the first thing he/she will likely do is to directly load in that path, table, or run a SQL query. In this context, I think it's necessary to have the "sql" function.

Similarly, in order to work with big data, some additional functionalities that are unique to PySpark but not pandas are critical, e.g. "cache", "repartition", even though some data scientists will never use that.

One thing you could argue is that since Koalas runs on top of Spark, and it is trivial to convert between Spark DataFrames to Koalas DataFrames, why not just leave all the Spark specific functions in Spark and have users fall back to those?

To answer that question, we should consider the journey of data scientists. Let's use "Alice" as an example:

1. Alice learns how to do data science using pandas from MOOCs
2. Alice works on some real world data science problems with pandas
3. Alice needs to work with some large production dataset in S3. Before Koalas (is mature/popular), she learns that PySpark is the thing to use and she now needs to learn a brand new API, which is daunting. After Koalas (is mature/popular), Alice reads some quick start guide on Koalas and then can immediately start applying most of what she learned from pandas to Koalas.
4. Alice runs into some trouble that are specific to big data. For example, some operation that used to be super fast on tiny amount of data is now super slow on large amount of data. She reads up on how to address those issues. Hypothetically speaking, let's say Alice's more senior coworker tells her she just needs to cache the data to make the workload go faster.


At this point, which one do you think is easier for Alice?

Option 1. Go research and figure out how to do cache. Alice hasn't really been exposed to any of the Spark API yet, so even reading up the Spark API reference doc is daunting.

Option 2. Alice tries to do df.ca and tab autocomplete suggests "cache", which seems like what she wants to do. She confirms that by looking at the reference doc.


My theory is that it would be significantly easier for Alice and other data scientists to just learn from one place and learn one API. And this doesn't stop at just performance optimizations. Accessing data, plotting, ...

Along that direction, I do think it will be super convenient for data scientists to just run SQL queries in Koalas against Koalas DataFrames, without having to learn about `registerTempView`. After all, the main objective of the Koalas API should be to reduce friction and make data scientists more productive.



Those are really good points... 
In any case, making the overall goal clear is important. Summarizing the discussion: we can go in two directions. 

1. Make Koalas mirror pandas as much as possible 1:1. In this scenario, we only implement the gaps in making pandas work on big data. Users familiar with Python DS ecosystem will be able to migrate single node workloads with minimal changes. Also crucially, they can migrate work back to single node trivially. However as popular as pandas is, it still has its quirks. It is not trivial for SQL or R users to pick up.
2. Build the standard big data API, where we augment pandas.  We implement pandas' methods and are careful not to deviate. As we've seen from PySpark, slight differences can really trip up users. Instead of just copying pandas, however, we are user-driven. There are many more users who know SQL than pandas for instance. Combining the two could be extremely powerful - more powerful than pandas. The tradeoff is that moving from pandas to Koalas is no longer bidirectional. 

I think the bidirectional use case is actually really important. Within an organization, there are many patterns for working with big data. For example, many developers still have a strong preference for sampling data and developing locally. If Koalas and pandas are not 1:1, there could be issues with handoffs. 

@tdliu they could run Koalas locally with Spark though :) No setup needed.

BTW on this specific function ... I can think of three ways:

1. Be more "magical" and just automatically import all locals() and globals() DataFrames, and create temporary views for them. This would be by far the most convenient and fairly error prone. So the following will work out of the box:

```python
my_range = ks.range(1000)
ks.sql("select count(*) from my_range")
```

2. Similar to pandasql, require explicitly passing in the list of DataFrames. The above will now look like this:

```python
my_range = ks.range(1000)
ks.sql("select count(*) from my_range", locals())
```

Users can accomplish option 1 by defining a method:

```python
sql = lambda q: ks.sql(q, globals())
```

3. Use the string interpolation @thunterdb outlined above, e.g.

```python
my_range = ks.range(1000)
ks.sql("select count(*) from {my_range}")
```

Hi @rxin, thanks for your extensive thoughts on the topic. Here is my opinion on the suggested approaches:

1. I would definitely prefer this version of the `ks.sql()` API since it directly translates to common SQL which I assume is what users would expect. Also, it would be consistent with Spark SQL's `sql()` method. However, I strongly dislike creating temporary views for all DataFrames out of which the majority might never be used. While this would work for a first proof of concept, in the long term I think it would be cleaner to scan the query for potential DataFrame names (e.g. whatever comes after the `from` statement) and only create temporary views for those DataFrames. Additionally, the use of `locals()` and `globals()`, which would be necessary in this case, can quickly cause problems once users split their code across several Python modules.
2. Personally, that's the option I like the least since it combines a somewhat awkward API (does the majority of users even understand what `locals()` is?) with the same technical issues as above by potentially creating temporary views for all DataFrames. Also, the lambda statement only makes it more complicated in my opinion (remember the case where analysts simply want to run a SQL query instead of getting into software development).
3. What I like about this approach is that it seems the cleanest from a technical perspective. However, it introduces a kind of new SQL dialect which (while easy to learn) would surely confuse most users at first. Also, as described above, I feel like the same could be achieved by parsing the query to find the involved DataFrame names without explicitly marking them with curly brackets.
On the more general vision of Koalas:
- I feel like the priority should be on meeting the expectations of users coming from a pandas background. Augmenting the pandas API with Spark-specific functionality like `.cache()`, or `.sql()` for that matter, would therefore not be an issue as long as no pandas methods with the same name exist.
- A valid objection would be not being able to move back from Koalas to pandas. However, it should be assessed how common this case actually is since I believe people do much rather move from small data to big data than the other way around. Also, as pointed out by @rxin, it is still perfectly possible to run Koalas on any edge device with a local Spark installation. At last, for Koalas to be feasible in real-world use cases, I argue that it is essential to support at least a small subset of Spark-specific methods like `.cache()` without which Koalas would rather be some nice tech demo than anything usable in practice. So while it would be great to have full API compatibility in theory, I don't see how this could be achieved in practice and, as described above, it might not even be necessary.

Resolving patch:
diff --git a/databricks/koalas/__init__.py b/databricks/koalas/__init__.py
--- a/databricks/koalas/__init__.py
+++ b/databricks/koalas/__init__.py
@@ -40,6 +40,7 @@ def assert_pyspark_version():
 from databricks.koalas.indexes import Index, MultiIndex
 from databricks.koalas.series import Series
 from databricks.koalas.typedef import Col, pandas_wraps
+from databricks.koalas.sql import sql
 
 __all__ = ['read_csv', 'read_parquet', 'to_datetime', 'from_pandas',
            'get_dummies', 'DataFrame', 'Series', 'Index', 'MultiIndex', 'Col', 'pandas_wraps',
diff --git a/databricks/koalas/namespace.py b/databricks/koalas/namespace.py
--- a/databricks/koalas/namespace.py
+++ b/databricks/koalas/namespace.py
@@ -61,29 +61,6 @@ def from_pandas(pobj: Union['pd.DataFrame', 'pd.Series']) -> Union['Series', 'Da
         raise ValueError("Unknown data type: {}".format(type(pobj)))
 
 
-def sql(query: str) -> DataFrame:
-    """
-    Execute a SQL query and return the result as a Koalas DataFrame.
-
-    Parameters
-    ----------
-    query : str
-        the SQL query
-
-    Returns
-    -------
-    DataFrame
-
-    Examples
-    --------
-    >>> ks.sql("select * from range(10) where id > 7")
-       id
-    0   8
-    1   9
-    """
-    return DataFrame(default_session().sql(query))
-
-
 def range(start: int,
           end: Optional[int] = None,
           step: int = 1,
diff --git a/databricks/koalas/sql.py b/databricks/koalas/sql.py
new file mode 100644
--- /dev/null
+++ b/databricks/koalas/sql.py
@@ -0,0 +1,288 @@
+#
+# Copyright (C) 2019 Databricks, Inc.
+#
+# Licensed 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.
+#
+
+import _string
+from typing import Dict, Any, Optional
+import inspect
+import pandas as pd
+
+from pyspark.sql import SparkSession, DataFrame as SDataFrame
+
+from databricks import koalas as ks  # For running doctests and reference resolution in PyCharm.
+from databricks.koalas.utils import default_session
+from databricks.koalas.frame import DataFrame
+from databricks.koalas.series import Series
+
+
+__all__ = ["sql"]
+
+from builtins import globals as builtin_globals
+from builtins import locals as builtin_locals
+
+
+def sql(query: str, globals=None, locals=None, **kwargs) -> DataFrame:
+    """
+    Execute a SQL query and return the result as a Koalas DataFrame.
+
+    This function also supports embedding Python variables (locals, globals, and parameters)
+    in the SQL statement by wrapping them in curly braces. See examples section for details.
+
+    In addition to the locals, globals and parameters, the function will also attempt
+    to determine if the program currently runs in an IPython (or Jupyter) environment
+    and to import the variables from this environment. The variables have the same
+    precedence as globals.
+
+    The following variable types are supported:
+
+    - string
+    - int
+    - float
+    - list, tuple, range of above types
+    - Koalas DataFrame
+    - Koalas Series
+    - pandas DataFrame
+
+    Parameters
+    ----------
+    query : str
+        the SQL query
+    globals : dict, optional
+        the dictionary of global variables, if explicitly set by the user
+    locals : dict, optional
+        the dictionary of local variables, if explicitly set by the user
+    kwargs
+        other variables that the user may want to set manually that can be referenced in the query
+
+    Returns
+    -------
+    Koalas DataFrame
+
+    Examples
+    --------
+
+    Calling a built-in SQL function.
+
+    >>> ks.sql("select * from range(10) where id > 7")
+       id
+    0   8
+    1   9
+
+    A query can also reference a local variable or parameter by wrapping them in curly braces:
+
+    >>> bound1 = 7
+    >>> ks.sql("select * from range(10) where id > {bound1} and id < {bound2}", bound2=9)
+       id
+    0   8
+
+    You can also wrap a DataFrame with curly braces to query it directly. Note that when you do
+    that, the indexes, if any, automatically become top level columns.
+
+    >>> mydf = ks.range(10)
+    >>> x = range(4)
+    >>> ks.sql("SELECT * from {mydf} WHERE id IN {x}")
+       id
+    0   0
+    1   1
+    2   2
+    3   3
+
+    Queries can also be arbitrarily nested in functions:
+
+    >>> def statement():
+    ...     mydf2 = ks.DataFrame({"x": range(2)})
+    ...     return ks.sql("SELECT * from {mydf2}")
+    >>> statement()
+       __index_level_0__  x
+    0                  0  0
+    1                  1  1
+
+    Mixing Koalas and pandas DataFrames in a join operation. Note that the index is dropped.
+
+    >>> ks.sql('''
+    ...   SELECT m1.a, m2.b
+    ...   FROM {table1} m1 INNER JOIN {table2} m2
+    ...   ON m1.key = m2.key
+    ...   ORDER BY m1.a, m2.b''',
+    ...   table1=ks.DataFrame({"a": [1,2], "key": ["a", "b"]}),
+    ...   table2=pd.DataFrame({"b": [3,4,5], "key": ["a", "b", "b"]}))
+       a  b
+    0  1  3
+    1  2  4
+    2  2  5
+    """
+    if globals is None:
+        globals = _get_ipython_scope()
+    _globals = builtin_globals() if globals is None else dict(globals)
+    _locals = builtin_locals() if locals is None else dict(locals)
+    # The default choice is the globals
+    _dict = dict(_globals)
+    # The vars:
+    _scope = _get_local_scope()
+    _dict.update(_scope)
+    # Then the locals
+    _dict.update(_locals)
+    # Highest order of precedence is the locals
+    _dict.update(kwargs)
+    return SQLProcessor(_dict, query, default_session()).execute()
+
+
+def _get_local_scope():
+    # Get 2 scopes above (_get_local_scope -> sql -> ...) to capture the vars there.
+    try:
+        return inspect.stack()[2][0].f_locals
+    except Exception as e:
+        # TODO (rxin, thunterdb): use a more narrow scope exception.
+        # See https://github.com/databricks/koalas/pull/448
+        return {}
+
+
+def _get_ipython_scope():
+    """
+    Tries to extract the dictionary of variables if the program is running
+    in an IPython notebook environment.
+    """
+    try:
+        from IPython import get_ipython
+        shell = get_ipython()
+        return shell.user_ns
+    except Exception as e:
+        # TODO (rxin, thunterdb): use a more narrow scope exception.
+        # See https://github.com/databricks/koalas/pull/448
+        return None
+
+
+# Originally from pymysql package
+_escape_table = [chr(x) for x in range(128)]
+_escape_table[0] = u'\\0'
+_escape_table[ord('\\')] = u'\\\\'
+_escape_table[ord('\n')] = u'\\n'
+_escape_table[ord('\r')] = u'\\r'
+_escape_table[ord('\032')] = u'\\Z'
+_escape_table[ord('"')] = u'\\"'
+_escape_table[ord("'")] = u"\\'"
+
+
+def escape_sql_string(value: str) -> str:
+    """Escapes value without adding quotes.
+
+    >>> escape_sql_string("foo\\nbar")
+    'foo\\\\nbar'
+
+    >>> escape_sql_string("'abc'de")
+    "\\\\'abc\\\\'de"
+
+    >>> escape_sql_string('"abc"de')
+    '\\\\"abc\\\\"de'
+    """
+    return value.translate(_escape_table)
+
+
+class SQLProcessor(object):
+
+    def __init__(self, scope: Dict[str, Any], statement: str, session: SparkSession):
+        self._scope = scope
+        self._statement = statement
+        # All the temporary views created when executing this statement
+        # The key is the name of the variable in {}
+        # The value is the cached Spark Dataframe.
+        self._temp_views = {}  # type: Dict[str, SDataFrame]
+        # All the other variables, converted to a normalized form.
+        # The normalized form is typically a string
+        self._cached_vars = {}  # type: Dict[str, Any]
+        # The SQL statement after:
+        # - all the dataframes have been have been registered as temporary views
+        # - all the values have been converted normalized to equivalent SQL representations
+        self._normalized_statement = None  # type: Optional[str]
+        self._session = session
+
+    def execute(self) -> DataFrame:
+        """
+        Returns a DataFrame for which the SQL statement has been executed by
+        the underlying SQL engine.
+
+        >>> str0 = 'abc'
+        >>> ks.sql("select {str0}")
+           abc
+        0  abc
+
+        >>> str1 = 'abc"abc'
+        >>> str2 = "abc'abc"
+        >>> ks.sql("select {str0}, {str1}, {str2}")
+           abc  abc"abc  abc'abc
+        0  abc  abc"abc  abc'abc
+
+        >>> strs = ['a', 'b']
+        >>> ks.sql("select 'a' in {strs} as cond1, 'c' in {strs} as cond2")
+           cond1  cond2
+        0   True  False
+        """
+        blocks = _string.formatter_parser(self._statement)
+        # TODO: use a string builder
+        res = ""
+        try:
+            for (pre, inner, _, _) in blocks:
+                var_next = "" if inner is None else self._convert(inner)
+                res = res + pre + var_next
+            self._normalized_statement = res
+
+            sdf = self._session.sql(self._normalized_statement)
+        finally:
+            for v in self._temp_views:
+                self._session.catalog.dropTempView(v)
+        return DataFrame(sdf)
+
+    def _convert(self, key) -> Any:
+        """
+        Given a {} key, returns an equivalent SQL representation.
+        This conversion performs all the necessary escaping so that the string
+        returned can be directly injected into the SQL statement.
+        """
+        # Already cached?
+        if key in self._cached_vars:
+            return self._cached_vars[key]
+        # Analyze:
+        if key not in self._scope:
+            raise ValueError("The key {} in the SQL statement was not found in global,"
+                             " local or parameters variables".format(key))
+        var = self._scope[key]
+        fillin = self._convert_var(var)
+        self._cached_vars[key] = fillin
+        return fillin
+
+    def _convert_var(self, var) -> Any:
+        """
+        Converts a python object into a string that is legal SQL.
+        """
+        if isinstance(var, (int, float)):
+            return str(var)
+        if isinstance(var, Series):
+            return self._convert_var(var.to_dataframe())
+        if isinstance(var, pd.DataFrame):
+            return self._convert_var(ks.DataFrame(var))
+        if isinstance(var, DataFrame):
+            df_id = "koalas_" + str(id(var))
+            if df_id not in self._temp_views:
+                sdf = var.to_spark()
+                sdf.createOrReplaceTempView(df_id)
+                self._temp_views[df_id] = sdf
+            return df_id
+        if isinstance(var, str):
+            return '"' + escape_sql_string(var) + '"'
+        if isinstance(var, list):
+            return "(" + ", ".join([self._convert_var(v) for v in var]) + ")"
+        if isinstance(var, (tuple, range)):
+            return self._convert_var(list(var))
+        raise ValueError("Unsupported variable type {}: {}".format(type(var), str(var)))


Test patch:
diff --git a/databricks/koalas/tests/test_sql.py b/databricks/koalas/tests/test_sql.py
new file mode 100644
--- /dev/null
+++ b/databricks/koalas/tests/test_sql.py
@@ -0,0 +1,38 @@
+#
+# Copyright (C) 2019 Databricks, Inc.
+#
+# Licensed 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.
+#
+
+from databricks import koalas as ks
+from databricks.koalas.testing.utils import ReusedSQLTestCase, SQLTestUtils
+
+from pyspark.sql.utils import ParseException
+
+
+class SQLTest(ReusedSQLTestCase, SQLTestUtils):
+
+    def test_error_variable_not_exist(self):
+        msg = 'The key variable_foo in the SQL statement was not found.*'
+        with self.assertRaisesRegex(ValueError, msg):
+            ks.sql('select * from {variable_foo}')
+
+    def test_error_unsupported_type(self):
+        msg = "Unsupported variable type <class 'dict'>: {'a': 1}"
+        with self.assertRaisesRegex(ValueError, msg):
+            some_dict = {'a': 1}
+            ks.sql('select * from {some_dict}')
+
+    def test_error_bad_sql(self):
+        with self.assertRaises(ParseException):
+            ks.sql('this is not valid sql')