Repository: pandas-dev/pandas

Issue:
ENH: support both SQLAlchemy engines and connections
Feature request for supporting also Connection objects instead of only Engines.

Also reported in #8533, #10104 

---

My use case - I often add a week's worth of data (stored in a pandas dataframe) to an existing table. Before I add the data, I want to be able to delete any existing data for this week from the table, but I want the whole thing to be wrapped in a transaction so I'm not left in a mess halfway through.

Here's what I therefore want to be able to do

``` python
# runs a transaction
with engine.begin() as connection:
    r1 = connection.execute(table.delete().where(table.c[WEEK]==week))
    data.to_sql(table.name, connection, if_exists='append')
```

i.e. my change is that I want to be able to pass a SQLAlchemy `Connection` object into `DataFrame.to_sql` instead of an `Engine`. (The underlying `Engine` of the `Connection` object can be accessed as `.engine`, so we should still be able to pull metadata etc)



Discussion:
Is it possible to provide this engine yourself by doing the following in the `with` context: `data.to_sql(table.name, connection.engine, if_exists='append')`?

Because then it is the question: do we ask the user to do this themselves, or do we provide the convenience (but also clutter in interface) to also accept a connection?

Can this issue be closed? I agree w/ jorisvandenbossche that a change to pandas isn't necessary--you can easily access the .engine property from a SQLAlchemy connection object:

```
from sqlalchemy import create_engine
engine = create_engine('sqlite:///file.db')
connection = engine.connect()
# 'connection.engine' now refers to a readily usable engine object
```

You can indeed easily access the engine, but I don't really know how then the atomicity is handled (the reason to use a transaction in this case). Accessing the engine and using that, within the `with engine.begin() as con` context, does this defeat the purpose of that, or does this keep it atomic? 
Anybody who knows this?

It's already done as a transaction:

https://github.com/pydata/pandas/blob/master/pandas/io/sql.py#L633-L644

yes, but that is for _one_ `to_sql` call, while here @maxgrenderjones wants to do multiple such calls in one transaction

I tested it and running a statement as

``` python
conn.engine.execute(sql)
```

does _not_ run it within the transaction (in other words, the statement would not be rolledback as needed)

One possibility would be to a `connection` kwarg to `to_sql`.  So the call would be:

``` python
data.to_sql(table.name, engine, connection=connection, if_exists='append')
```

`connection` could be saved in the appropriate object and, if not `None`, used in `run_transaction` instead of creating a new connection.  Thoughts?

That would work. Still feels like it might be easier to pass either a `connection` or an `engine`, but I guess the `connection` option perhaps _should_ be a little bit awkward, as it requires you to understand that you need to do your own transaction wrapping.

