feat: add view CRUD APIs

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.

Adds `create_view`, `describe_view`, `drop_view` and `list_views` on
`Connection` and the `Database` trait, the remote client that speaks
`/v1/view/{id}/*` and `/v1/namespace/{id}/view/list`, and the Python and
Node bindings. Local databases return `NotSupported`; the server side
lives in Sophon.

`ViewDescription` carries the defining query, the database 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. There is no replace: changing a view is a drop and a create.

Querying a view stays SQL's job -- there are no rows behind it, so there
is no `open_view` returning a `Table`.
This commit is contained in:
Jack Ye
2026-09-22 10:06:01 -07:00
parent e5683f82fe
commit aeea54ff48
17 changed files with 1004 additions and 4 deletions
+57
View File
@@ -100,6 +100,63 @@ describe("remote connection", () => {
);
});
it("creates a view and decodes the schema it resolved to", async () => {
await withMockDatabase(
(req, res) => {
expect(req.method).toBe("POST");
expect(req.url).toBe("/v1/view/analytics$adults/create");
res.writeHead(200, { "content-type": "application/json" }).end(
JSON.stringify({
name: "adults",
namespace: ["analytics"],
query: "SELECT name FROM people",
default_database: "db",
schema: {
fields: [{ name: "name", nullable: true, type: { type: "utf8" } }],
},
}),
);
},
async (db) => {
const view = await db.createView(
"adults",
"SELECT name FROM people",
["analytics"],
);
expect(view.name).toBe("adults");
expect(view.namespacePath).toEqual(["analytics"]);
expect(view.query).toBe("SELECT name FROM people");
expect(view.defaultDatabase).toBe("db");
expect(view.schema.fields.map((f) => f.name)).toEqual(["name"]);
},
);
});
it("lists and drops views through their own routes", async () => {
await withMockDatabase(
(req, res) => {
expect(req.url).toBe("/v1/namespace/$/view/list");
res
.writeHead(200, { "content-type": "application/json" })
.end(JSON.stringify({ views: ["adults"] }));
},
async (db) => {
expect(await db.listViews()).toEqual(["adults"]);
},
);
await withMockDatabase(
(req, res) => {
expect(req.method).toBe("POST");
expect(req.url).toBe("/v1/view/adults/drop");
res.writeHead(200, { "content-type": "application/json" }).end("{}");
},
async (db) => {
await db.dropView("adults");
},
);
});
it("should accept partial connection options", async () => {
await connect("db://test", {
apiKey: "fake",