mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-28 22:46:14 +00:00
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:
@@ -70,7 +70,9 @@ Create a database, or open an existing database when existOk is true.
|
||||
dropDatabase(name, options): Promise<void>
|
||||
```
|
||||
|
||||
Drop an empty database. The server rejects nonempty databases.
|
||||
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.
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -78,6 +80,8 @@ Drop an empty database. The server rejects nonempty databases.
|
||||
|
||||
* **options** = `{}`
|
||||
|
||||
* **options.cascade?**: `boolean`
|
||||
|
||||
* **options.ignoreMissing?**: `boolean`
|
||||
|
||||
#### Returns
|
||||
|
||||
@@ -86,7 +86,10 @@ describe("remote catalog", () => {
|
||||
value: "team/search",
|
||||
});
|
||||
expect(names.pageToken()).toBe("next");
|
||||
await catalog.dropDatabase("team/search", { ignoreMissing: true });
|
||||
await catalog.dropDatabase("team/search", {
|
||||
ignoreMissing: true,
|
||||
cascade: true,
|
||||
});
|
||||
expect(requests[0].url).toBe("/v1/namespace/team%2Fsearch/create");
|
||||
expect(requests[0].body).toEqual({ mode: "ExistOk" });
|
||||
expect(requests[5].url).toBe(
|
||||
@@ -94,7 +97,7 @@ describe("remote catalog", () => {
|
||||
);
|
||||
expect(requests[6].body).toEqual({
|
||||
mode: "Skip",
|
||||
behavior: "Restrict",
|
||||
behavior: "Cascade",
|
||||
});
|
||||
for (const [i, request] of requests.entries()) {
|
||||
expect(request.headers["x-lancedb-database"]).toBe(
|
||||
|
||||
@@ -82,12 +82,16 @@ export class Catalog {
|
||||
return new LocalConnection(await this.inner.connectDatabase(name));
|
||||
}
|
||||
|
||||
/** Drop an empty database. The server rejects nonempty databases. */
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async dropDatabase(
|
||||
name: string,
|
||||
options: { ignoreMissing?: boolean } = {},
|
||||
options: { ignoreMissing?: boolean; cascade?: boolean } = {},
|
||||
): Promise<void> {
|
||||
await this.inner.dropDatabase(name, options.ignoreMissing);
|
||||
await this.inner.dropDatabase(name, options.ignoreMissing, options.cascade);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -124,10 +124,17 @@ impl Catalog {
|
||||
.default_error()
|
||||
}
|
||||
#[napi]
|
||||
pub async fn drop_database(&self, name: String, ignore_missing: Option<bool>) -> Result<()> {
|
||||
pub async fn drop_database(
|
||||
&self,
|
||||
name: String,
|
||||
ignore_missing: Option<bool>,
|
||||
cascade: Option<bool>,
|
||||
) -> Result<()> {
|
||||
self.inner
|
||||
.drop_database(
|
||||
DropDatabaseRequest::new(name).ignore_missing(ignore_missing.unwrap_or(false)),
|
||||
DropDatabaseRequest::new(name)
|
||||
.ignore_missing(ignore_missing.unwrap_or(false))
|
||||
.cascade(cascade.unwrap_or(false)),
|
||||
)
|
||||
.await
|
||||
.default_error()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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") == (
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -59,7 +59,7 @@ impl<T: Into<String>> From<T> for CreateDatabaseRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for restricted database deletion. Tables must be removed first.
|
||||
/// Options for database deletion. The database must be empty unless `cascade` is set.
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct DropDatabaseRequest {
|
||||
@@ -67,6 +67,8 @@ pub struct DropDatabaseRequest {
|
||||
pub name: String,
|
||||
/// Succeed if the database is absent.
|
||||
pub ignore_missing: bool,
|
||||
/// Drop everything in the database first.
|
||||
pub cascade: bool,
|
||||
}
|
||||
|
||||
impl DropDatabaseRequest {
|
||||
@@ -75,6 +77,7 @@ impl DropDatabaseRequest {
|
||||
Self {
|
||||
name: name.into(),
|
||||
ignore_missing: false,
|
||||
cascade: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +86,13 @@ impl DropDatabaseRequest {
|
||||
self.ignore_missing = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Drop every table, view, function, secret and namespace in the database first. The
|
||||
/// server cleans up their storage in the background, and the name stays taken until then.
|
||||
pub fn cascade(mut self, value: bool) -> Self {
|
||||
self.cascade = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<String>> From<T> for DropDatabaseRequest {
|
||||
@@ -204,7 +214,7 @@ pub trait Catalog: Send + Sync + std::fmt::Debug + 'static {
|
||||
fn uri(&self) -> &str;
|
||||
/// Create a database, or open it when `exist_ok` permits.
|
||||
async fn create_database(&self, request: CreateDatabaseRequest) -> Result<Arc<dyn Database>>;
|
||||
/// Drop an empty database. This must not cascade to tables.
|
||||
/// Drop a database, which must be empty unless the request cascades.
|
||||
async fn drop_database(&self, request: DropDatabaseRequest) -> Result<()>;
|
||||
/// List a page of database names.
|
||||
async fn list_databases(&self, request: ListDatabasesRequest) -> Result<ListDatabasesResponse>;
|
||||
@@ -267,7 +277,7 @@ impl CatalogConnection {
|
||||
))
|
||||
}
|
||||
|
||||
/// Drop an empty database; pass a name or [`DropDatabaseRequest`] for additional options.
|
||||
/// Drop a database; pass a name or [`DropDatabaseRequest`] for options such as `cascade`.
|
||||
pub async fn drop_database(&self, request: impl Into<DropDatabaseRequest>) -> Result<()> {
|
||||
self.catalog.drop_database(request.into()).await
|
||||
}
|
||||
|
||||
@@ -226,7 +226,14 @@ impl Catalog for RemoteCatalog {
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
behavior: Some("Restrict".into()),
|
||||
behavior: Some(
|
||||
if request.cascade {
|
||||
"Cascade"
|
||||
} else {
|
||||
"Restrict"
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
@@ -474,7 +481,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_preserves_errors_and_never_cascades() {
|
||||
async fn catalog_preserves_errors_and_restricts_drops_by_default() {
|
||||
let (endpoint, task) = server(vec![
|
||||
(404, json!({"error": "missing"})),
|
||||
(409, json!({"error": "exists"})),
|
||||
@@ -522,6 +529,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drop_database_cascades_only_when_asked() {
|
||||
let (endpoint, task) = server(vec![(200, json!({})), (200, json!({}))]).await;
|
||||
let catalog = RemoteCatalog::try_new(endpoint, RemoteCatalogOptions::default()).unwrap();
|
||||
catalog.drop_database("db".into()).await.unwrap();
|
||||
catalog
|
||||
.drop_database(DropDatabaseRequest::new("db").cascade(true))
|
||||
.await
|
||||
.unwrap();
|
||||
let requests = task.await.unwrap();
|
||||
assert_eq!(
|
||||
requests[0].body,
|
||||
json!({"mode": "Fail", "behavior": "Restrict"})
|
||||
);
|
||||
assert_eq!(
|
||||
requests[1].body,
|
||||
json!({"mode": "Fail", "behavior": "Cascade"})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn database_names_follow_pages_lazily() {
|
||||
use futures::TryStreamExt;
|
||||
|
||||
Reference in New Issue
Block a user