From 96ba85da460565bef8bddeee731dfbed40d00326 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Sirois Date: Mon, 9 Feb 2026 22:37:02 +0400 Subject: [PATCH 1/2] feat: create index endpoint --- src/remote/query-optimizer.ts | 6 ++ src/remote/remote-controller.dto.ts | 16 +++++ src/remote/remote-controller.test.ts | 91 ++++++++++++++++++++++++++++ src/remote/remote-controller.ts | 46 +++++++++++++- 4 files changed, 158 insertions(+), 1 deletion(-) diff --git a/src/remote/query-optimizer.ts b/src/remote/query-optimizer.ts index d18ba3d..2cd2760 100644 --- a/src/remote/query-optimizer.ts +++ b/src/remote/query-optimizer.ts @@ -175,6 +175,12 @@ export class QueryOptimizer extends EventEmitter { return disabled; } + async createIndex(sql: string): Promise { + const pg = this.manager.getOrCreateConnection(this.connectable); + await pg.exec(sql); + this.restart(); + } + /** * Insert new queries to be processed. The {@link start} method must * have been called previously for this to take effect diff --git a/src/remote/remote-controller.dto.ts b/src/remote/remote-controller.dto.ts index 3a06885..99913d6 100644 --- a/src/remote/remote-controller.dto.ts +++ b/src/remote/remote-controller.dto.ts @@ -8,3 +8,19 @@ export const ToggleIndexDto = z.object({ }); export type ToggleIndexDto = z.infer; + +export const CreateIndexDto = z.object({ + connectionString: z.string().min(1), + table: z.string().min(1), + columns: z + .array( + z.object({ + name: z.string().min(1), + order: z.enum(["asc", "desc"]), + }), + ) + .min(1), +}); + +export type CreateIndexDto = z.infer; + diff --git a/src/remote/remote-controller.test.ts b/src/remote/remote-controller.test.ts index 0f20eab..c02df93 100644 --- a/src/remote/remote-controller.test.ts +++ b/src/remote/remote-controller.test.ts @@ -99,3 +99,94 @@ Deno.test({ } }, }); + +Deno.test({ + name: "creating an index via endpoint adds it to the optimizing db", + sanitizeOps: false, + sanitizeResources: false, + fn: async () => { + const [sourceDb, targetDb] = await Promise.all([ + new PostgreSqlContainer("postgres:17") + .withCopyContentToContainer([ + { + content: ` + create table testing(a int, b text); + insert into testing values (1, 'hello'); + create extension pg_stat_statements; + `, + target: "/docker-entrypoint-initdb.d/init.sql", + }, + ]) + .withCommand(["-c", "shared_preload_libraries=pg_stat_statements"]) + .start(), + new PostgreSqlContainer("postgres:17").start(), + ]); + const controller = new AbortController(); + + const target = Connectable.fromString(targetDb.getConnectionUri()); + const source = Connectable.fromString(sourceDb.getConnectionUri()); + const sourceOptimizer = ConnectionManager.forLocalDatabase(); + + const remote = new RemoteController( + new Remote(target, sourceOptimizer), + ); + + const server = Deno.serve( + { port: 0, signal: controller.signal }, + async (req: Request): Promise => { + const result = await remote.execute(req); + if (!result) { + return new Response("Not found", { status: 404 }); + } + return result; + }, + ); + + try { + // First sync the database + const syncResponse = await fetch( + `http://localhost:${server.addr.port}/postgres`, + { + method: "POST", + body: RemoteSyncRequest.encode({ db: source }), + }, + ); + assertEquals(syncResponse.status, 200); + + const sql = postgres( + target.withDatabaseName(Remote.optimizingDbName).toString(), + ); + + // Verify no indexes exist initially + const indexesBefore = + await sql`select * from pg_indexes where schemaname = 'public'`; + assertEquals(indexesBefore.count, 0); + + // Create an index via the endpoint + const createResponse = await fetch( + `http://localhost:${server.addr.port}/postgres/indexes`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + connectionString: sourceDb.getConnectionUri(), + table: "testing", + columns: [{ name: "a", order: "asc" }], + }), + }, + ); + + assertEquals(createResponse.status, 200); + const body = await createResponse.json(); + assertEquals(body.success, true); + + // Verify the index was created on the optimizing db + const indexesAfter = + await sql`select * from pg_indexes where schemaname = 'public'`; + assertEquals(indexesAfter.count, 1); + assertEquals(indexesAfter[0].tablename, "testing"); + } finally { + await Promise.all([sourceDb.stop(), targetDb.stop(), server.shutdown()]); + } + }, +}); diff --git a/src/remote/remote-controller.ts b/src/remote/remote-controller.ts index 8a239ab..d6e85da 100644 --- a/src/remote/remote-controller.ts +++ b/src/remote/remote-controller.ts @@ -4,7 +4,10 @@ import { RemoteSyncRequest } from "./remote.dto.ts"; import { Remote } from "./remote.ts"; import * as errors from "../sync/errors.ts"; import type { OptimizedQuery } from "../sql/recent-query.ts"; -import { ToggleIndexDto } from "./remote-controller.dto.ts"; +import { + CreateIndexDto, + ToggleIndexDto, +} from "./remote-controller.dto.ts"; import { ZodError } from "zod"; import { PgIdentifier } from "@query-doctor/core"; @@ -62,6 +65,14 @@ export class RemoteController { } return methodNotAllowed(); } + + if (url.pathname === "/postgres/indexes") { + if (request.method === "POST") { + return await this.createIndex(request); + } + return methodNotAllowed(); + } + } private hookUpWebsockets(remote: Remote) { @@ -181,6 +192,39 @@ export class RemoteController { } } + private async createIndex(request: Request): Promise { + try { + const data = await request.json(); + const body = CreateIndexDto.parse(data); + + const columnDefs = body.columns + .map((c) => { + const quoted = PgIdentifier.fromString(c.name); + return c.order === "desc" ? `${quoted} DESC` : `${quoted}`; + }) + .join(", "); + + const quotedTable = PgIdentifier.fromString(body.table); + await this.remote.optimizer.createIndex( + `CREATE INDEX ON ${quotedTable}(${columnDefs})`, + ); + + return Response.json({ success: true }); + } catch (error) { + if (error instanceof ZodError) { + return Response.json({ + type: "error", + error: "invalid_body", + message: error.message, + }, { status: 400 }); + } + console.error("Failed to create index:", error); + return Response.json({ + error: error instanceof Error ? error.message : "Failed to create index", + }, { status: 500 }); + } + } + private onWebsocketRequest(request: Request): Response { const { socket, response } = Deno.upgradeWebSocket(request); this.socket = socket; From d03c652b7e7dd0d057ed96f2aa6e454bac318005 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Sirois Date: Tue, 10 Feb 2026 18:36:06 +0400 Subject: [PATCH 2/2] refact: extract index creation logic from controller --- src/remote/query-optimizer.ts | 14 ++++++++++++-- src/remote/remote-controller.ts | 15 +-------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/remote/query-optimizer.ts b/src/remote/query-optimizer.ts index 2cd2760..e3e4cb8 100644 --- a/src/remote/query-optimizer.ts +++ b/src/remote/query-optimizer.ts @@ -175,9 +175,19 @@ export class QueryOptimizer extends EventEmitter { return disabled; } - async createIndex(sql: string): Promise { + async createIndex( + table: string, + columns: { name: string; order: "asc" | "desc" }[], + ): Promise { + const columnDefs = columns + .map((c) => { + const quoted = PgIdentifier.fromString(c.name); + return c.order === "desc" ? `${quoted} DESC` : `${quoted}`; + }) + .join(", "); + const quotedTable = PgIdentifier.fromString(table); const pg = this.manager.getOrCreateConnection(this.connectable); - await pg.exec(sql); + await pg.exec(`CREATE INDEX ON ${quotedTable}(${columnDefs})`); this.restart(); } diff --git a/src/remote/remote-controller.ts b/src/remote/remote-controller.ts index d6e85da..11d0555 100644 --- a/src/remote/remote-controller.ts +++ b/src/remote/remote-controller.ts @@ -9,7 +9,6 @@ import { ToggleIndexDto, } from "./remote-controller.dto.ts"; import { ZodError } from "zod"; -import { PgIdentifier } from "@query-doctor/core"; const SyncStatus = { NOT_STARTED: "notStarted", @@ -196,19 +195,7 @@ export class RemoteController { try { const data = await request.json(); const body = CreateIndexDto.parse(data); - - const columnDefs = body.columns - .map((c) => { - const quoted = PgIdentifier.fromString(c.name); - return c.order === "desc" ? `${quoted} DESC` : `${quoted}`; - }) - .join(", "); - - const quotedTable = PgIdentifier.fromString(body.table); - await this.remote.optimizer.createIndex( - `CREATE INDEX ON ${quotedTable}(${columnDefs})`, - ); - + await this.remote.optimizer.createIndex(body.table, body.columns); return Response.json({ success: true }); } catch (error) { if (error instanceof ZodError) {