Skip to content
Merged
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
9 changes: 9 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
analyzer:
build: .
ports:
- 2345:2345
env_file:
- .env
volumes:
- ../query-doctor/packages/core/dist:/app/node_modules/@query-doctor/core/dist
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"start": "node --import tsx src/main.ts",
"start:dev": "node --import tsx --watch src/main.ts",
"dev": "node --env-file=.env --import tsx --watch src/main.ts",
"dev:docker": "docker compose -f docker-compose.dev.yml up --build",
"test": "vitest",
"typecheck": "tsc --noEmit",
"build": "esbuild src/main.ts --bundle --platform=node --format=esm --outfile=dist/main.mjs --packages=external && cp src/reporters/github/success.md.j2 src/sync/schema_dump.sql dist/"
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async function runInCI(
// POST to Site API first so we get the run ID for the PR comment link
let runId: string | null = null;
if (siteApiEndpoint) {
runId = await postToSiteApi(siteApiEndpoint, queries);
runId = await postToSiteApi(siteApiEndpoint, queries, reportContext.statisticsMode, reportContext.computedStats);
}

// Build the run URL and query base URL for the PR comment
Expand Down
10 changes: 6 additions & 4 deletions src/remote/query-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
OptimizeResult,
PgIdentifier,
PostgresExplainStage,
ComputedStats,
PostgresQueryBuilder,
PostgresTransaction,
PostgresVersion,
Expand Down Expand Up @@ -41,10 +42,7 @@ type Target = {

export class QueryOptimizer extends EventEmitter<EventMap> {
private static readonly MAX_CONCURRENCY = 1;
private static readonly defaultStatistics: StatisticsMode = {
kind: "fromAssumption",
reltuples: 10_000,
};
private static readonly defaultStatistics: StatisticsMode = Statistics.defaultStatsMode;
private readonly queries = new Map<QueryHash, OptimizedQuery>();
private readonly disabledIndexes = new DisabledIndexes();

Expand Down Expand Up @@ -105,6 +103,10 @@ export class QueryOptimizer extends EventEmitter<EventMap> {
return this.target?.statistics.mode ?? QueryOptimizer.defaultStatistics;
}

get computedStats(): ComputedStats | undefined {
return this.target?.statistics.computedStats;
}

getExistingIndexes(): IndexedTable[] {
return this.existingIndexes;
}
Expand Down
16 changes: 13 additions & 3 deletions src/remote/remote-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
ToggleIndexDto,
} from "./remote-controller.dto.ts";
import { ZodError } from "zod";
import { ExportedStats, Statistics } from "@query-doctor/core";
import { CombinedExport, ExportedStats, Statistics } from "@query-doctor/core";
import { type Connectable } from "../sync/connectable.ts";
import { connectToSource } from "../sql/postgresjs.ts";

Expand Down Expand Up @@ -141,6 +141,8 @@ export class RemoteController {
: { type: "ok", value: queries },
disabledIndexes: { type: "ok", value: disabledIndexes },
deltas,
statisticsMode: this.remote.optimizer.statisticsMode,
computedStats: this.remote.optimizer.computedStats,
};
}

Expand Down Expand Up @@ -192,6 +194,8 @@ export class RemoteController {
queries: pgStatStatementsNotInstalled
? this.pgStatStatementsNotInstalledError()
: { type: "ok", value: queries },
statisticsMode: this.remote.optimizer.statisticsMode,
computedStats: this.remote.optimizer.computedStats,
},
};
} catch (error) {
Expand Down Expand Up @@ -222,7 +226,10 @@ export class RemoteController {
async onImportStats(body: unknown): Promise<HandlerResult> {
let stats: ExportedStats[];
try {
stats = ExportedStats.array().parse(body);
const combined = CombinedExport.safeParse(body);
stats = combined.success
? combined.data.stats
: ExportedStats.array().parse(body);
} catch (error) {
if (error instanceof ZodError) {
return {
Expand All @@ -232,14 +239,17 @@ export class RemoteController {
}
return {
status: 400,
body: { type: "error", error: "invalid_body", message: "body must be an array of ExportedStats" },
body: { type: "error", error: "invalid_body", message: "body must be an array of ExportedStats or a CombinedExport object" },
};
}

try {
await this.remote.applyStatistics(
Statistics.statsModeFromExport(stats),
);
if (this.syncResponse) {
this.syncResponse.meta.inferredStatsStrategy = "imported";
}
return { status: 200, body: { success: true } };
} catch (error) {
console.error(error);
Expand Down
2 changes: 1 addition & 1 deletion src/remote/remote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ test("infers '10k' stats strategy when row count is below threshold", async () =
const result = await remote.syncFrom(source);
await remote.optimizer.finish;

expect(result.meta.inferredStatsStrategy).toEqual("10k");
expect(result.meta.inferredStatsStrategy).toEqual("default");
await remote.cleanup();
} finally {
await Promise.all([sourceDb.stop(), targetDb.stop()]);
Expand Down
12 changes: 8 additions & 4 deletions src/remote/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,11 @@ export class Remote extends EventEmitter<RemoteEvents> {
const nextDbName = this.generationDbName(nextGeneration);
log.info(`Creating new generation database: ${nextDbName}`, "remote");
const baseDb = this.manager.getOrCreateConnection(this.baseDbURL);
await baseDb.exec(`create database ${nextDbName};`);
try {
await baseDb.exec(`create database ${nextDbName};`);
} catch (err) {
// it's ok if the db already exists (previously crashed)
}
const prevDbName = this.generationDbName(prevGeneration);
this.generation = nextGeneration;
this.optimizingDbUDRL = this.optimizingDbUDRL.withDatabaseName(nextDbName);
Expand Down Expand Up @@ -312,10 +316,10 @@ export class Remote extends EventEmitter<RemoteEvents> {

if (totalRows < Remote.STATS_ROWS_THRESHOLD) {
log.info(
`Total rows (${totalRows}) below threshold, using default 10k stats`,
`Total rows (${totalRows}) below threshold, using default stats`,
"remote",
);
return { mode: Statistics.defaultStatsMode, strategy: "10k" };
return { mode: Statistics.defaultStatsMode, strategy: "default" };
}

log.info(
Expand Down Expand Up @@ -434,7 +438,7 @@ export type StatisticsStrategy = {
stats: StatisticsMode;
};

export type InferredStatsStrategy = "10k" | "fromSource";
export type InferredStatsStrategy = "default" | "fromSource" | "imported";

type StatsResult = {
mode: StatisticsMode;
Expand Down
3 changes: 2 additions & 1 deletion src/reporters/reporter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IndexIdentifier, StatisticsMode } from "@query-doctor/core";
import type { ComputedStats, IndexIdentifier, StatisticsMode } from "@query-doctor/core";
import type { RunComparison } from "./site-api.ts";

export interface Reporter {
Expand Down Expand Up @@ -74,6 +74,7 @@ export interface ReportStatistics {

export interface ReportContext {
statisticsMode: StatisticsMode;
computedStats?: ComputedStats;
recommendations: ReportIndexRecommendation[];
queriesPastThreshold: ReportQueryCostWarning[];
queryStats: Readonly<ReportStatistics>;
Expand Down
8 changes: 7 additions & 1 deletion src/reporters/site-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as github from "@actions/github";
import type { IndexRecommendation, Nudge, SQLCommenterTag, TableReference } from "@query-doctor/core";
import type { ComputedStats, IndexRecommendation, Nudge, SQLCommenterTag, StatisticsMode, TableReference } from "@query-doctor/core";
import { DEFAULT_CONFIG, type AnalyzerConfig } from "../config.ts";
import type { OptimizedQuery } from "../sql/recent-query.ts";

Expand All @@ -11,6 +11,8 @@ interface CiRunPayload {
prNumber?: number;
runId: string;
queries: CiQueryPayload[];
statisticsMode?: StatisticsMode;
computedStats?: ComputedStats;
}

export interface CiQueryPayload {
Expand Down Expand Up @@ -243,6 +245,8 @@ export function compareRuns(
export async function postToSiteApi(
endpoint: string,
queries: CiQueryPayload[],
statisticsMode?: StatisticsMode,
computedStats?: ComputedStats,
): Promise<string | null> {
const payload: CiRunPayload = {
repo: process.env.GITHUB_REPOSITORY ?? "",
Expand All @@ -251,6 +255,8 @@ export async function postToSiteApi(
prNumber: github.context.payload.pull_request?.number,
runId: process.env.GITHUB_RUN_ID ?? "",
queries,
statisticsMode,
computedStats,
};

const url = `${endpoint.replace(/\/$/, "")}/ci/runs`;
Expand Down
1 change: 1 addition & 0 deletions src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export class Runner {
const timeElapsed = Date.now() - startDate.getTime();
const reportContext: ReportContext = {
statisticsMode: this.remote.optimizer.statisticsMode,
computedStats: this.remote.optimizer.computedStats,
recommendations: filteredRecommendations,
queriesPastThreshold: filteredThresholdWarnings,
queryStats: Object.freeze({
Expand Down
1 change: 0 additions & 1 deletion src/sync/pg-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { SegmentedQueryCache } from "./seen-cache.ts";
import { FullSchema, FullSchemaColumn } from "./schema_differ.ts";
import { ExtensionNotInstalledError, PostgresError } from "./errors.ts";
import { RawRecentQuery, RecentQuery } from "../sql/recent-query.ts";
import { ConnectionManager } from "./connection-manager.ts";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand Down
Loading