Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions src/remote/remote-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,7 @@ import {
import { ZodError } from "zod";
import { CombinedExport, ExportedStats, Statistics } from "@query-doctor/core";
import { type Connectable } from "../sync/connectable.ts";
import { connectToSource } from "../sql/postgresjs.ts";

async function resolveDockerHost(db: Connectable): Promise<Connectable> {
if (!db.isLocalhost()) {
return db;
}
const dockerDb = db.escapeDocker();
if (dockerDb.url.hostname === db.url.hostname) {
return db;
}
const pg = connectToSource(dockerDb);
try {
await pg.exec("SELECT 1");
log.info(`Resolved localhost to ${dockerDb.url.hostname} for docker escape`, "remote-controller");
return dockerDb;
} catch {
log.info(`${dockerDb.url.hostname} unreachable, falling back to ${db.url.hostname}`, "remote-controller");
return db;
} finally {
// @ts-expect-error | close is added in wrapPgPool
await pg.close();
}
}

const SyncStatus = {
NOT_STARTED: "notStarted",
Expand Down Expand Up @@ -158,7 +136,7 @@ export class RemoteController {
let resolvedDb: Connectable;
try {
this.sendSyncLog("Reaching out to database...");
resolvedDb = await resolveDockerHost(db);
resolvedDb = await db.resolveDockerHost();
this.lastSourceDb = resolvedDb;
this.sendSyncLog(`Connected to ${resolvedDb.toString()}`);
} catch (error) {
Expand Down Expand Up @@ -264,6 +242,29 @@ export class RemoteController {
}
}

async onInstallPgStatStatements(rawBody: string): Promise<HandlerResult> {
const body = RemoteSyncRequest.safeDecode(rawBody);
if (!body.success) {
return { status: 400, body: body.error };
}

try {
const result = await this.remote.installPgStatStatements(body.data.db);
return { status: 200, body: { success: true, preloadUpdated: result.preloadUpdated } };
} catch (error) {
console.error(error);
if (error instanceof errors.PostgresError) {
return { status: error.statusCode, body: error.toJSON() };
}
return {
status: 500,
body: {
error: error instanceof Error ? error.message : "Unknown error",
},
};
}
}

async onReset(rawBody: string): Promise<HandlerResult> {
const body = RemoteSyncRequest.safeDecode(rawBody);
if (!body.success) {
Expand Down
5 changes: 5 additions & 0 deletions src/remote/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,11 @@ export class Remote extends EventEmitter<RemoteEvents> {
this.optimizer.restart({ clearQueries: true });
}

async installPgStatStatements(source: Connectable): Promise<{ preloadUpdated: boolean }> {
const connector = this.sourceManager.getConnectorFor(source);
return connector.installPgStatStatements();
}

/**
* Process a successful sync and run any potential cleanup functions
*/
Expand Down
16 changes: 16 additions & 0 deletions src/server/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ export async function createServer(
return reply.status(result.status).send(result.body);
});

fastify.post("/postgres/extensions/pg_stat_statements", async (request, reply) => {
log.info(`[POST] /postgres/extensions/pg_stat_statements`, "http");
const body = RemoteSyncRequest.safeDecode(JSON.stringify(request.body));
if (!body.success) {
return reply.status(400).send(body.error);
}
try {
const db = await body.data.db.resolveDockerHost();
const connector = sourceConnectionManager.getConnectorFor(db);
const result = await connector.installPgStatStatements();
return reply.status(200).send({ success: true, ...result });
} catch (error) {
return reply.status(500).send(makeUnexpectedErrorResult(error).body);
}
});

fastify.post("/postgres/live", async (request, reply) => {
log.info(`[POST] /postgres/live`, "http");
const result = await onSyncLiveQuery(request.body);
Expand Down
24 changes: 24 additions & 0 deletions src/sync/connectable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { z } from "zod";
import { env } from "../env.ts";
import { PgIdentifier } from "@query-doctor/core";
import { connectToSource } from "../sql/postgresjs.ts";
import { log } from "../log.ts";

/**
* Represents a valid connection to a database.
Expand Down Expand Up @@ -130,6 +132,28 @@ export class Connectable {
);
}

async resolveDockerHost(): Promise<Connectable> {
if (!this.isLocalhost()) {
return this;
}
const dockerDb = this.escapeDocker();
if (dockerDb.url.hostname === this.url.hostname) {
return this;
}
const pg = connectToSource(dockerDb);
try {
await pg.exec("SELECT 1");
log.info(`Resolved localhost to ${dockerDb.url.hostname} for docker escape`, "connectable");
return dockerDb;
} catch {
log.info(`${dockerDb.url.hostname} unreachable, falling back to ${this.url.hostname}`, "connectable");
return this;
} finally {
// @ts-expect-error | close is added in wrapPgPool
await pg.close();
}
}

toString() {
return this.url.toString();
}
Expand Down
40 changes: 40 additions & 0 deletions src/sync/pg-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,46 @@ ORDER BY
}
}

public async installPgStatStatements(): Promise<{ preloadUpdated: boolean }> {
let preloadUpdated = false;

const [preload] = await this.db.exec<{ setting: string }>(`
SELECT setting FROM pg_settings WHERE name = 'shared_preload_libraries'; -- @qd_introspection
`);
const current = preload?.setting ?? "";
const libs = current.split(",").map((s) => s.trim()).filter(Boolean);
if (!libs.includes("pg_stat_statements")) {
const updated = [...libs, "pg_stat_statements"].join(",");
try {
await this.db.exec(`ALTER SYSTEM SET shared_preload_libraries = '${updated}';`);
preloadUpdated = true;
} catch (err) {
throw new PostgresError(err instanceof Error ? err.message : String(err));
}
}

const [result] = await this.db.exec<{ exists: boolean }>(`
SELECT EXISTS(
SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'
) AS exists; -- @qd_introspection
`);
if (!result?.exists) {
try {
await this.db.exec(`CREATE EXTENSION pg_stat_statements;`);
} catch (err) {
throw new PostgresError(err instanceof Error ? err.message : String(err));
}
}

try {
await this.db.exec(`SELECT 1 FROM pg_stat_statements LIMIT 1; -- @qd_introspection`);
} catch (err) {
throw new PostgresError(err instanceof Error ? err.message : String(err));
}

return { preloadUpdated };
}

public async checkPrivilege(): Promise<{
username: string;
isSuperuser: boolean;
Expand Down
Loading