Repository: ibis-project/ibis

Issue:
Support SQLAlchemy 1.4
Looks like `sqlalchemy.Binary` has been removed in 1.4, released in conda-forge 4 hours ago, and it's breaking Ibis.

There are some other issues in SQLAlchemy, and also, our `setup.py` requires old versions, and 1.3.7 is not supported.


Discussion:
https://docs.sqlalchemy.org/en/14/changelog/changelog_14.html#change-214869a306f72f33237772d33fc332ec

Resolving patch:
diff --git a/ibis/backends/base_sqlalchemy/alchemy.py b/ibis/backends/base_sqlalchemy/alchemy.py
--- a/ibis/backends/base_sqlalchemy/alchemy.py
+++ b/ibis/backends/base_sqlalchemy/alchemy.py
@@ -44,7 +44,7 @@
     dt.Date: sa.Date,
     dt.Time: sa.Time,
     dt.Boolean: sa.Boolean,
-    dt.Binary: sa.Binary,
+    dt.Binary: sa.LargeBinary,
     dt.String: sa.Text,
     dt.Decimal: sa.NUMERIC,
     # Mantissa-based
@@ -245,7 +245,7 @@ def sa_string(_, satype, nullable=True):
     return dt.String(nullable=nullable)
 
 
-@dt.dtype.register(SQLAlchemyDialect, sa.types.Binary)
+@dt.dtype.register(SQLAlchemyDialect, sa.LargeBinary)
 def sa_binary(_, satype, nullable=True):
     return dt.Binary(nullable=nullable)
 
@@ -291,7 +291,7 @@ def schema_from_table(table, schema=None):
     """
     schema = schema if schema is not None else {}
     pairs = []
-    for name, column in table.columns.items():
+    for name, column in zip(table.columns.keys(), table.columns):
         if name in schema:
             dtype = dt.dtype(schema[name])
         else:
@@ -548,7 +548,7 @@ def _translate_case(t, cases, results, default):
     whens = zip(case_args, result_args)
     default = t.translate(default)
 
-    return sa.case(whens, else_=default)
+    return sa.case(list(whens), else_=default)
 
 
 def _negate(t, expr):
@@ -1347,7 +1347,12 @@ def _compile_subqueries(self):
     def _compile_table_set(self):
         if self.table_set is not None:
             helper = _AlchemyTableSet(self, self.table_set)
-            return helper.get_result()
+            result = helper.get_result()
+            if isinstance(result, sql.selectable.Select) and hasattr(
+                result, 'subquery'
+            ):
+                return result.subquery()
+            return result
         else:
             return None
 
diff --git a/ibis/backends/mysql/compiler.py b/ibis/backends/mysql/compiler.py
--- a/ibis/backends/mysql/compiler.py
+++ b/ibis/backends/mysql/compiler.py
@@ -107,7 +107,7 @@ def _cast(t, expr):
     if typ.equals(dt.binary):
         #  decode yields a column of memoryview which is annoying to deal with
         # in pandas. CAST(expr AS BYTEA) is correct and returns byte strings.
-        return sa.cast(sa_arg, sa.Binary())
+        return sa.cast(sa_arg, sa.LargeBinary())
 
     return sa.cast(sa_arg, sa_type)
 
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -15,16 +15,18 @@
 
 VERSION = sys.version_info.major, sys.version_info.minor
 
-impala_requires = ['hdfs>=2.0.16', 'sqlalchemy>=1.1,<1.3.7', 'requests']
+impala_requires = ['hdfs>=2.0.16', 'sqlalchemy>=1.3', 'requests']
 impala_requires.append('impyla[kerberos]>=0.15.0')
 
-sqlite_requires = ['sqlalchemy>=1.1,<1.3.7']
+sqlite_requires = ['sqlalchemy>=1.3']
 postgres_requires = sqlite_requires + ['psycopg2']
 mysql_requires = sqlite_requires + ['pymysql']
 
 kerberos_requires = ['requests-kerberos']
 visualization_requires = ['graphviz']
 clickhouse_requires = [
+    'sqlalchemy<1.4',
+    'clickhouse-sqlalchemy',
     'clickhouse-driver>=0.1.3',
     'clickhouse-cityhash',
 ]


Test patch:
diff --git a/ibis/backends/postgres/tests/test_functions.py b/ibis/backends/postgres/tests/test_functions.py
--- a/ibis/backends/postgres/tests/test_functions.py
+++ b/ibis/backends/postgres/tests/test_functions.py
@@ -1255,11 +1255,10 @@ def test_anti_join(t, s):
     t_a, s_a = t.op().sqla_table.alias('t0'), s.op().sqla_table.alias('t1')
     expr = t.anti_join(s, t.id == s.id)
     result = expr.compile().compile(compile_kwargs={'literal_binds': True})
-    expected = sa.select([sa.column('id'), sa.column('name')]).select_from(
-        sa.select([t_a.c.id, t_a.c.name]).where(
-            ~(sa.exists(sa.select([1]).where(t_a.c.id == s_a.c.id)))
-        )
+    base = sa.select([t_a.c.id, t_a.c.name]).where(
+        ~sa.exists(sa.select([1]).where(t_a.c.id == s_a.c.id))
     )
+    expected = sa.select([base.c.id, base.c.name])
     assert str(result) == str(expected)