Skip to content

Commit

Permalink
feat(duckdb): add attach and detach methods for adding and removi…
Browse files Browse the repository at this point in the history
…ng databases to the current duckdb session
  • Loading branch information
cpcloud authored and jcrist committed Oct 19, 2023
1 parent 2cc9d0e commit 162b058
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 2 deletions.
38 changes: 38 additions & 0 deletions ibis/backends/duckdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,44 @@ def read_sqlite(self, path: str | Path, table_name: str | None = None) -> ir.Tab

return self.table(table_name)

def attach(
self, path: str | Path, name: str | None = None, read_only: bool = False
) -> None:
"""Attach another DuckDB database to the current DuckDB session.
Parameters
----------
path
Path to the database to attach.
name
Name to attach the database as. Defaults to the basename of `path`.
read_only
Whether to attach the database as read-only.
"""
code = f"ATTACH '{path}'"

if name is not None:
name = sg.to_identifier(name).sql(self.name)
code += f" AS {name}"

if read_only:
code += " (READ_ONLY)"

with self.begin() as con:
con.exec_driver_sql(code)

def detach(self, name: str) -> None:
"""Detach a database from the current DuckDB session.
Parameters
----------
name
The name of the database to detach.
"""
name = sg.to_identifier(name).sql(self.name)
with self.begin() as con:
con.exec_driver_sql(f"DETACH {name}")

def attach_sqlite(
self, path: str | Path, overwrite: bool = False, all_varchar: bool = False
) -> None:
Expand Down
37 changes: 35 additions & 2 deletions ibis/backends/duckdb/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ def test_cross_db(tmpdir):
con2 = ibis.duckdb.connect(path2)
t2 = con2.create_table("t2", schema=ibis.schema(dict(x="int")))

with con2.begin() as c:
c.exec_driver_sql(f"ATTACH '{path1}' AS test1 (READ_ONLY)")
con2.attach(path1, name="test1", read_only=True)

t1_from_con2 = con2.table("t1", schema="test1.main")
assert t1_from_con2.schema() == t2.schema()
Expand All @@ -80,3 +79,37 @@ def test_cross_db(tmpdir):
foo_t1_from_con2 = con2.table("t1", schema="test1.foo")
assert foo_t1_from_con2.schema() == t2.schema()
assert foo_t1_from_con2.execute().equals(t2.execute())


def test_attach_detach(tmpdir):
import duckdb

path1 = str(tmpdir.join("test1.ddb"))
with duckdb.connect(path1):
pass

path2 = str(tmpdir.join("test2.ddb"))
con2 = ibis.duckdb.connect(path2)

# default name
name = "test1"
assert name not in con2.list_databases()

con2.attach(path1)
assert name in con2.list_databases()

con2.detach(name)
assert name not in con2.list_databases()

# passed-in name
name = "test_foo"
assert name not in con2.list_databases()

con2.attach(path1, name=name)
assert name in con2.list_databases()

con2.detach(name)
assert name not in con2.list_databases()

with pytest.raises(sa.exc.ProgrammingError):
con2.detach(name)

0 comments on commit 162b058

Please sign in to comment.