mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-13 15:22:30 +00:00
feat: return a cleanup job from drop_view (#4262)
Dropping a view unbinds its name and leaves the definition dataset to a server-side cleanup job, so the two are separate events a caller may want to wait on. `drop_view_async` returns that job — the same shape `drop_materialized_view_async` and `drop_function_async` already use: a `202` carries the job id, a `200` (nothing was bound to the name) yields an already-finished job with no id, and any other success status is an error rather than a silent no-op. ## `drop_view` waits `drop_view` now awaits the job before returning, so a caller who does not want to think about cleanup gets the stronger guarantee: when it returns, the definition really is deleted. That is deliberately **different** from `drop_materialized_view` and `drop_function`, which return as soon as the name is unbound and document that content may still be deleting. The view API is the newer one, and waiting is the semantic worth having; the other two are left alone rather than changing behaviour already released. ## Surfaces `Database` trait, the remote client, `Connection`, and the Python and Node bindings — matching where `drop_materialized_view_async` is already exposed. Four client tests cover the accepted case reporting its job id, the nothing-bound case reporting a finished job, a `202` without a usable `job_id`, and an unexpected success status.
This commit is contained in:
@@ -820,15 +820,42 @@ impl Connection {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Drop a view.
|
||||
/// Drop a view and wait for its definition to be deleted.
|
||||
///
|
||||
/// The tables it reads are untouched: a view holds no rows of its own.
|
||||
/// Local databases return [`Error::NotSupported`].
|
||||
/// Use [`Connection::drop_view_async`] to get the cleanup job instead of
|
||||
/// waiting on it. Local databases return [`Error::NotSupported`].
|
||||
pub async fn drop_view(&self, name: impl AsRef<str>, namespace_path: &[String]) -> Result<()> {
|
||||
validate_view_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal.drop_view(name.as_ref(), namespace_path).await
|
||||
}
|
||||
|
||||
/// Start dropping a view and return the job deleting its definition.
|
||||
///
|
||||
/// The name is free before this returns; the definition dataset may still
|
||||
/// be being deleted. Await [`Job::wait`][crate::job::Job::wait] to wait for
|
||||
/// that. When nothing was bound to the name, the returned job is already
|
||||
/// finished and has no id. Local databases return [`Error::NotSupported`].
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use lancedb::Connection;
|
||||
/// # async fn drop(conn: &Connection) -> lancedb::Result<()> {
|
||||
/// let job = conn.drop_view_async("recent_orders", &[]).await?;
|
||||
/// job.wait().await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn drop_view_async(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
namespace_path: &[String],
|
||||
) -> Result<crate::job::Job> {
|
||||
validate_view_reference(name.as_ref(), namespace_path)?;
|
||||
self.internal
|
||||
.drop_view_async(name.as_ref(), namespace_path)
|
||||
.await
|
||||
}
|
||||
|
||||
/// The names of the views in one namespace.
|
||||
///
|
||||
/// Names only; a definition is query metadata and comes from
|
||||
|
||||
@@ -470,11 +470,16 @@ pub trait Database:
|
||||
) -> Result<ViewDescription> {
|
||||
view_ops_not_supported()
|
||||
}
|
||||
/// Drop a view. Its sources are untouched -- a view holds no rows of its
|
||||
/// own.
|
||||
/// Drop a view and wait for its definition to be deleted. Its sources are
|
||||
/// untouched -- a view holds no rows of its own.
|
||||
async fn drop_view(&self, _name: &str, _namespace_path: &[String]) -> Result<()> {
|
||||
view_ops_not_supported()
|
||||
}
|
||||
/// Drop a view and return the job deleting its definition, without waiting.
|
||||
#[doc(hidden)]
|
||||
async fn drop_view_async(&self, _name: &str, _namespace_path: &[String]) -> Result<Job> {
|
||||
view_ops_not_supported()
|
||||
}
|
||||
/// The names of the views in one namespace.
|
||||
async fn list_views(&self, _namespace_path: &[String]) -> Result<Vec<String>> {
|
||||
view_ops_not_supported()
|
||||
|
||||
@@ -1210,11 +1210,39 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
|
||||
}
|
||||
|
||||
async fn drop_view(&self, name: &str, namespace_path: &[String]) -> Result<()> {
|
||||
self.drop_view_async(name, namespace_path)
|
||||
.await?
|
||||
.wait()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn drop_view_async(&self, name: &str, namespace_path: &[String]) -> Result<Job> {
|
||||
let view_id = build_object_identifier("View name", name, namespace_path)?;
|
||||
let req = self.client.post(&format!("/v1/view/{view_id}/drop"));
|
||||
let (request_id, response) = self.client.send(req).await?;
|
||||
self.client.check_response(&request_id, response).await?;
|
||||
Ok(())
|
||||
let response = self.client.check_response(&request_id, response).await?;
|
||||
let status = response.status();
|
||||
let body = response.text().await.err_to_http(request_id.clone())?;
|
||||
match status {
|
||||
// Nothing was bound to the name, so nothing is being deleted.
|
||||
StatusCode::OK => Ok(Job::new_done()),
|
||||
StatusCode::ACCEPTED => {
|
||||
let job_id = extract_job_id(&body).ok_or_else(|| Error::Http {
|
||||
source: "view drop response did not contain a valid job_id".into(),
|
||||
request_id,
|
||||
status_code: Some(status),
|
||||
})?;
|
||||
Ok(Job::new(Box::new(RemoteJob::new(
|
||||
self.client.clone(),
|
||||
job_id,
|
||||
))))
|
||||
}
|
||||
_ => Err(Error::Http {
|
||||
source: "view drop must return 200 OK or 202 Accepted".into(),
|
||||
request_id,
|
||||
status_code: Some(status),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_views(&self, namespace_path: &[String]) -> Result<Vec<String>> {
|
||||
@@ -4046,6 +4074,55 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// An accepted drop hands back the job so a caller can wait on the delete,
|
||||
/// and the waiting `drop_view` does that for them.
|
||||
#[tokio::test]
|
||||
async fn test_drop_view_async_reports_the_cleanup_job() {
|
||||
let db = super::RemoteDatabase::new_mock(|_| {
|
||||
http::Response::builder()
|
||||
.status(202)
|
||||
.body(r#"{"job_id":"j1-do-abc"}"#)
|
||||
.unwrap()
|
||||
});
|
||||
let job = db.drop_view_async("adults", &[]).await.unwrap();
|
||||
assert_eq!(job.id(), Some("j1-do-abc"));
|
||||
}
|
||||
|
||||
/// Nothing was bound, so nothing is being deleted and the job is already done.
|
||||
#[tokio::test]
|
||||
async fn test_drop_view_async_reports_a_finished_job_when_nothing_was_bound() {
|
||||
let db = super::RemoteDatabase::new_mock(|_| {
|
||||
http::Response::builder().status(200).body("{}").unwrap()
|
||||
});
|
||||
let job = db.drop_view_async("adults", &[]).await.unwrap();
|
||||
assert_eq!(job.id(), None);
|
||||
assert_eq!(job.status().await.unwrap(), "finished");
|
||||
job.wait().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drop_view_rejects_incomplete_acceptance() {
|
||||
for body in ["{}", r#"{"job_id":""}"#, r#"{"job_id":null}"#] {
|
||||
let db = super::RemoteDatabase::new_mock(move |_| {
|
||||
http::Response::builder().status(202).body(body).unwrap()
|
||||
});
|
||||
let error = db.drop_view_async("adults", &[]).await.err().unwrap();
|
||||
assert!(error.to_string().contains("valid job_id"), "{error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_drop_view_rejects_unexpected_success_status() {
|
||||
let db = super::RemoteDatabase::new_mock(|_| {
|
||||
http::Response::builder().status(204).body("").unwrap()
|
||||
});
|
||||
let error = db.drop_view_async("adults", &[]).await.err().unwrap();
|
||||
assert!(
|
||||
error.to_string().contains("200 OK or 202 Accepted"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A schema the client cannot decode is a broken response, not a view
|
||||
/// with no columns: reporting it as an error keeps a caller from reading
|
||||
/// an empty schema as the truth about the view.
|
||||
|
||||
Reference in New Issue
Block a user