Repository: ibis-project/ibis

Issue:
bug: Can't load tables from non-default DB
### What happened?

Hi 👋 

We have several different databases+schemas inside of our Snowflake account, and it's very common to need to join two tables in different databases. Snowflake SQL makes this pretty straightforward, you just do something like:

```sql
SELECT tb1.*, tb2.*
FROM <db1>.<schema1>.<table1> tb1
INNER JOIN <db2>.<schema2>.<table2> tb2 on tb1.id = tb2.id
```

However when trying to do this with Ibis I'm getting errors. I set up the first database+schema as the default when making my connection string:
```python
>>> connection = ibis.snowflake.connect(
...     user="<username>",
...     password="<password>",
...     account="<url>",
...     database="<db1>/<schema1>",
...     warehouse="<warehouse>"
... )
```
This works for getting `<table1>` with just `connection.table("<table1>")`. However, when I try to get the other table in the other database it errors:
```python
>>> connection.table(
...     "<table2>", 
...     database="<db2>", 
...     schema="<schema2>"
... )
TypeError: BaseBackend.database() got an unexpected keyword argument 'database'
```

I can't make a minimum reproducible example of the problem since it depends on access to our snowflake instance, but I did reproduce the issue using the postgres backend and a testing DB:

```python
import ibis
import psycopg2
import testing.postgresql

from sqlalchemy.exc import NoSuchTableError

ibis.options.interactive = True

with testing.postgresql.Postgresql() as db:
    # Create the two databases, db1 and db2
    url = db.url()
    conn = psycopg2.connect(**db.dsn())
    conn.autocommit = True
    cur = conn.cursor()
    cur.execute("create database db1")
    cur.execute("create database db2")
    cur.close()
    conn.close()

    # Make a small table in db1 named test_db1
    conn = psycopg2.connect(**{**db.dsn(), **{"database": "db1"}})
    conn.autocommit = True
    cur = conn.cursor()
    cur.execute("create table test_db1 (id serial primary key, num integer);")
    cur.execute("insert into test_db1 (id, num) values (1, 1)")
    cur.execute("insert into test_db1 (id, num) values (2, 2)")
    cur.execute("select * from test_db1")
    cur.close()
    conn.close()

    # Make a small table in db2 named test_db2
    conn = psycopg2.connect(**{**db.dsn(), **{"database": "db2"}})
    conn.autocommit = True
    cur = conn.cursor()
    cur.execute("create table test_db2 (id serial primary key, num integer);")
    cur.execute("insert into test_db2 (id, num) values (1, 3)")
    cur.execute("insert into test_db2 (id, num) values (2, 4)")
    cur.close()
    conn.close()

    # Connect to db1 with ibis
    connection = ibis.postgres.connect(**{**db.dsn(), **{"database": "db1"}})

    # Getting a table in the database we connected to works
    test_db1 = connection.table("test_db1")

    # Trying to get a table in the other database fails because it looks like
    # something in the base backend was deprecated and not replaced in the
    # connector backends:
    try:
        test_db2 = connection.table("test_db2", database="db2")
    except TypeError as e:
        print(e,"\n")

    # This claims to change the database but then can't find the table
    try:
        connection.database("db2").table("test_db2")
    except NoSuchTableError as e:
        print("NoSuchTable: test_db2\n")

    # Setting up a separate connection to the other db works
    connection_2 = ibis.postgres.connect(**{**db.dsn(), **{"database": "db2"}})
    print(connection_2.table("test_db2"))
```
The above script uses the following dependencies:
* python=3.10 
* ibis-framework==4.1.0 
* ibis-postgres==4.1.0 
* postgresql==15.2 
* psycopg2==2.9.3 
* testing.postgresql==1.3.0 

I've pasted the output I get from this script in the "logs" section. 

Any thoughts or advice? I'm new at this so I could just be doing something wrong. Thanks!


### What version of ibis are you using?

4.1.0

### What backend(s) are you using, if any?

Snowflake, postgres

### Relevant log output

