feat: add view CRUD APIs (#4236)

A view is a named query a database stores and plans on every read. It
holds no rows, which is the whole difference from a materialized view.

## API

| Verb | Route |
| --- | --- |
| `create_view(name, query, namespace_path)` | `POST
/v1/view/{id}/create` |
| `describe_view(name, namespace_path)` | `POST /v1/view/{id}/describe`
|
| `drop_view(name, namespace_path)` | `POST /v1/view/{id}/drop` |
| `list_views(namespace_path)` | `GET /v1/namespace/{id}/view/list` |

On `Connection` and the `Database` trait, with the remote client, Python
(sync and async) and Node bindings. Local databases return
`NotSupported`: the server side is Sophon's, where a view is an object
of the database manifest.

`ViewDescription` carries the defining query, the database *and
namespace path* unqualified names in it resolve against, and the schema
the query resolved to. `create_view` returns one, so a caller has the
schema without a second call.

Both defaults travel with the view because it outlives the session that
declared it: the server re-plans the stored query on every read, so a
reader resolving an unqualified name against its own defaults would read
a different table. `default_namespace_path` crosses the wire as
`default_namespace`, a path like `namespace`, absent for the root.

There is no replace: a name already taken is an error, and changing a
view is a drop followed by a create, each authorized against what it
actually touches.

Querying a view stays SQL's job. There are no rows behind a view, so
there is no `open_view` returning a `Table`.
This commit is contained in:
Jack Ye
2026-09-23 15:09:40 +08:00
committed by GitHub
parent dd2539c2ca
commit 8541d6d9df
22 changed files with 1283 additions and 4 deletions
+83
View File
@@ -55,6 +55,32 @@ pub struct DropNamespaceResponse {
pub transaction_id: Option<Vec<String>>,
}
/// What a database records about one view.
#[napi(object)]
pub struct ViewDescription {
pub name: String,
pub namespace_path: Vec<String>,
pub query: String,
pub default_database: String,
pub default_namespace_path: Vec<String>,
/// The view's schema as an empty Arrow IPC file, the way a table reports
/// its own.
pub schema: Buffer,
}
impl ViewDescription {
fn from_inner(view: lancedb::view::ViewDescription) -> napi::Result<Self> {
Ok(Self {
name: view.name,
namespace_path: view.namespace_path,
query: view.query,
default_database: view.default_database,
default_namespace_path: view.default_namespace_path,
schema: crate::util::schema_to_buffer(&view.schema)?,
})
}
}
impl Connection {
pub(crate) fn inner_new(inner: LanceDBConnection) -> Self {
Self { inner: Some(inner) }
@@ -368,6 +394,63 @@ impl Connection {
.default_error()
}
/// Create a view: a named query planned on every read.
#[napi(catch_unwind)]
pub async fn create_view(
&self,
name: String,
query: String,
namespace_path: Option<Vec<String>>,
) -> napi::Result<ViewDescription> {
let ns = namespace_path.unwrap_or_default();
let view = self
.get_inner()?
.create_view(&name, &query, &ns)
.await
.default_error()?;
ViewDescription::from_inner(view)
}
/// What the database records about one view.
#[napi(catch_unwind)]
pub async fn describe_view(
&self,
name: String,
namespace_path: Option<Vec<String>>,
) -> napi::Result<ViewDescription> {
let ns = namespace_path.unwrap_or_default();
let view = self
.get_inner()?
.describe_view(&name, &ns)
.await
.default_error()?;
ViewDescription::from_inner(view)
}
/// Drop a view. The tables it reads are untouched.
#[napi(catch_unwind)]
pub async fn drop_view(
&self,
name: String,
namespace_path: Option<Vec<String>>,
) -> napi::Result<()> {
let ns = namespace_path.unwrap_or_default();
self.get_inner()?
.drop_view(&name, &ns)
.await
.default_error()
}
/// The names of the views in one namespace.
#[napi(catch_unwind)]
pub async fn list_views(
&self,
namespace_path: Option<Vec<String>>,
) -> napi::Result<Vec<String>> {
let ns = namespace_path.unwrap_or_default();
self.get_inner()?.list_views(&ns).await.default_error()
}
/// Start dropping a materialized view and return its cleanup job.
#[napi(catch_unwind)]
pub async fn drop_materialized_view_async(