feat: add a cascade option to drop_database (#4697)

The remote catalog could only drop an empty database: it always asked
the server for a restricted drop.

`DropDatabaseRequest::cascade`, `drop_database(..., cascade=True)` in
Python and `dropDatabase(name, { cascade: true })` in TypeScript ask the
server to drop everything in the database first. The name stays taken
until the server finishes cleaning up the dropped objects' storage. A
restricted drop remains the default.
This commit is contained in:
Wyatt Alt
2026-10-27 21:51:10 -07:00
committed by GitHub
parent 45dbf5eff5
commit bb6aa1a147
10 changed files with 98 additions and 24 deletions
+1 -1
View File
@@ -990,7 +990,7 @@ class Catalog:
) -> Connection: ...
async def connect_database(self, name: str) -> Connection: ...
async def drop_database(
self, name: str, *, ignore_missing: bool = False
self, name: str, *, ignore_missing: bool = False, cascade: bool = False
) -> None: ...
def list_databases(
self, *, page_token: Optional[str] = None, page_limit: Optional[int] = None
+20 -6
View File
@@ -130,9 +130,15 @@ class AsyncCatalog:
self._inner.list_databases(page_token=page_token, page_limit=page_limit)
)
async def drop_database(self, name: str, *, ignore_missing: bool = False) -> None:
"""Drop an empty database. A nonempty database is an error."""
await self._inner.drop_database(name, ignore_missing=ignore_missing)
async def drop_database(
self, name: str, *, ignore_missing: bool = False, cascade: bool = False
) -> None:
"""Drop a database. A nonempty database is an error unless ``cascade`` is set,
which drops everything in it first; the name stays taken until the server
finishes cleaning up their storage."""
await self._inner.drop_database(
name, ignore_missing=ignore_missing, cascade=cascade
)
class Catalog:
@@ -207,9 +213,17 @@ class Catalog:
self._inner.list_databases(page_token=page_token, page_limit=page_limit)
)
def drop_database(self, name: str, *, ignore_missing: bool = False) -> None:
"""Drop an empty database. A nonempty database is an error."""
LOOP.run(self._inner.drop_database(name, ignore_missing=ignore_missing))
def drop_database(
self, name: str, *, ignore_missing: bool = False, cascade: bool = False
) -> None:
"""Drop a database. A nonempty database is an error unless ``cascade`` is set,
which drops everything in it first; the name stays taken until the server
finishes cleaning up their storage."""
LOOP.run(
self._inner.drop_database(
name, ignore_missing=ignore_missing, cascade=cascade
)
)
async def connect_catalog_async(
+2 -2
View File
@@ -86,12 +86,12 @@ def test_catalog_sync_scope_and_serialization(catalog_server):
connection.execute_query_async("SELECT 1")
assert restored.table_names() == []
assert list(catalog.list_databases()) == ["team/search"]
catalog.drop_database("team/search", ignore_missing=True)
catalog.drop_database("team/search", ignore_missing=True, cascade=True)
assert requests[0][0] == "/v1/namespace/team%2Fsearch/create"
assert requests[0][2] == {"mode": "ExistOk"}
assert requests[1][0] == "/v1/namespace/team%2Fsearch/describe"
assert requests[4][0] == "/v1/namespace/$/list"
assert requests[5][2] == {"mode": "Skip", "behavior": "Restrict"}
assert requests[5][2] == {"mode": "Skip", "behavior": "Cascade"}
for i, (_, headers, _) in enumerate(requests):
headers = {key.lower(): value for key, value in headers.items()}
assert headers.get("x-lancedb-database") == (
+7 -2
View File
@@ -60,16 +60,21 @@ impl Catalog {
})
}
#[pyo3(signature = (name, *, ignore_missing=false))]
#[pyo3(signature = (name, *, ignore_missing=false, cascade=false))]
fn drop_database<'py>(
self_: PyRef<'py, Self>,
name: String,
ignore_missing: bool,
cascade: bool,
) -> PyResult<Bound<'py, PyAny>> {
let inner = self_.inner.clone();
future_into_py(self_.py(), async move {
inner
.drop_database(DropDatabaseRequest::new(name).ignore_missing(ignore_missing))
.drop_database(
DropDatabaseRequest::new(name)
.ignore_missing(ignore_missing)
.cascade(cascade),
)
.await
.infer_error()
})