```sh
FutureWarning: `BaseBackend.database` is deprecated; use equivalent methods in the backend
  return self.database(database=database).table(
BaseBackend.database() got an unexpected keyword argument 'database' 

/Users/pyw137/repos/ibis_exploration/test_multiple_dbs.py:57: FutureWarning: `BaseBackend.database` is deprecated; use equivalent methods in the backend
  connection.database("db2").table("test_db2")
NoSuchTable: test_db2

┏━━━━━━━━┳━━━━━━━┓
┃ id     ┃ num   ┃
┡━━━━━━━━╇━━━━━━━┩
│ !int32 │ int32 │
├────────┼───────┤
│      1 │     3 │
│      2 │     4 │
└────────┴───────┘
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
Hi @AndrewRook, thanks for the wonderful issue description and reproducible examples!

This definitely looks like a bug. Slated for 5.0, which should be out hopefully in a week or two!

Resolving patch:
diff --git a/ibis/backends/base/sql/alchemy/__init__.py b/ibis/backends/base/sql/alchemy/__init__.py
--- a/ibis/backends/base/sql/alchemy/__init__.py
+++ b/ibis/backends/base/sql/alchemy/__init__.py
@@ -478,16 +478,8 @@ def table(
             Table expression
         """
         if database is not None and database != self.current_database:
-            return self.database(database=database).table(
-                name=name,
-                database=database,
-                schema=schema,
-            )
-        sqla_table = self._get_sqla_table(
-            name,
-            database=database,
-            schema=schema,
-        )
+            return self.database(name=database).table(name=name, schema=schema)
+        sqla_table = self._get_sqla_table(name, database=database, schema=schema)
         return self._sqla_table_to_expr(sqla_table)
 
     def insert(
diff --git a/ibis/backends/snowflake/__init__.py b/ibis/backends/snowflake/__init__.py
--- a/ibis/backends/snowflake/__init__.py
+++ b/ibis/backends/snowflake/__init__.py
@@ -177,9 +177,22 @@ def connect(dbapi_connection, connection_record):
     def _get_sqla_table(
         self, name: str, schema: str | None = None, **_: Any
     ) -> sa.Table:
-        inspected = self.inspector.get_columns(name, schema)
+        with self.begin() as con:
+            cur_db, cur_schema = con.exec_driver_sql(
+                "SELECT CURRENT_DATABASE(), CURRENT_SCHEMA()"
+            ).fetchone()
+            if schema is not None:
+                con.exec_driver_sql(f"USE {schema}")
+        try:
+            inspected = self.inspector.get_columns(
+                name,
+                schema=schema.split(".", 2)[1] if schema is not None else schema,
+            )
+        finally:
+            with self.begin() as con:
+                con.exec_driver_sql(f"USE {cur_db}.{cur_schema}")
         cols = []
-        identifier = name if not schema else schema + "." + name
+        identifier = name if schema is None else schema + "." + name
         with self.begin() as con:
             cur = con.exec_driver_sql(f"DESCRIBE TABLE {identifier}").mappings()
             for colname, colinfo in zip(toolz.pluck("name", cur), inspected):


Test patch:
diff --git a/ibis/backends/snowflake/tests/conftest.py b/ibis/backends/snowflake/tests/conftest.py
--- a/ibis/backends/snowflake/tests/conftest.py
+++ b/ibis/backends/snowflake/tests/conftest.py
@@ -88,3 +88,8 @@ def connect(data_directory: Path) -> BaseBackend:
         if snowflake_url := os.environ.get("SNOWFLAKE_URL"):
             return ibis.connect(snowflake_url)  # type: ignore
         pytest.skip("SNOWFLAKE_URL environment variable is not defined")
+
+
+@pytest.fixture(scope="session")
+def con(data_directory):
+    return TestConf.connect(data_directory)
diff --git a/ibis/backends/snowflake/tests/test_client.py b/ibis/backends/snowflake/tests/test_client.py
new file mode 100644
--- /dev/null
+++ b/ibis/backends/snowflake/tests/test_client.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+import pytest
+
+import ibis
+from ibis.util import guid
+
+
+@pytest.mark.parametrize(
+    ("db", "schema", "cleanup"),
+    [
+        (f"tmp_db_{guid()}", f"tmp_schema_{guid()}", True),
+        ("ibis_testing", f"tmp_schema_{guid()}", False),
+    ],
+    ids=["temp", "perm"],
+)
+def test_cross_db_access(con, db, schema, cleanup):
+    schema = f"{db}.{schema}"
+    table = f"tmp_table_{guid()}"
+    con.raw_sql(f"CREATE DATABASE IF NOT EXISTS {db}")
+    try:
+        con.raw_sql(f"CREATE SCHEMA IF NOT EXISTS {schema}")
+        try:
+            con.raw_sql(f'CREATE TEMP TABLE {schema}.{table} ("x" INT)')
+            t = con.table(table, schema=f"{schema}")
+            assert t.schema() == ibis.schema(dict(x="int"))
+            assert t.execute().empty
+        finally:
+            if cleanup:
+                con.raw_sql(f"DROP SCHEMA {schema}")
+    finally:
+        if cleanup:
+            con.raw_sql(f"DROP DATABASE {db}")