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:
Jack Ye
2026-09-24 00:27:20 -07:00
committed by GitHub
parent 85c9cf6240
commit e4d0f9e2da
12 changed files with 286 additions and 20 deletions
+31
View File
@@ -161,6 +161,37 @@ describe("remote connection", () => {
);
});
it("reports the cleanup job when a view drop is accepted", async () => {
await withMockDatabase(
(req, res) => {
expect(req.method).toBe("POST");
expect(req.url).toBe("/v1/view/adults/drop");
res
.writeHead(202, { "content-type": "application/json" })
.end('{"job_id": "j1-do-abc"}');
},
async (db) => {
const job = await db.dropViewAsync("adults");
expect(job.id).toBe("j1-do-abc");
},
);
});
it("reports a finished job when a view drop had nothing to delete", async () => {
await withMockDatabase(
(req, res) => {
expect(req.url).toBe("/v1/view/adults/drop");
res.writeHead(200, { "content-type": "application/json" }).end("{}");
},
async (db) => {
// A 200 means the name was not bound, so there is no cleanup to wait on.
const job = await db.dropViewAsync("adults");
expect(job.id).toBeNull();
await job.wait();
},
);
});
it("should accept partial connection options", async () => {
await connect("db://test", {
apiKey: "fake",
+16 -2
View File
@@ -404,12 +404,22 @@ export abstract class Connection {
): Promise<ViewDescription>;
/**
* Drop the view named `name`.
* Drop the view named `name` and wait for its definition to be deleted.
*
* The tables it reads are untouched: a view holds no rows of its own.
* The tables it reads are untouched: a view holds no rows of its own. Use
* {@link dropViewAsync} to retain the cleanup job instead of waiting on it.
*/
abstract dropView(name: string, namespacePath?: string[]): Promise<void>;
/**
* Start dropping the view named `name` and return the job deleting its
* definition, without waiting for completion.
*
* The name is free before this resolves. When nothing was bound to it, the
* returned job is already finished and has no id.
*/
abstract dropViewAsync(name: string, namespacePath?: string[]): Promise<Job>;
/**
* The names of the views in one namespace.
*
@@ -759,6 +769,10 @@ export class LocalConnection extends Connection {
return this.inner.dropView(name, namespacePath ?? []);
}
async dropViewAsync(name: string, namespacePath?: string[]): Promise<Job> {
return new Job(await this.inner.dropViewAsync(name, namespacePath ?? []));
}
async listViews(namespacePath?: string[]): Promise<string[]> {
return this.inner.listViews(namespacePath ?? []);
}
+18 -1
View File
@@ -427,7 +427,8 @@ impl Connection {
ViewDescription::from_inner(view)
}
/// Drop a view. The tables it reads are untouched.
/// Drop a view and wait for its definition to be deleted. The tables it
/// reads are untouched.
#[napi(catch_unwind)]
pub async fn drop_view(
&self,
@@ -441,6 +442,22 @@ impl Connection {
.default_error()
}
/// Start dropping a view and return the job deleting its definition.
#[napi(catch_unwind)]
pub async fn drop_view_async(
&self,
name: String,
namespace_path: Option<Vec<String>>,
) -> napi::Result<crate::job::Job> {
let ns = namespace_path.unwrap_or_default();
let job = self
.get_inner()?
.drop_view_async(&name, &ns)
.await
.default_error()?;
Ok(crate::job::Job::new(job))
}
/// The names of the views in one namespace.
#[napi(catch_unwind)]
pub async fn list_views(