@artemyk Are you sure that providing a `connection` _does_ run it within a transaction? As `connection.begin()` instead of `engine.begin()` does also run a transaction on its own. 
Or does it work if the `to_sql(..., connection, ...)` is nested in another connection begin? (http://docs.sqlalchemy.org/en/latest/core/connections.html#nesting-of-transaction-blocks)

@jorisvandenbossche The connection contextmanager (i.e. `with engine.begin() as connection:`) does run things within a transaction --- that's what we use now in `run_transaction`, and it passes tests for properly commiting/rollingback.  
You are right that `connection.begin()` also is a way to run a transaction, that might be the right way to do nested transactions. Is that something we'd want to support?
BTW, there already seems to be a way to pass in a cursor --- see `cur` and `is_cursor` parameters some functions take.  I'm not sure if that is deprecated or not, but seems clearly related to the functionality being requested here.  `to_sql` doesn't support it though, nor does `run_transaction` check to see if the connection is a cursor.

@artemyk, what is the advantage of using a sqlalchemy engine instead of a connection? Going through the io/sql.py code, we really only use `has_tables` and `table_names` functions of the engine, which is not that much of an issue since we can recover the engine from the connection (using the engine attribute of the connection).  

In fact, it seems to make more sense for the scope of pandas to operate at the connection level only. That way the user can have full control over sqlalchemy connection issues such as isolation levels, nested transactions/savepoints, etc (see [sqlalchemy engines and connections](http://docs.sqlalchemy.org/en/latest/core/connections.html)).

Maybe we could default to use connections and create our own connection in case the user passes an engine? That way this would backwards compatible, but still allow for more advanced usage.

@tr11 that sounds like a good idea to me.  @jorisvandenbossche thoughts?

OK, I really needed this right now, so I just hacked a quick version (https://github.com/tr11/pandas/commit/fe6fee394f78a908e74cf3675668a4475db3c616) that passes all the tests with engines or connections. I will add standard tests that use connection parameters and a couple other ones that show the new things that can be done with transaction rollbacks, using temporary tables, etc.  If there is any interest in merging this, I can also update the documentation before submitting a pull request.

@tr11 Sorry for the slow reaction here! 
Indeed, that looks like a good idea. If you would still want to make a PR for this (with tests and examples/docs), or give some comments on the newly opened PR #10105, that would be really great

I've updated the PR with these changes, and added tests


Resolving patch:
diff --git a/pandas/io/sql.py b/pandas/io/sql.py
--- a/pandas/io/sql.py
+++ b/pandas/io/sql.py
@@ -38,7 +38,7 @@ class DatabaseError(IOError):
 _SQLALCHEMY_INSTALLED = None
 
 
-def _is_sqlalchemy_engine(con):
+def _is_sqlalchemy_connectable(con):
     global _SQLALCHEMY_INSTALLED
     if _SQLALCHEMY_INSTALLED is None:
         try:
@@ -62,7 +62,7 @@ def compile_big_int_sqlite(type_, compiler, **kw):
 
     if _SQLALCHEMY_INSTALLED:
         import sqlalchemy
-        return isinstance(con, sqlalchemy.engine.Engine)
+        return isinstance(con, sqlalchemy.engine.Connectable)
     else:
         return False
 
@@ -139,7 +139,7 @@ def execute(sql, con, cur=None, params=None):
     ----------
     sql : string
         Query to be executed
-    con : SQLAlchemy engine or sqlite3 DBAPI2 connection
+    con : SQLAlchemy connectable(engine/connection) or sqlite3 DBAPI2 connection
         Using SQLAlchemy makes it possible to use any DB supported by that
         library.
         If a DBAPI2 object, only sqlite3 is supported.
@@ -282,14 +282,14 @@ def read_sql_table(table_name, con, schema=None, index_col=None,
                    chunksize=None):
     """Read SQL database table into a DataFrame.
 
-    Given a table name and an SQLAlchemy engine, returns a DataFrame.
+    Given a table name and an SQLAlchemy connectable, returns a DataFrame.
     This function does not support DBAPI connections.
 
     Parameters
     ----------
     table_name : string
         Name of SQL table in database
-    con : SQLAlchemy engine
+    con : SQLAlchemy connectable
         Sqlite DBAPI connection mode not supported
     schema : string, default None
         Name of SQL schema in database to query (if database flavor
@@ -328,9 +328,9 @@ def read_sql_table(table_name, con, schema=None, index_col=None,
     read_sql
 
     """
-    if not _is_sqlalchemy_engine(con):
+    if not _is_sqlalchemy_connectable(con):
         raise NotImplementedError("read_sql_table only supported for "
-                                  "SQLAlchemy engines.")
+                                  "SQLAlchemy connectable.")
     import sqlalchemy
     from sqlalchemy.schema import MetaData
     meta = MetaData(con, schema=schema)
@@ -362,7 +362,7 @@ def read_sql_query(sql, con, index_col=None, coerce_float=True, params=None,
     ----------
     sql : string
         SQL query to be executed
-    con : SQLAlchemy engine or sqlite3 DBAPI2 connection
+    con : SQLAlchemy connectable(engine/connection) or sqlite3 DBAPI2 connection
         Using SQLAlchemy makes it possible to use any DB supported by that
         library.
         If a DBAPI2 object, only sqlite3 is supported.
@@ -420,7 +420,7 @@ def read_sql(sql, con, index_col=None, coerce_float=True, params=None,
     ----------
     sql : string
         SQL query to be executed or database table name.
-    con : SQLAlchemy engine or DBAPI2 connection (fallback mode)
+    con : SQLAlchemy connectable(engine/connection) or DBAPI2 connection (fallback mode)
         Using SQLAlchemy makes it possible to use any DB supported by that
         library.
         If a DBAPI2 object, only sqlite3 is supported.
@@ -504,14 +504,14 @@ def to_sql(frame, name, con, flavor='sqlite', schema=None, if_exists='fail',
     frame : DataFrame
     name : string
         Name of SQL table
-    con : SQLAlchemy engine or sqlite3 DBAPI2 connection
+    con : SQLAlchemy connectable(engine/connection) or sqlite3 DBAPI2 connection
         Using SQLAlchemy makes it possible to use any DB supported by that
         library.
         If a DBAPI2 object, only sqlite3 is supported.
     flavor : {'sqlite', 'mysql'}, default 'sqlite'
-        The flavor of SQL to use. Ignored when using SQLAlchemy engine.
+        The flavor of SQL to use. Ignored when using SQLAlchemy connectable.
         'mysql' is deprecated and will be removed in future versions, but it
-        will be further supported through SQLAlchemy engines.
+        will be further supported through SQLAlchemy connectables.
     schema : string, default None
         Name of SQL schema in database to write to (if database flavor
         supports this). If None, use default schema (default).
@@ -557,14 +557,14 @@ def has_table(table_name, con, flavor='sqlite', schema=None):
     ----------
     table_name: string
         Name of SQL table
-    con: SQLAlchemy engine or sqlite3 DBAPI2 connection
+    con: SQLAlchemy connectable(engine/connection) or sqlite3 DBAPI2 connection
         Using SQLAlchemy makes it possible to use any DB supported by that
         library.
         If a DBAPI2 object, only sqlite3 is supported.
     flavor: {'sqlite', 'mysql'}, default 'sqlite'
-        The flavor of SQL to use. Ignored when using SQLAlchemy engine.
+        The flavor of SQL to use. Ignored when using SQLAlchemy connectable.
         'mysql' is deprecated and will be removed in future versions, but it
-        will be further supported through SQLAlchemy engines.
+        will be further supported through SQLAlchemy connectables.
     schema : string, default None
         Name of SQL schema in database to write to (if database flavor supports
         this). If None, use default schema (default).
@@ -581,7 +581,7 @@ def has_table(table_name, con, flavor='sqlite', schema=None):
 
 _MYSQL_WARNING = ("The 'mysql' flavor with DBAPI connection is deprecated "
                   "and will be removed in future versions. "
-                  "MySQL will be further supported with SQLAlchemy engines.")
+                  "MySQL will be further supported with SQLAlchemy connectables.")
 
 
 def pandasSQL_builder(con, flavor=None, schema=None, meta=None,
@@ -592,7 +592,7 @@ def pandasSQL_builder(con, flavor=None, schema=None, meta=None,
     """
     # When support for DBAPI connections is removed,
     # is_cursor should not be necessary.
-    if _is_sqlalchemy_engine(con):
+    if _is_sqlalchemy_connectable(con):
         return SQLDatabase(con, schema=schema, meta=meta)
     else:
         if flavor == 'mysql':
@@ -637,7 +637,7 @@ def exists(self):
 
     def sql_schema(self):
         from sqlalchemy.schema import CreateTable
-        return str(CreateTable(self.table).compile(self.pd_sql.engine))
+        return str(CreateTable(self.table).compile(self.pd_sql.connectable))
 
     def _execute_create(self):
         # Inserting table into database, add to MetaData object
@@ -982,11 +982,11 @@ class PandasSQL(PandasObject):
     """
 
     def read_sql(self, *args, **kwargs):
-        raise ValueError("PandasSQL must be created with an SQLAlchemy engine"
+        raise ValueError("PandasSQL must be created with an SQLAlchemy connectable"
                          " or connection+sql flavor")
 
     def to_sql(self, *args, **kwargs):
-        raise ValueError("PandasSQL must be created with an SQLAlchemy engine"
+        raise ValueError("PandasSQL must be created with an SQLAlchemy connectable"
                          " or connection+sql flavor")
 
 
@@ -997,8 +997,8 @@ class SQLDatabase(PandasSQL):
 
     Parameters
     ----------
-    engine : SQLAlchemy engine
-        Engine to connect with the database. Using SQLAlchemy makes it
+    engine : SQLAlchemy connectable
+        Connectable to connect with the database. Using SQLAlchemy makes it
         possible to use any DB supported by that library.
     schema : string, default None
         Name of SQL schema in database to write to (if database flavor
@@ -1011,19 +1011,24 @@ class SQLDatabase(PandasSQL):
     """
 
     def __init__(self, engine, schema=None, meta=None):
-        self.engine = engine
+        self.connectable = engine
         if not meta:
             from sqlalchemy.schema import MetaData
-            meta = MetaData(self.engine, schema=schema)
+            meta = MetaData(self.connectable, schema=schema)
 
         self.meta = meta
 
+    @contextmanager
     def run_transaction(self):
-        return self.engine.begin()
+        with self.connectable.begin() as tx:
+            if hasattr(tx, 'execute'):
+                yield tx
+            else:
+                yield self.connectable
 
     def execute(self, *args, **kwargs):
-        """Simple passthrough to SQLAlchemy engine"""
-        return self.engine.execute(*args, **kwargs)
+        """Simple passthrough to SQLAlchemy connectable"""
+        return self.connectable.execute(*args, **kwargs)
 
     def read_table(self, table_name, index_col=None, coerce_float=True,
                    parse_dates=None, columns=None, schema=None,
@@ -1191,7 +1196,13 @@ def to_sql(self, frame, name, if_exists='fail', index=True,
         table.create()
         table.insert(chunksize)
         # check for potentially case sensitivity issues (GH7815)
-        if name not in self.engine.table_names(schema=schema or self.meta.schema):
+        engine = self.connectable.engine
+        with self.connectable.connect() as conn:
+            table_names = engine.table_names(
+                schema=schema or self.meta.schema,
+                connection=conn,
+            )
+        if name not in table_names:
             warnings.warn("The provided table name '{0}' is not found exactly "
                           "as such in the database after writing the table, "
                           "possibly due to case sensitivity issues. Consider "
@@ -1202,7 +1213,11 @@ def tables(self):
         return self.meta.tables
 
     def has_table(self, name, schema=None):
-        return self.engine.has_table(name, schema or self.meta.schema)
+        return self.connectable.run_callable(
+            self.connectable.dialect.has_table,
+            name,
+            schema or self.meta.schema,
+        )
 
     def get_table(self, table_name, schema=None):
         schema = schema or self.meta.schema
@@ -1221,7 +1236,7 @@ def get_table(self, table_name, schema=None):
 
     def drop_table(self, table_name, schema=None):
         schema = schema or self.meta.schema
-        if self.engine.has_table(table_name, schema):
+        if self.has_table(table_name, schema):
             self.meta.reflect(only=[table_name], schema=schema)
             self.get_table(table_name, schema).drop()
             self.meta.clear()
@@ -1610,12 +1625,12 @@ def get_schema(frame, name, flavor='sqlite', keys=None, con=None, dtype=None):
     name : string
         name of SQL table
     flavor : {'sqlite', 'mysql'}, default 'sqlite'
-        The flavor of SQL to use. Ignored when using SQLAlchemy engine.
+        The flavor of SQL to use. Ignored when using SQLAlchemy connectable.
         'mysql' is deprecated and will be removed in future versions, but it
         will be further supported through SQLAlchemy engines.
     keys : string or sequence
         columns to use a primary key
-    con: an open SQL database connection object or an SQLAlchemy engine
+    con: an open SQL database connection object or a SQLAlchemy connectable
         Using SQLAlchemy makes it possible to use any DB supported by that
         library.
         If a DBAPI2 object, only sqlite3 is supported.
@@ -1673,8 +1688,8 @@ def write_frame(frame, name, con, flavor='sqlite', if_exists='fail', **kwargs):
 
     - With ``to_sql`` the index is written to the sql database by default. To
       keep the behaviour this function you need to specify ``index=False``.
-    - The new ``to_sql`` function supports sqlalchemy engines to work with
-      different sql flavors.
+    - The new ``to_sql`` function supports sqlalchemy connectables to work
+      with different sql flavors.
 
     See also
     --------


Test patch:
diff --git a/pandas/io/tests/test_sql.py b/pandas/io/tests/test_sql.py
--- a/pandas/io/tests/test_sql.py
+++ b/pandas/io/tests/test_sql.py
@@ -9,7 +9,8 @@
     - `TestSQLiteFallbackApi`: test the public API with a sqlite DBAPI connection
 - Tests for the different SQL flavors (flavor specific type conversions)
     - Tests for the sqlalchemy mode: `_TestSQLAlchemy` is the base class with
-      common methods, the different tested flavors (sqlite3, MySQL, PostgreSQL)
+      common methods, `_TestSQLAlchemyConn` tests the API with a SQLAlchemy
+      Connection object. The different tested flavors (sqlite3, MySQL, PostgreSQL)
       derive from the base class
     - Tests for the fallback mode (`TestSQLiteFallback` and `TestMySQLLegacy`)
 
@@ -43,6 +44,8 @@
     import sqlalchemy
     import sqlalchemy.schema
     import sqlalchemy.sql.sqltypes as sqltypes
+    from sqlalchemy.ext import declarative
+    from sqlalchemy.orm import session as sa_session
     SQLALCHEMY_INSTALLED = True
 except ImportError:
     SQLALCHEMY_INSTALLED = False
@@ -867,6 +870,31 @@ def test_sqlalchemy_type_mapping(self):
         self.assertTrue(isinstance(table.table.c['time'].type, sqltypes.DateTime))
 
 
+class _EngineToConnMixin(object):
+    """
+    A mixin that causes setup_connect to create a conn rather than an engine.
+    """
+
+    def setUp(self):
+        super(_EngineToConnMixin, self).setUp()
+        engine = self.conn
+        conn = engine.connect()
+        self.__tx = conn.begin()
+        self.pandasSQL = sql.SQLDatabase(conn)
+        self.__engine = engine
+        self.conn = conn
+
+    def tearDown(self):
+        self.__tx.rollback()
+        self.conn.close()
+        self.conn = self.__engine
+        self.pandasSQL = sql.SQLDatabase(self.__engine)
+
+
+class TestSQLApiConn(_EngineToConnMixin, TestSQLApi):
+    pass
+
+
 class TestSQLiteFallbackApi(_TestSQLApi):
     """
     Test the public sqlite connection fallback API
@@ -1003,9 +1031,6 @@ def setup_connect(self):
         except sqlalchemy.exc.OperationalError:
             raise nose.SkipTest("Can't connect to {0} server".format(self.flavor))
 
-    def tearDown(self):
-        raise NotImplementedError()
-
     def test_aread_sql(self):
         self._read_sql_iris()
 
@@ -1359,9 +1384,58 @@ def test_double_precision(self):
         self.assertTrue(isinstance(col_dict['i32'].type, sqltypes.Integer))
         self.assertTrue(isinstance(col_dict['i64'].type, sqltypes.BigInteger))
 
+    def test_connectable_issue_example(self):
+        # This tests the example raised in issue
+        # https://github.com/pydata/pandas/issues/10104
+
+        def foo(connection):
+            query = 'SELECT test_foo_data FROM test_foo_data'
+            return sql.read_sql_query(query, con=connection)
+
+        def bar(connection, data):
+            data.to_sql(name='test_foo_data', con=connection, if_exists='append')
+
+        def main(connectable):
+            with connectable.connect() as conn:
+                with conn.begin():
+                    foo_data = conn.run_callable(foo)
+                    conn.run_callable(bar, foo_data)
+
+        DataFrame({'test_foo_data': [0, 1, 2]}).to_sql('test_foo_data', self.conn)
+        main(self.conn)
+
+    def test_temporary_table(self):
+        test_data = u'Hello, World!'
+        expected = DataFrame({'spam': [test_data]})
+        Base = declarative.declarative_base()
+
+        class Temporary(Base):
+            __tablename__ = 'temp_test'
+            __table_args__ = {'prefixes': ['TEMPORARY']}
+            id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
+            spam = sqlalchemy.Column(sqlalchemy.Unicode(30), nullable=False)
+
+        Session = sa_session.sessionmaker(bind=self.conn)
+        session = Session()
+        with session.transaction:
+            conn = session.connection()
+            Temporary.__table__.create(conn)
+            session.add(Temporary(spam=test_data))
+            session.flush()
+            df = sql.read_sql_query(
+                sql=sqlalchemy.select([Temporary.spam]),
+                con=conn,
+            )
+
+        tm.assert_frame_equal(df, expected)
+
+
+class _TestSQLAlchemyConn(_EngineToConnMixin, _TestSQLAlchemy):
+    def test_transactions(self):
+        raise nose.SkipTest("Nested transactions rollbacks don't work with Pandas")
 
 
-class TestSQLiteAlchemy(_TestSQLAlchemy):
+class _TestSQLiteAlchemy(object):
     """
     Test the sqlalchemy backend against an in-memory sqlite database.
 
@@ -1378,8 +1452,8 @@ def setup_driver(cls):
         cls.driver = None
 
     def tearDown(self):
+        super(_TestSQLiteAlchemy, self).tearDown()
         # in memory so tables should not be removed explicitly
-        pass
 
     def test_default_type_conversion(self):
         df = sql.read_sql_table("types_test_data", self.conn)
@@ -1417,7 +1491,7 @@ def test_bigint_warning(self):
             self.assertEqual(len(w), 0, "Warning triggered for other table")
 
 
-class TestMySQLAlchemy(_TestSQLAlchemy):
+class _TestMySQLAlchemy(object):
     """
     Test the sqlalchemy backend against an MySQL database.
 
@@ -1438,6 +1512,7 @@ def setup_driver(cls):
             raise nose.SkipTest('pymysql not installed')
 
     def tearDown(self):
+        super(_TestMySQLAlchemy, self).tearDown()
         c = self.conn.execute('SHOW TABLES')
         for table in c.fetchall():
             self.conn.execute('DROP TABLE %s' % table[0])
@@ -1491,7 +1566,7 @@ def test_read_procedure(self):
         tm.assert_frame_equal(df, res2)
 
 
-class TestPostgreSQLAlchemy(_TestSQLAlchemy):
+class _TestPostgreSQLAlchemy(object):
     """
     Test the sqlalchemy backend against an PostgreSQL database.
 
@@ -1512,6 +1587,7 @@ def setup_driver(cls):
             raise nose.SkipTest('psycopg2 not installed')
 
     def tearDown(self):
+        super(_TestPostgreSQLAlchemy, self).tearDown()
         c = self.conn.execute(
             "SELECT table_name FROM information_schema.tables"
             " WHERE table_schema = 'public'")
@@ -1563,15 +1639,18 @@ def test_schema_support(self):
 
         ## specifying schema in user-provided meta
 
-        engine2 = self.connect()
-        meta = sqlalchemy.MetaData(engine2, schema='other')
-        pdsql = sql.SQLDatabase(engine2, meta=meta)
-        pdsql.to_sql(df, 'test_schema_other2', index=False)
-        pdsql.to_sql(df, 'test_schema_other2', index=False, if_exists='replace')
-        pdsql.to_sql(df, 'test_schema_other2', index=False, if_exists='append')
-        res1 = sql.read_sql_table('test_schema_other2', self.conn, schema='other')
-        res2 = pdsql.read_table('test_schema_other2')
-        tm.assert_frame_equal(res1, res2)
+        # The schema won't be applied on another Connection
+        # because of transactional schemas
+        if isinstance(self.conn, sqlalchemy.engine.Engine):
+            engine2 = self.connect()
+            meta = sqlalchemy.MetaData(engine2, schema='other')
+            pdsql = sql.SQLDatabase(engine2, meta=meta)
+            pdsql.to_sql(df, 'test_schema_other2', index=False)
+            pdsql.to_sql(df, 'test_schema_other2', index=False, if_exists='replace')
+            pdsql.to_sql(df, 'test_schema_other2', index=False, if_exists='append')
+            res1 = sql.read_sql_table('test_schema_other2', self.conn, schema='other')
+            res2 = pdsql.read_table('test_schema_other2')
+            tm.assert_frame_equal(res1, res2)
 
     def test_datetime_with_time_zone(self):
         # Test to see if we read the date column with timezones that
@@ -1587,6 +1666,31 @@ def test_datetime_with_time_zone(self):
         # "2000-06-01 00:00:00-07:00" should convert to "2000-06-01 07:00:00"
         self.assertEqual(df.DateColWithTz[1], Timestamp('2000-06-01 07:00:00'))
 
+
+class TestMySQLAlchemy(_TestMySQLAlchemy, _TestSQLAlchemy):
+    pass
+
+
+class TestMySQLAlchemyConn(_TestMySQLAlchemy, _TestSQLAlchemyConn): 
+    pass
+
+
+class TestPostgreSQLAlchemy(_TestPostgreSQLAlchemy, _TestSQLAlchemy):
+    pass
+
+
+class TestPostgreSQLAlchemyConn(_TestPostgreSQLAlchemy, _TestSQLAlchemyConn):
+    pass
+
+
+class TestSQLiteAlchemy(_TestSQLiteAlchemy, _TestSQLAlchemy):
+    pass
+
+
+class TestSQLiteAlchemyConn(_TestSQLiteAlchemy, _TestSQLAlchemyConn):
+    pass
+
+
 #------------------------------------------------------------------------------
 #--- Test Sqlite / MySQL fallback