@rawsql-ts/ztd-cli 0.20.0 → 0.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +68 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/checkContract.d.ts +46 -0
- package/dist/commands/checkContract.js +359 -0
- package/dist/commands/checkContract.js.map +1 -0
- package/dist/commands/connectionOptions.d.ts +12 -0
- package/dist/commands/connectionOptions.js +22 -0
- package/dist/commands/connectionOptions.js.map +1 -0
- package/dist/commands/ddl.d.ts +7 -0
- package/dist/commands/ddl.js +145 -0
- package/dist/commands/ddl.js.map +1 -0
- package/dist/commands/describe.d.ts +23 -0
- package/dist/commands/describe.js +399 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/diff.d.ts +24 -0
- package/dist/commands/diff.js +73 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/genEntities.d.ts +14 -0
- package/dist/commands/genEntities.js +58 -0
- package/dist/commands/genEntities.js.map +1 -0
- package/dist/commands/init.d.ts +105 -0
- package/dist/commands/init.js +1508 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +89 -0
- package/dist/commands/lint.js +501 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/modelGen.d.ts +60 -0
- package/dist/commands/modelGen.js +572 -0
- package/dist/commands/modelGen.js.map +1 -0
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +48 -0
- package/dist/commands/options.js.map +1 -0
- package/dist/commands/perf.d.ts +9 -0
- package/dist/commands/perf.js +374 -0
- package/dist/commands/perf.js.map +1 -0
- package/dist/commands/pull.d.ts +21 -0
- package/dist/commands/pull.js +115 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/query.d.ts +9 -0
- package/dist/commands/query.js +377 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/testEvidence.d.ts +237 -0
- package/dist/commands/testEvidence.js +1220 -0
- package/dist/commands/testEvidence.js.map +1 -0
- package/dist/commands/ztdConfig.d.ts +30 -0
- package/dist/commands/ztdConfig.js +224 -0
- package/dist/commands/ztdConfig.js.map +1 -0
- package/dist/commands/ztdConfigCommand.d.ts +18 -0
- package/dist/commands/ztdConfigCommand.js +268 -0
- package/dist/commands/ztdConfigCommand.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/dist/perf/benchmark.d.ts +277 -0
- package/dist/perf/benchmark.js +2186 -0
- package/dist/perf/benchmark.js.map +1 -0
- package/dist/perf/sandbox.d.ts +73 -0
- package/dist/perf/sandbox.js +492 -0
- package/dist/perf/sandbox.js.map +1 -0
- package/dist/query/analysis.d.ts +20 -0
- package/dist/query/analysis.js +192 -0
- package/dist/query/analysis.js.map +1 -0
- package/dist/query/analyzeColumnUsage.d.ts +10 -0
- package/dist/query/analyzeColumnUsage.js +451 -0
- package/dist/query/analyzeColumnUsage.js.map +1 -0
- package/dist/query/analyzeTableUsage.d.ts +10 -0
- package/dist/query/analyzeTableUsage.js +318 -0
- package/dist/query/analyzeTableUsage.js.map +1 -0
- package/dist/query/execute.d.ts +40 -0
- package/dist/query/execute.js +784 -0
- package/dist/query/execute.js.map +1 -0
- package/dist/query/format.d.ts +1 -0
- package/dist/query/format.js +9 -0
- package/dist/query/format.js.map +1 -0
- package/dist/query/lint.d.ts +29 -0
- package/dist/query/lint.js +340 -0
- package/dist/query/lint.js.map +1 -0
- package/dist/query/location.d.ts +18 -0
- package/dist/query/location.js +204 -0
- package/dist/query/location.js.map +1 -0
- package/dist/query/patch.d.ts +21 -0
- package/dist/query/patch.js +151 -0
- package/dist/query/patch.js.map +1 -0
- package/dist/query/planner.d.ts +31 -0
- package/dist/query/planner.js +134 -0
- package/dist/query/planner.js.map +1 -0
- package/dist/query/report.d.ts +7 -0
- package/dist/query/report.js +19 -0
- package/dist/query/report.js.map +1 -0
- package/dist/query/scalarFilterAnalysis.d.ts +6 -0
- package/dist/query/scalarFilterAnalysis.js +212 -0
- package/dist/query/scalarFilterAnalysis.js.map +1 -0
- package/dist/query/slice.d.ts +17 -0
- package/dist/query/slice.js +204 -0
- package/dist/query/slice.js.map +1 -0
- package/dist/query/structure.d.ts +24 -0
- package/dist/query/structure.js +135 -0
- package/dist/query/structure.js.map +1 -0
- package/dist/query/targets.d.ts +2 -0
- package/dist/query/targets.js +6 -0
- package/dist/query/targets.js.map +1 -0
- package/dist/query/types.d.ts +97 -0
- package/dist/query/types.js +3 -0
- package/dist/query/types.js.map +1 -0
- package/dist/specs/sql/activeOrders.catalog.d.ts +12 -0
- package/dist/specs/sql/activeOrders.catalog.js +36 -0
- package/dist/specs/sql/activeOrders.catalog.js.map +1 -0
- package/dist/specs/sql/usersList.catalog.d.ts +8 -0
- package/dist/specs/sql/usersList.catalog.js +14 -0
- package/dist/specs/sql/usersList.catalog.js.map +1 -0
- package/dist/specs/sqlCatalogDefinition.d.ts +20 -0
- package/dist/specs/sqlCatalogDefinition.js +10 -0
- package/dist/specs/sqlCatalogDefinition.js.map +1 -0
- package/dist/utils/agentCli.d.ts +23 -0
- package/dist/utils/agentCli.js +84 -0
- package/dist/utils/agentCli.js.map +1 -0
- package/dist/utils/agentSafety.d.ts +4 -0
- package/dist/utils/agentSafety.js +50 -0
- package/dist/utils/agentSafety.js.map +1 -0
- package/dist/utils/agents.d.ts +31 -0
- package/dist/utils/agents.js +362 -0
- package/dist/utils/agents.js.map +1 -0
- package/dist/utils/collectSqlFiles.d.ts +9 -0
- package/dist/utils/collectSqlFiles.js +58 -0
- package/dist/utils/collectSqlFiles.js.map +1 -0
- package/dist/utils/connectionSummary.d.ts +3 -0
- package/dist/utils/connectionSummary.js +29 -0
- package/dist/utils/connectionSummary.js.map +1 -0
- package/dist/utils/dbConnection.d.ts +31 -0
- package/dist/utils/dbConnection.js +151 -0
- package/dist/utils/dbConnection.js.map +1 -0
- package/dist/utils/fs.d.ts +1 -0
- package/dist/utils/fs.js +12 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/modelGenBinder.d.ts +8 -0
- package/dist/utils/modelGenBinder.js +31 -0
- package/dist/utils/modelGenBinder.js.map +1 -0
- package/dist/utils/modelGenRender.d.ts +29 -0
- package/dist/utils/modelGenRender.js +158 -0
- package/dist/utils/modelGenRender.js.map +1 -0
- package/dist/utils/modelGenScanner.d.ts +24 -0
- package/dist/utils/modelGenScanner.js +196 -0
- package/dist/utils/modelGenScanner.js.map +1 -0
- package/dist/utils/modelProbe.d.ts +14 -0
- package/dist/utils/modelProbe.js +121 -0
- package/dist/utils/modelProbe.js.map +1 -0
- package/dist/utils/normalizePulledSchema.d.ts +12 -0
- package/dist/utils/normalizePulledSchema.js +213 -0
- package/dist/utils/normalizePulledSchema.js.map +1 -0
- package/dist/utils/optionalDependencies.d.ts +45 -0
- package/dist/utils/optionalDependencies.js +153 -0
- package/dist/utils/optionalDependencies.js.map +1 -0
- package/dist/utils/pgDump.d.ts +12 -0
- package/dist/utils/pgDump.js +58 -0
- package/dist/utils/pgDump.js.map +1 -0
- package/dist/utils/queryFingerprint.d.ts +14 -0
- package/dist/utils/queryFingerprint.js +34 -0
- package/dist/utils/queryFingerprint.js.map +1 -0
- package/dist/utils/sqlCatalogDiscovery.d.ts +44 -0
- package/dist/utils/sqlCatalogDiscovery.js +166 -0
- package/dist/utils/sqlCatalogDiscovery.js.map +1 -0
- package/dist/utils/sqlCatalogStatements.d.ts +20 -0
- package/dist/utils/sqlCatalogStatements.js +23 -0
- package/dist/utils/sqlCatalogStatements.js.map +1 -0
- package/dist/utils/sqlLintHelpers.d.ts +18 -0
- package/dist/utils/sqlLintHelpers.js +270 -0
- package/dist/utils/sqlLintHelpers.js.map +1 -0
- package/dist/utils/telemetry.d.ts +71 -0
- package/dist/utils/telemetry.js +597 -0
- package/dist/utils/telemetry.js.map +1 -0
- package/dist/utils/typeMapper.d.ts +4 -0
- package/dist/utils/typeMapper.js +79 -0
- package/dist/utils/typeMapper.js.map +1 -0
- package/dist/utils/ztdProjectConfig.d.ts +41 -0
- package/dist/utils/ztdProjectConfig.js +182 -0
- package/dist/utils/ztdProjectConfig.js.map +1 -0
- package/package.json +19 -20
- package/templates/src/catalog/runtime/_smoke.runtime.ts +2 -2
- package/templates/src/db/sql-client-adapters.ts +1 -1
- package/templates/src/infrastructure/db/sql-client-adapters.ts +1 -1
- package/templates/src/infrastructure/telemetry/consoleRepositoryTelemetry.ts +1 -1
- package/templates/src/infrastructure/telemetry/repositoryTelemetry.ts +4 -4
- package/templates/tests/smoke.test.ts +2 -2
- package/templates/tests/smoke.validation.test.ts +1 -1
- package/templates/tests/support/testkit-client.ts +1 -1
- package/templates/tests/support/testkit-client.webapi.ts +1 -1
|
@@ -0,0 +1,2186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PERF_BENCHMARK_DEFAULTS = void 0;
|
|
7
|
+
exports.runPerfBenchmark = runPerfBenchmark;
|
|
8
|
+
exports.toPerfPlannedSteps = toPerfPlannedSteps;
|
|
9
|
+
exports.mapPipelineStatements = mapPipelineStatements;
|
|
10
|
+
exports.diffPerfBenchmarkReports = diffPerfBenchmarkReports;
|
|
11
|
+
exports.formatPerfBenchmarkReport = formatPerfBenchmarkReport;
|
|
12
|
+
exports.formatPerfDiffReport = formatPerfDiffReport;
|
|
13
|
+
exports.buildPerfPipelineAnalysis = buildPerfPipelineAnalysis;
|
|
14
|
+
exports.summarizePerfDdlInventory = summarizePerfDdlInventory;
|
|
15
|
+
exports.buildPerfTuningGuidance = buildPerfTuningGuidance;
|
|
16
|
+
exports.buildPerfTuningSummary = buildPerfTuningSummary;
|
|
17
|
+
exports.loadPerfBenchmarkReport = loadPerfBenchmarkReport;
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
20
|
+
const yaml_1 = require("yaml");
|
|
21
|
+
const optionalDependencies_1 = require("../utils/optionalDependencies");
|
|
22
|
+
const modelGenBinder_1 = require("../utils/modelGenBinder");
|
|
23
|
+
const modelGenScanner_1 = require("../utils/modelGenScanner");
|
|
24
|
+
const structure_1 = require("../query/structure");
|
|
25
|
+
const scalarFilterAnalysis_1 = require("../query/scalarFilterAnalysis");
|
|
26
|
+
const execute_1 = require("../query/execute");
|
|
27
|
+
const planner_1 = require("../query/planner");
|
|
28
|
+
const sandbox_1 = require("./sandbox");
|
|
29
|
+
const DEFAULT_REPEAT = 10;
|
|
30
|
+
const DEFAULT_WARMUP = 3;
|
|
31
|
+
const DEFAULT_CLASSIFY_THRESHOLD_SECONDS = 60;
|
|
32
|
+
const DEFAULT_TIMEOUT_MINUTES = 5;
|
|
33
|
+
function assertValidPerfRunOptions(options) {
|
|
34
|
+
const issues = [];
|
|
35
|
+
if (!Number.isInteger(options.repeat) || options.repeat <= 0) {
|
|
36
|
+
issues.push('repeat must be a positive integer');
|
|
37
|
+
}
|
|
38
|
+
if (!Number.isInteger(options.warmup) || options.warmup < 0) {
|
|
39
|
+
issues.push('warmup must be a non-negative integer');
|
|
40
|
+
}
|
|
41
|
+
if (!Number.isFinite(options.timeoutMinutes) || options.timeoutMinutes <= 0) {
|
|
42
|
+
issues.push('timeoutMinutes must be greater than 0');
|
|
43
|
+
}
|
|
44
|
+
if (!Number.isFinite(options.classifyThresholdSeconds) || options.classifyThresholdSeconds <= 0) {
|
|
45
|
+
issues.push('classifyThresholdSeconds must be greater than 0');
|
|
46
|
+
}
|
|
47
|
+
if (issues.length > 0) {
|
|
48
|
+
throw new Error('invalid perf options: ' + issues.join('; '));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const PERF_REVIEW_POLICY_BY_SCALE = {
|
|
52
|
+
tiny: 'none',
|
|
53
|
+
small: 'none',
|
|
54
|
+
medium: 'recommended',
|
|
55
|
+
large: 'strongly-recommended',
|
|
56
|
+
batch: 'strongly-recommended'
|
|
57
|
+
};
|
|
58
|
+
const PERF_REVIEW_POLICY_SEVERITY = {
|
|
59
|
+
none: 0,
|
|
60
|
+
recommended: 1,
|
|
61
|
+
'strongly-recommended': 2
|
|
62
|
+
};
|
|
63
|
+
const PERF_SPEC_DISCOVERY_ROOTS = [
|
|
64
|
+
node_path_1.default.join('src', 'catalog', 'specs'),
|
|
65
|
+
node_path_1.default.join('src', 'specs'),
|
|
66
|
+
'specs'
|
|
67
|
+
];
|
|
68
|
+
function buildPerfQuerySpecGuidance(rootDir, queryFile, seedConfig, saveEvidence, dryRun) {
|
|
69
|
+
const discovered = discoverPerfQuerySpecMetadata(rootDir, queryFile);
|
|
70
|
+
if (!discovered) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const reviewPolicy = toPerfReviewPolicy(discovered.expectedScale, discovered.expectedInputRows);
|
|
74
|
+
const relationsUsed = (0, structure_1.buildQueryStructureReport)(queryFile, 'ztd perf run').referenced_tables;
|
|
75
|
+
const fixtureRowsAvailable = countPerfSeedRows(seedConfig, relationsUsed);
|
|
76
|
+
const fixtureRowsStatus = toPerfFixtureRowsStatus(fixtureRowsAvailable, discovered.expectedInputRows);
|
|
77
|
+
const evidenceStatus = saveEvidence && !dryRun
|
|
78
|
+
? 'captured'
|
|
79
|
+
: reviewPolicy === 'strongly-recommended' ? 'missing' : 'not-required';
|
|
80
|
+
return {
|
|
81
|
+
spec_id: discovered.specId,
|
|
82
|
+
spec_file: discovered.specFile,
|
|
83
|
+
expected_scale: discovered.expectedScale,
|
|
84
|
+
expected_input_rows: discovered.expectedInputRows,
|
|
85
|
+
expected_output_rows: discovered.expectedOutputRows,
|
|
86
|
+
review_policy: reviewPolicy,
|
|
87
|
+
evidence_status: evidenceStatus,
|
|
88
|
+
fixture_rows_available: fixtureRowsAvailable,
|
|
89
|
+
fixture_rows_status: fixtureRowsStatus
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function toPerfReviewPolicy(expectedScale, expectedInputRows) {
|
|
93
|
+
const scalePolicy = expectedScale ? PERF_REVIEW_POLICY_BY_SCALE[expectedScale] : 'none';
|
|
94
|
+
const rowsPolicy = expectedInputRows === undefined
|
|
95
|
+
? 'none'
|
|
96
|
+
: expectedInputRows >= 100000
|
|
97
|
+
? 'strongly-recommended'
|
|
98
|
+
: expectedInputRows >= 10000
|
|
99
|
+
? 'recommended'
|
|
100
|
+
: 'none';
|
|
101
|
+
return PERF_REVIEW_POLICY_SEVERITY[scalePolicy] >= PERF_REVIEW_POLICY_SEVERITY[rowsPolicy]
|
|
102
|
+
? scalePolicy
|
|
103
|
+
: rowsPolicy;
|
|
104
|
+
}
|
|
105
|
+
function toPerfFixtureRowsStatus(fixtureRowsAvailable, expectedInputRows) {
|
|
106
|
+
if (expectedInputRows === undefined || fixtureRowsAvailable === undefined) {
|
|
107
|
+
return 'unknown';
|
|
108
|
+
}
|
|
109
|
+
return fixtureRowsAvailable >= expectedInputRows ? 'sufficient' : 'undersized';
|
|
110
|
+
}
|
|
111
|
+
function countPerfSeedRows(seedConfig, relationsUsed) {
|
|
112
|
+
const normalizedRelations = normalizePerfRelationNames(relationsUsed);
|
|
113
|
+
if (normalizedRelations.size === 0) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
let matched = false;
|
|
117
|
+
let rows = 0;
|
|
118
|
+
for (const [tableName, tableSeed] of Object.entries(seedConfig.tables)) {
|
|
119
|
+
if (!matchesPerfSeedRelation(tableName, normalizedRelations)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
matched = true;
|
|
123
|
+
rows += tableSeed.rows;
|
|
124
|
+
}
|
|
125
|
+
return matched ? rows : undefined;
|
|
126
|
+
}
|
|
127
|
+
function discoverPerfQuerySpecMetadata(rootDir, queryFile) {
|
|
128
|
+
const queryCandidates = buildPerfQuerySpecSqlCandidates(rootDir, queryFile);
|
|
129
|
+
const matches = [];
|
|
130
|
+
for (const relativeRoot of PERF_SPEC_DISCOVERY_ROOTS) {
|
|
131
|
+
const absoluteRoot = node_path_1.default.resolve(rootDir, relativeRoot);
|
|
132
|
+
if (!(0, node_fs_1.existsSync)(absoluteRoot)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
for (const filePath of walkPerfSpecFiles(absoluteRoot)) {
|
|
136
|
+
matches.push(...loadPerfQuerySpecMetadataFromFile(filePath, queryCandidates));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (matches.length <= 1) {
|
|
140
|
+
return matches[0];
|
|
141
|
+
}
|
|
142
|
+
// Fail loudly when multiple specs claim the same SQL file so perf guidance stays deterministic.
|
|
143
|
+
throw new Error(`Multiple QuerySpecs matched ${node_path_1.default.resolve(queryFile)}: ${matches.map((match) => `${match.specId} (${match.specFile})`).join(', ')}`);
|
|
144
|
+
}
|
|
145
|
+
function buildPerfQuerySpecSqlCandidates(rootDir, queryFile) {
|
|
146
|
+
const absoluteQueryFile = node_path_1.default.resolve(queryFile);
|
|
147
|
+
const candidates = [
|
|
148
|
+
node_path_1.default.relative(rootDir, absoluteQueryFile),
|
|
149
|
+
node_path_1.default.relative(node_path_1.default.resolve(rootDir, 'src', 'sql'), absoluteQueryFile),
|
|
150
|
+
node_path_1.default.relative(node_path_1.default.resolve(rootDir, 'sql'), absoluteQueryFile)
|
|
151
|
+
].map((value) => normalizePerfSpecPath(value))
|
|
152
|
+
.filter((value) => value.length > 0 && !value.startsWith('..'));
|
|
153
|
+
return new Set(candidates);
|
|
154
|
+
}
|
|
155
|
+
function normalizePerfSpecPath(value) {
|
|
156
|
+
return value.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\//, '');
|
|
157
|
+
}
|
|
158
|
+
function walkPerfSpecFiles(rootDir) {
|
|
159
|
+
const files = [];
|
|
160
|
+
const stack = [rootDir];
|
|
161
|
+
while (stack.length > 0) {
|
|
162
|
+
const current = stack.pop();
|
|
163
|
+
const entries = (0, node_fs_1.readdirSync)(current, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
const absolute = node_path_1.default.join(current, entry.name);
|
|
166
|
+
if (entry.isDirectory()) {
|
|
167
|
+
stack.push(absolute);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!entry.isFile()) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (/\.(?:json|[cm]?[jt]s)$/iu.test(entry.name) && !/\.test\./iu.test(entry.name)) {
|
|
174
|
+
files.push(absolute);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
179
|
+
}
|
|
180
|
+
function loadPerfQuerySpecMetadataFromFile(filePath, queryCandidates) {
|
|
181
|
+
if (node_path_1.default.extname(filePath).toLowerCase() === '.json') {
|
|
182
|
+
return loadPerfQuerySpecMetadataFromJson(filePath, queryCandidates);
|
|
183
|
+
}
|
|
184
|
+
const source = (0, node_fs_1.readFileSync)(filePath, 'utf8');
|
|
185
|
+
const discovered = [];
|
|
186
|
+
for (const block of extractPerfQuerySpecBlocks(source)) {
|
|
187
|
+
const parsed = parsePerfQuerySpecBlock(block, filePath, queryCandidates);
|
|
188
|
+
if (parsed) {
|
|
189
|
+
discovered.push(parsed);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return discovered;
|
|
193
|
+
}
|
|
194
|
+
function loadPerfQuerySpecMetadataFromJson(filePath, queryCandidates) {
|
|
195
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(filePath, 'utf8'));
|
|
196
|
+
if (Array.isArray(parsed)) {
|
|
197
|
+
const matches = [];
|
|
198
|
+
for (const entry of parsed) {
|
|
199
|
+
const discovered = parsePerfQuerySpecObject(entry, filePath, queryCandidates);
|
|
200
|
+
if (discovered) {
|
|
201
|
+
matches.push(discovered);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return matches;
|
|
205
|
+
}
|
|
206
|
+
if (typeof parsed === 'object' && parsed !== null && Array.isArray(parsed.specs)) {
|
|
207
|
+
const matches = [];
|
|
208
|
+
for (const entry of parsed.specs) {
|
|
209
|
+
const discovered = parsePerfQuerySpecObject(entry, filePath, queryCandidates);
|
|
210
|
+
if (discovered) {
|
|
211
|
+
matches.push(discovered);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return matches;
|
|
215
|
+
}
|
|
216
|
+
const discovered = parsePerfQuerySpecObject(parsed, filePath, queryCandidates);
|
|
217
|
+
return discovered ? [discovered] : [];
|
|
218
|
+
}
|
|
219
|
+
function parsePerfQuerySpecObject(value, filePath, queryCandidates) {
|
|
220
|
+
var _a, _b, _c;
|
|
221
|
+
if (typeof value !== 'object' || value === null) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
const record = value;
|
|
225
|
+
const sqlFile = typeof record.sqlFile === 'string' ? record.sqlFile : undefined;
|
|
226
|
+
if (!sqlFile || !matchesPerfQuerySpecSqlFile(queryCandidates, sqlFile)) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
const metadata = typeof record.metadata === 'object' && record.metadata !== null
|
|
230
|
+
? record.metadata
|
|
231
|
+
: undefined;
|
|
232
|
+
const perf = metadata && typeof metadata.perf === 'object' && metadata.perf !== null
|
|
233
|
+
? metadata.perf
|
|
234
|
+
: undefined;
|
|
235
|
+
const expectedScale = normalizePerfExpectedScale((_a = perf === null || perf === void 0 ? void 0 : perf.expectedScale) !== null && _a !== void 0 ? _a : perf === null || perf === void 0 ? void 0 : perf.expected_scale);
|
|
236
|
+
const expectedInputRows = normalizePerfMetadataNumber((_b = perf === null || perf === void 0 ? void 0 : perf.expectedInputRows) !== null && _b !== void 0 ? _b : perf === null || perf === void 0 ? void 0 : perf.expected_input_rows);
|
|
237
|
+
const expectedOutputRows = normalizePerfMetadataNumber((_c = perf === null || perf === void 0 ? void 0 : perf.expectedOutputRows) !== null && _c !== void 0 ? _c : perf === null || perf === void 0 ? void 0 : perf.expected_output_rows);
|
|
238
|
+
if (!expectedScale && expectedInputRows === undefined && expectedOutputRows === undefined) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
specId: typeof record.id === 'string' ? record.id : normalizePerfSpecPath(sqlFile),
|
|
243
|
+
specFile: filePath,
|
|
244
|
+
expectedScale,
|
|
245
|
+
expectedInputRows,
|
|
246
|
+
expectedOutputRows
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function extractPerfQuerySpecBlocks(source) {
|
|
250
|
+
const blocks = [];
|
|
251
|
+
const seen = new Set();
|
|
252
|
+
const sqlFileRegex = /sqlFile\s*:\s*['"`][^'"`\n]+['"`]/g;
|
|
253
|
+
for (const match of source.matchAll(sqlFileRegex)) {
|
|
254
|
+
if (typeof match.index !== 'number') {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const start = source.lastIndexOf('{', match.index);
|
|
258
|
+
if (start < 0) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
let depth = 0;
|
|
262
|
+
let end = -1;
|
|
263
|
+
for (let index = start; index < source.length; index += 1) {
|
|
264
|
+
const char = source[index];
|
|
265
|
+
if (char === '{') {
|
|
266
|
+
depth += 1;
|
|
267
|
+
}
|
|
268
|
+
else if (char === '}') {
|
|
269
|
+
depth -= 1;
|
|
270
|
+
if (depth === 0) {
|
|
271
|
+
end = index;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (end < 0) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const block = source.slice(start, end + 1);
|
|
280
|
+
if (!seen.has(block)) {
|
|
281
|
+
seen.add(block);
|
|
282
|
+
blocks.push(block);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return blocks;
|
|
286
|
+
}
|
|
287
|
+
function parsePerfQuerySpecBlock(block, filePath, queryCandidates) {
|
|
288
|
+
var _a, _b, _c;
|
|
289
|
+
const sqlFile = (_a = block.match(/sqlFile\s*:\s*['"`]([^'"`\n]+)['"`]/u)) === null || _a === void 0 ? void 0 : _a[1];
|
|
290
|
+
if (!sqlFile || !matchesPerfQuerySpecSqlFile(queryCandidates, sqlFile)) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
const expectedScale = parsePerfExpectedScale(block);
|
|
294
|
+
const expectedInputRows = parsePerfMetadataNumber(block, ['expectedInputRows', 'expected_input_rows']);
|
|
295
|
+
const expectedOutputRows = parsePerfMetadataNumber(block, ['expectedOutputRows', 'expected_output_rows']);
|
|
296
|
+
if (!expectedScale && expectedInputRows === undefined && expectedOutputRows === undefined) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
const specId = (_c = (_b = block.match(/id\s*:\s*['"`]([^'"`\n]+)['"`]/u)) === null || _b === void 0 ? void 0 : _b[1]) !== null && _c !== void 0 ? _c : normalizePerfSpecPath(sqlFile);
|
|
300
|
+
return {
|
|
301
|
+
specId,
|
|
302
|
+
specFile: filePath,
|
|
303
|
+
expectedScale,
|
|
304
|
+
expectedInputRows,
|
|
305
|
+
expectedOutputRows
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function matchesPerfQuerySpecSqlFile(queryCandidates, sqlFile) {
|
|
309
|
+
const normalizedSpecSqlFile = normalizePerfSpecPath(sqlFile);
|
|
310
|
+
return queryCandidates.has(normalizedSpecSqlFile);
|
|
311
|
+
}
|
|
312
|
+
function parsePerfExpectedScale(block) {
|
|
313
|
+
const match = block.match(/expected(?:Scale|_scale)\s*:\s*['"`](tiny|small|medium|large|batch)['"`]/u);
|
|
314
|
+
return normalizePerfExpectedScale(match === null || match === void 0 ? void 0 : match[1]);
|
|
315
|
+
}
|
|
316
|
+
function parsePerfMetadataNumber(block, keys) {
|
|
317
|
+
for (const key of keys) {
|
|
318
|
+
const escapedKey = key.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
319
|
+
const match = block.match(new RegExp(`${escapedKey}\\s*:\\s*(\\d+(?:_\\d+)*(?:\\.\\d+)?)`, 'u'));
|
|
320
|
+
const parsed = normalizePerfMetadataNumber(match === null || match === void 0 ? void 0 : match[1]);
|
|
321
|
+
if (parsed !== undefined) {
|
|
322
|
+
return parsed;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
function normalizePerfExpectedScale(value) {
|
|
328
|
+
return value === 'tiny' || value === 'small' || value === 'medium' || value === 'large' || value === 'batch'
|
|
329
|
+
? value
|
|
330
|
+
: undefined;
|
|
331
|
+
}
|
|
332
|
+
function normalizePerfMetadataNumber(value) {
|
|
333
|
+
if (typeof value === 'number') {
|
|
334
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
335
|
+
}
|
|
336
|
+
if (typeof value === 'string') {
|
|
337
|
+
const parsed = Number(value.replace(/_/g, ''));
|
|
338
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
|
|
339
|
+
}
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
function normalizePerfRelationNames(relationsUsed) {
|
|
343
|
+
const normalized = new Set();
|
|
344
|
+
for (const relation of relationsUsed !== null && relationsUsed !== void 0 ? relationsUsed : []) {
|
|
345
|
+
const relationName = normalizePerfRelationName(relation);
|
|
346
|
+
if (!relationName) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
normalized.add(relationName);
|
|
350
|
+
const unqualified = relationName.split('.').at(-1);
|
|
351
|
+
if (unqualified) {
|
|
352
|
+
normalized.add(unqualified);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return normalized;
|
|
356
|
+
}
|
|
357
|
+
function matchesPerfSeedRelation(tableName, relationsUsed) {
|
|
358
|
+
const normalizedTableName = normalizePerfRelationName(tableName);
|
|
359
|
+
if (!normalizedTableName) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if (relationsUsed.has(normalizedTableName)) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
const unqualified = normalizedTableName.split('.').at(-1);
|
|
366
|
+
return Boolean(unqualified && relationsUsed.has(unqualified));
|
|
367
|
+
}
|
|
368
|
+
function normalizePerfRelationName(value) {
|
|
369
|
+
return value.trim().replace(/^"+|"+$/g, '').toLowerCase();
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Execute or plan a perf benchmark against the sandbox using either direct or decomposed execution.
|
|
373
|
+
*/
|
|
374
|
+
async function runPerfBenchmark(options) {
|
|
375
|
+
var _a, _b, _c;
|
|
376
|
+
const strategy = (_a = options.strategy) !== null && _a !== void 0 ? _a : 'direct';
|
|
377
|
+
const material = (_b = options.material) !== null && _b !== void 0 ? _b : [];
|
|
378
|
+
assertValidPerfRunOptions({ ...options, strategy, material });
|
|
379
|
+
const prepared = prepareBenchmarkQuery(options.rootDir, options.queryFile, options.paramsFile);
|
|
380
|
+
const pipelineAnalysis = buildPerfPipelineAnalysis(prepared.absolutePath);
|
|
381
|
+
const strategyMetadata = buildRequestedStrategyMetadata(prepared.absolutePath, strategy, material);
|
|
382
|
+
const classifyThresholdMs = options.classifyThresholdSeconds * 1000;
|
|
383
|
+
const timeoutMs = options.timeoutMinutes * 60 * 1000;
|
|
384
|
+
const seedConfig = (0, sandbox_1.loadPerfSeedConfig)(options.rootDir);
|
|
385
|
+
const specGuidance = buildPerfQuerySpecGuidance(options.rootDir, prepared.absolutePath, seedConfig, options.save, options.dryRun);
|
|
386
|
+
const ddlInventory = (0, sandbox_1.inspectPerfDdlInventory)(options.rootDir);
|
|
387
|
+
const ddlInventorySummary = summarizePerfDdlInventory(ddlInventory);
|
|
388
|
+
const databaseVersion = options.dryRun ? undefined : await fetchPerfDatabaseVersion(options.rootDir);
|
|
389
|
+
const selection = options.dryRun
|
|
390
|
+
? {
|
|
391
|
+
selectedMode: options.mode === 'auto' ? 'completion' : options.mode,
|
|
392
|
+
reason: options.mode === 'auto'
|
|
393
|
+
? 'dry-run skips live auto classification; the real run will pick latency or completion after a thresholded probe'
|
|
394
|
+
: 'mode forced by user'
|
|
395
|
+
}
|
|
396
|
+
: options.mode === 'auto'
|
|
397
|
+
? await classifyPerfBenchmarkMode(options.rootDir, prepared, strategy, material, classifyThresholdMs)
|
|
398
|
+
: {
|
|
399
|
+
selectedMode: options.mode,
|
|
400
|
+
reason: 'mode forced by user'
|
|
401
|
+
};
|
|
402
|
+
if (options.dryRun) {
|
|
403
|
+
const dryRunPlanFacts = {
|
|
404
|
+
observations: [],
|
|
405
|
+
statement_summary: '(no plan captured)',
|
|
406
|
+
hasCapturedPlan: false,
|
|
407
|
+
hasSequentialScan: false,
|
|
408
|
+
hasJoin: false
|
|
409
|
+
};
|
|
410
|
+
const tuningGuidance = buildPerfTuningGuidance(pipelineAnalysis, dryRunPlanFacts, specGuidance);
|
|
411
|
+
return {
|
|
412
|
+
schema_version: 1,
|
|
413
|
+
command: 'perf run',
|
|
414
|
+
query_file: prepared.absolutePath,
|
|
415
|
+
query_type: prepared.queryType,
|
|
416
|
+
params_file: options.paramsFile ? node_path_1.default.resolve(options.rootDir, options.paramsFile) : undefined,
|
|
417
|
+
params_shape: prepared.paramsShape,
|
|
418
|
+
ordered_param_names: prepared.orderedParamNames,
|
|
419
|
+
source_sql_file: prepared.absolutePath,
|
|
420
|
+
source_sql: prepared.sourceSql,
|
|
421
|
+
bound_sql: prepared.boundSql,
|
|
422
|
+
bindings: prepared.bindings,
|
|
423
|
+
strategy: strategy,
|
|
424
|
+
strategy_metadata: strategyMetadata,
|
|
425
|
+
requested_mode: options.mode,
|
|
426
|
+
selected_mode: selection.selectedMode,
|
|
427
|
+
selection_reason: selection.reason,
|
|
428
|
+
classify_threshold_ms: classifyThresholdMs,
|
|
429
|
+
timeout_ms: timeoutMs,
|
|
430
|
+
database_version: databaseVersion,
|
|
431
|
+
dry_run: true,
|
|
432
|
+
saved: false,
|
|
433
|
+
classification_probe: selection.probe ? toPerfClassificationProbe(selection.probe) : undefined,
|
|
434
|
+
executed_statements: buildDryRunStatements(prepared, strategy, strategyMetadata),
|
|
435
|
+
plan_observations: [],
|
|
436
|
+
recommended_actions: buildPerfRecommendedActions(selection.selectedMode, true, pipelineAnalysis, dryRunPlanFacts, specGuidance),
|
|
437
|
+
pipeline_analysis: pipelineAnalysis,
|
|
438
|
+
spec_guidance: specGuidance,
|
|
439
|
+
ddl_inventory: ddlInventorySummary,
|
|
440
|
+
tuning_guidance: tuningGuidance,
|
|
441
|
+
tuning_summary: buildPerfTuningSummary(tuningGuidance),
|
|
442
|
+
seed: { seed: seedConfig.seed }
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const latencyRuns = [];
|
|
446
|
+
let representativeExecution;
|
|
447
|
+
let classificationProbe = selection.probe ? toPerfClassificationProbe(selection.probe) : undefined;
|
|
448
|
+
if (selection.selectedMode === 'latency') {
|
|
449
|
+
let remainingWarmups = options.warmup;
|
|
450
|
+
let remainingMeasuredRuns = options.repeat;
|
|
451
|
+
// Reuse the auto-classification probe so the benchmark does not hide an extra live execution.
|
|
452
|
+
if (selection.probe) {
|
|
453
|
+
if (selection.probe.timedOut) {
|
|
454
|
+
throw new Error('Latency benchmark classification probe timed out unexpectedly. Re-run with --mode completion or a larger timeout.');
|
|
455
|
+
}
|
|
456
|
+
if (remainingWarmups > 0) {
|
|
457
|
+
remainingWarmups -= 1;
|
|
458
|
+
classificationProbe = {
|
|
459
|
+
...toPerfClassificationProbe(selection.probe),
|
|
460
|
+
reused_as_warmup: true
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
else if (remainingMeasuredRuns > 0) {
|
|
464
|
+
remainingMeasuredRuns -= 1;
|
|
465
|
+
latencyRuns.push(selection.probe.elapsedMs);
|
|
466
|
+
representativeExecution = selection.probe;
|
|
467
|
+
classificationProbe = {
|
|
468
|
+
...toPerfClassificationProbe(selection.probe),
|
|
469
|
+
reused_as_measured_run: true
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (let index = 0; index < remainingWarmups; index += 1) {
|
|
474
|
+
const warmupExecution = await executePerfBenchmarkOnce(options.rootDir, prepared, strategy, material, timeoutMs, false);
|
|
475
|
+
if (warmupExecution.timedOut) {
|
|
476
|
+
throw new Error('Latency benchmark timed out during warmup. Re-run with --mode completion or a larger timeout.');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
for (let index = 0; index < remainingMeasuredRuns; index += 1) {
|
|
480
|
+
const execution = await executePerfBenchmarkOnce(options.rootDir, prepared, strategy, material, timeoutMs, !representativeExecution);
|
|
481
|
+
if (execution.timedOut) {
|
|
482
|
+
throw new Error('Latency benchmark timed out during a measured run. Re-run with --mode completion or a larger timeout.');
|
|
483
|
+
}
|
|
484
|
+
latencyRuns.push(execution.elapsedMs);
|
|
485
|
+
if (!representativeExecution) {
|
|
486
|
+
representativeExecution = execution;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
if (selection.probe && !selection.probe.timedOut) {
|
|
492
|
+
representativeExecution = selection.probe;
|
|
493
|
+
classificationProbe = {
|
|
494
|
+
...toPerfClassificationProbe(selection.probe),
|
|
495
|
+
reused_as_measured_run: true
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
representativeExecution = await executePerfBenchmarkOnce(options.rootDir, prepared, strategy, material, timeoutMs, true);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (!representativeExecution) {
|
|
503
|
+
throw new Error('Perf benchmark did not produce a representative execution.');
|
|
504
|
+
}
|
|
505
|
+
const executedStatements = toPerfStatementReports(representativeExecution.statements);
|
|
506
|
+
const planFacts = buildPerfPlanFactsFromStatements(representativeExecution.statements);
|
|
507
|
+
const tuningGuidance = buildPerfTuningGuidance(pipelineAnalysis, planFacts, specGuidance);
|
|
508
|
+
const report = {
|
|
509
|
+
schema_version: 1,
|
|
510
|
+
command: 'perf run',
|
|
511
|
+
query_file: prepared.absolutePath,
|
|
512
|
+
query_type: prepared.queryType,
|
|
513
|
+
params_file: options.paramsFile ? node_path_1.default.resolve(options.rootDir, options.paramsFile) : undefined,
|
|
514
|
+
params_shape: prepared.paramsShape,
|
|
515
|
+
ordered_param_names: prepared.orderedParamNames,
|
|
516
|
+
source_sql_file: prepared.absolutePath,
|
|
517
|
+
source_sql: prepared.sourceSql,
|
|
518
|
+
bound_sql: prepared.boundSql,
|
|
519
|
+
bindings: prepared.bindings,
|
|
520
|
+
strategy: strategy,
|
|
521
|
+
strategy_metadata: (_c = representativeExecution.strategyMetadata) !== null && _c !== void 0 ? _c : strategyMetadata,
|
|
522
|
+
requested_mode: options.mode,
|
|
523
|
+
selected_mode: selection.selectedMode,
|
|
524
|
+
selection_reason: selection.reason,
|
|
525
|
+
classify_threshold_ms: classifyThresholdMs,
|
|
526
|
+
timeout_ms: timeoutMs,
|
|
527
|
+
database_version: databaseVersion,
|
|
528
|
+
dry_run: false,
|
|
529
|
+
saved: false,
|
|
530
|
+
classification_probe: classificationProbe,
|
|
531
|
+
total_elapsed_ms: selection.selectedMode === 'latency'
|
|
532
|
+
? latencyRuns.reduce((sum, value) => sum + value, 0)
|
|
533
|
+
: representativeExecution.elapsedMs,
|
|
534
|
+
latency_metrics: selection.selectedMode === 'latency'
|
|
535
|
+
? buildLatencyMetrics(latencyRuns, options.warmup)
|
|
536
|
+
: undefined,
|
|
537
|
+
completion_metrics: selection.selectedMode === 'completion'
|
|
538
|
+
? {
|
|
539
|
+
completed: !representativeExecution.timedOut,
|
|
540
|
+
timed_out: representativeExecution.timedOut,
|
|
541
|
+
wall_time_ms: representativeExecution.elapsedMs
|
|
542
|
+
}
|
|
543
|
+
: undefined,
|
|
544
|
+
executed_statements: executedStatements,
|
|
545
|
+
plan_summary: summarizePlanJson(representativeExecution.finalPlanJson),
|
|
546
|
+
plan_observations: planFacts.observations,
|
|
547
|
+
recommended_actions: buildPerfRecommendedActions(selection.selectedMode, !representativeExecution.timedOut, pipelineAnalysis, planFacts, specGuidance),
|
|
548
|
+
pipeline_analysis: pipelineAnalysis,
|
|
549
|
+
spec_guidance: specGuidance,
|
|
550
|
+
ddl_inventory: ddlInventorySummary,
|
|
551
|
+
tuning_guidance: tuningGuidance,
|
|
552
|
+
tuning_summary: buildPerfTuningSummary(tuningGuidance),
|
|
553
|
+
seed: { seed: seedConfig.seed }
|
|
554
|
+
};
|
|
555
|
+
if (options.save) {
|
|
556
|
+
const persisted = savePerfBenchmarkEvidence(options.rootDir, report, representativeExecution.statements);
|
|
557
|
+
report.run_id = persisted.runId;
|
|
558
|
+
report.evidence_dir = persisted.evidenceDir;
|
|
559
|
+
report.saved = true;
|
|
560
|
+
report.executed_statements = report.executed_statements.map((statement, index) => ({
|
|
561
|
+
...statement,
|
|
562
|
+
sql_file: persisted.sqlFiles[index] || undefined,
|
|
563
|
+
resolved_sql_preview_file: persisted.resolvedSqlPreviewFiles[index] || undefined,
|
|
564
|
+
plan_file: persisted.planFiles[index] || undefined
|
|
565
|
+
}));
|
|
566
|
+
}
|
|
567
|
+
return report;
|
|
568
|
+
}
|
|
569
|
+
function buildRequestedStrategyMetadata(sqlFile, strategy, material) {
|
|
570
|
+
if (strategy !== 'decomposed') {
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
const plan = (0, planner_1.buildQueryPipelinePlan)(sqlFile, { material });
|
|
574
|
+
return toPerfStrategyMetadata(plan);
|
|
575
|
+
}
|
|
576
|
+
function buildDryRunStatements(prepared, strategy, strategyMetadata) {
|
|
577
|
+
if (strategy !== 'decomposed' || !strategyMetadata) {
|
|
578
|
+
return [
|
|
579
|
+
{
|
|
580
|
+
seq: 1,
|
|
581
|
+
role: 'final-query',
|
|
582
|
+
target: 'FINAL_QUERY',
|
|
583
|
+
sql: prepared.boundSql,
|
|
584
|
+
bindings: prepared.bindings,
|
|
585
|
+
resolved_sql_preview: renderResolvedSqlPreview(prepared.boundSql, prepared.bindings)
|
|
586
|
+
}
|
|
587
|
+
];
|
|
588
|
+
}
|
|
589
|
+
return strategyMetadata.planned_steps.map((step) => ({
|
|
590
|
+
seq: step.step,
|
|
591
|
+
role: mapPipelineStepKindToRole(step.kind),
|
|
592
|
+
target: step.target,
|
|
593
|
+
sql: step.kind === 'materialize'
|
|
594
|
+
? `create temp table "${step.target.replace(/"/g, '""')}" as -- resolved at runtime`
|
|
595
|
+
: prepared.boundSql,
|
|
596
|
+
bindings: prepared.bindings,
|
|
597
|
+
resolved_sql_preview: step.kind === 'materialize'
|
|
598
|
+
? `materialize ${step.target} from ${node_path_1.default.basename(prepared.absolutePath)}`
|
|
599
|
+
: renderResolvedSqlPreview(prepared.boundSql, prepared.bindings)
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
async function executePerfBenchmarkOnce(rootDir, prepared, strategy, material, timeoutMs, capturePlans) {
|
|
603
|
+
return strategy === 'decomposed'
|
|
604
|
+
? executeDecomposedBenchmarkOnce(rootDir, prepared, material, timeoutMs, capturePlans)
|
|
605
|
+
: executeDirectBenchmarkDetailed(rootDir, prepared, timeoutMs, capturePlans);
|
|
606
|
+
}
|
|
607
|
+
function preparePgStatementExecution(sql, params) {
|
|
608
|
+
if (!params) {
|
|
609
|
+
return { sql, bindings: undefined, resolvedSqlPreview: undefined };
|
|
610
|
+
}
|
|
611
|
+
if (Array.isArray(params)) {
|
|
612
|
+
return {
|
|
613
|
+
sql,
|
|
614
|
+
bindings: params,
|
|
615
|
+
resolvedSqlPreview: renderResolvedSqlPreview(sql, params)
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const scan = (0, modelGenScanner_1.scanModelGenSql)(sql);
|
|
619
|
+
if (scan.mode !== 'named') {
|
|
620
|
+
return { sql, bindings: undefined, resolvedSqlPreview: undefined };
|
|
621
|
+
}
|
|
622
|
+
const bound = (0, modelGenBinder_1.bindModelGenNamedSql)(sql);
|
|
623
|
+
const bindings = bound.orderedParamNames.map((name) => {
|
|
624
|
+
if (!(name in params)) {
|
|
625
|
+
throw new Error(`Missing named pipeline param: ${name}`);
|
|
626
|
+
}
|
|
627
|
+
return params[name];
|
|
628
|
+
});
|
|
629
|
+
return {
|
|
630
|
+
sql: bound.boundSql,
|
|
631
|
+
bindings,
|
|
632
|
+
resolvedSqlPreview: renderResolvedSqlPreview(bound.boundSql, bindings)
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function buildExplainTargetSql(sql) {
|
|
636
|
+
var _a;
|
|
637
|
+
const match = /^create\s+temp\s+table\s+.+?\s+as\s+([\s\S]+)$/i.exec(sql.trim());
|
|
638
|
+
return ((_a = match === null || match === void 0 ? void 0 : match[1]) === null || _a === void 0 ? void 0 : _a.trim()) || sql;
|
|
639
|
+
}
|
|
640
|
+
async function executeDirectBenchmarkDetailed(rootDir, prepared, timeoutMs, capturePlans) {
|
|
641
|
+
const pg = await (0, optionalDependencies_1.ensurePgModule)();
|
|
642
|
+
const sandboxConfig = (0, sandbox_1.loadPerfSandboxConfig)(rootDir);
|
|
643
|
+
const resolvedConnection = await (0, sandbox_1.ensurePerfConnection)(rootDir, sandboxConfig);
|
|
644
|
+
const client = new pg.Client({
|
|
645
|
+
connectionString: resolvedConnection.connectionUrl,
|
|
646
|
+
connectionTimeoutMillis: 3000
|
|
647
|
+
});
|
|
648
|
+
let statementTrace;
|
|
649
|
+
let finalPlanJson = null;
|
|
650
|
+
try {
|
|
651
|
+
await client.connect();
|
|
652
|
+
await client.query(`SET statement_timeout = ${Math.max(1, Math.trunc(timeoutMs))}`);
|
|
653
|
+
// Measure only live statement execution so connection/auth and plan capture do not pollute SQL latency.
|
|
654
|
+
const startedAt = nowMs();
|
|
655
|
+
const result = await client.query(prepared.boundSql, prepared.bindings);
|
|
656
|
+
const elapsedMs = nowMs() - startedAt;
|
|
657
|
+
if (capturePlans) {
|
|
658
|
+
finalPlanJson = await capturePlanWithConnectedClient(client, prepared.boundSql, Array.isArray(prepared.bindings) ? prepared.bindings : undefined, true, timeoutMs);
|
|
659
|
+
}
|
|
660
|
+
statementTrace = {
|
|
661
|
+
role: 'final-query',
|
|
662
|
+
target: 'FINAL_QUERY',
|
|
663
|
+
sql: prepared.boundSql,
|
|
664
|
+
bindings: prepared.bindings,
|
|
665
|
+
resolvedSqlPreview: renderResolvedSqlPreview(prepared.boundSql, prepared.bindings),
|
|
666
|
+
elapsedMs,
|
|
667
|
+
rowCount: extractRowCount(result),
|
|
668
|
+
timedOut: false,
|
|
669
|
+
planJson: finalPlanJson
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
if (!isQueryTimeout(error)) {
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
statementTrace = {
|
|
677
|
+
role: 'final-query',
|
|
678
|
+
target: 'FINAL_QUERY',
|
|
679
|
+
sql: prepared.boundSql,
|
|
680
|
+
bindings: prepared.bindings,
|
|
681
|
+
resolvedSqlPreview: renderResolvedSqlPreview(prepared.boundSql, prepared.bindings),
|
|
682
|
+
elapsedMs: timeoutMs,
|
|
683
|
+
timedOut: true
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
finally {
|
|
687
|
+
await client.end().catch(() => undefined);
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
elapsedMs: statementTrace.elapsedMs,
|
|
691
|
+
rowCount: statementTrace.rowCount,
|
|
692
|
+
timedOut: statementTrace.timedOut,
|
|
693
|
+
statements: [statementTrace],
|
|
694
|
+
finalPlanJson
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
async function executeDecomposedBenchmarkOnce(rootDir, prepared, material, timeoutMs, capturePlans) {
|
|
698
|
+
var _a, _b;
|
|
699
|
+
const pg = await (0, optionalDependencies_1.ensurePgModule)();
|
|
700
|
+
const sandboxConfig = (0, sandbox_1.loadPerfSandboxConfig)(rootDir);
|
|
701
|
+
const resolvedConnection = await (0, sandbox_1.ensurePerfConnection)(rootDir, sandboxConfig);
|
|
702
|
+
const plan = (0, planner_1.buildQueryPipelinePlan)(prepared.absolutePath, { material });
|
|
703
|
+
const rawStatements = [];
|
|
704
|
+
const client = new pg.Client({
|
|
705
|
+
connectionString: resolvedConnection.connectionUrl,
|
|
706
|
+
connectionTimeoutMillis: 3000
|
|
707
|
+
});
|
|
708
|
+
let totalElapsedMs = 0;
|
|
709
|
+
const sessionFactory = {
|
|
710
|
+
openSession: async () => ({
|
|
711
|
+
query: async (sql, params) => {
|
|
712
|
+
const execution = preparePgStatementExecution(sql, params);
|
|
713
|
+
if (shouldSkipPerfStatementCapture(sql)) {
|
|
714
|
+
return client.query(execution.sql, execution.bindings);
|
|
715
|
+
}
|
|
716
|
+
const remainingMs = Math.max(1, Math.trunc(timeoutMs - totalElapsedMs));
|
|
717
|
+
await client.query(`SET statement_timeout = ${remainingMs}`);
|
|
718
|
+
// Track total elapsed time across decomposed statements while keeping per-statement timings separate.
|
|
719
|
+
const startedAt = nowMs();
|
|
720
|
+
try {
|
|
721
|
+
const result = await client.query(execution.sql, execution.bindings);
|
|
722
|
+
const elapsedMs = nowMs() - startedAt;
|
|
723
|
+
totalElapsedMs += elapsedMs;
|
|
724
|
+
const planJson = capturePlans
|
|
725
|
+
? await capturePlanWithConnectedClient(client, buildExplainTargetSql(execution.sql), execution.bindings, false, remainingMs)
|
|
726
|
+
: null;
|
|
727
|
+
rawStatements.push({
|
|
728
|
+
sql: execution.sql,
|
|
729
|
+
bindings: execution.bindings,
|
|
730
|
+
resolvedSqlPreview: execution.resolvedSqlPreview,
|
|
731
|
+
elapsedMs,
|
|
732
|
+
rowCount: extractRowCount(result),
|
|
733
|
+
timedOut: false,
|
|
734
|
+
planJson
|
|
735
|
+
});
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
if (!isQueryTimeout(error)) {
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
const elapsedMs = Math.min(timeoutMs, Math.max(remainingMs, nowMs() - startedAt));
|
|
743
|
+
totalElapsedMs += elapsedMs;
|
|
744
|
+
rawStatements.push({
|
|
745
|
+
sql: execution.sql,
|
|
746
|
+
bindings: execution.bindings,
|
|
747
|
+
resolvedSqlPreview: execution.resolvedSqlPreview,
|
|
748
|
+
elapsedMs,
|
|
749
|
+
timedOut: true
|
|
750
|
+
});
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
end: async () => undefined
|
|
755
|
+
})
|
|
756
|
+
};
|
|
757
|
+
try {
|
|
758
|
+
await client.connect();
|
|
759
|
+
const pipelineResult = await (0, execute_1.executeQueryPipeline)(sessionFactory, {
|
|
760
|
+
sqlFile: prepared.absolutePath,
|
|
761
|
+
metadata: { material },
|
|
762
|
+
params: prepared.runtimeBindings
|
|
763
|
+
});
|
|
764
|
+
const statements = mapPipelineStatements(rawStatements, toPerfPlannedSteps(pipelineResult.steps));
|
|
765
|
+
const finalStatement = statements.find((statement) => statement.role === 'final-query');
|
|
766
|
+
return {
|
|
767
|
+
elapsedMs: totalElapsedMs,
|
|
768
|
+
rowCount: pipelineResult.final.rowCount,
|
|
769
|
+
timedOut: false,
|
|
770
|
+
statements,
|
|
771
|
+
finalPlanJson: (_a = finalStatement === null || finalStatement === void 0 ? void 0 : finalStatement.planJson) !== null && _a !== void 0 ? _a : null,
|
|
772
|
+
strategyMetadata: toPerfStrategyMetadata(plan)
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
if (!isQueryTimeout(error)) {
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
const statements = mapPipelineStatements(rawStatements, toPerfPlannedSteps(plan.steps.slice(0, rawStatements.length)));
|
|
780
|
+
const finalStatement = [...statements].reverse().find((statement) => statement.role === 'final-query');
|
|
781
|
+
return {
|
|
782
|
+
elapsedMs: totalElapsedMs,
|
|
783
|
+
rowCount: finalStatement === null || finalStatement === void 0 ? void 0 : finalStatement.rowCount,
|
|
784
|
+
timedOut: true,
|
|
785
|
+
statements,
|
|
786
|
+
finalPlanJson: (_b = finalStatement === null || finalStatement === void 0 ? void 0 : finalStatement.planJson) !== null && _b !== void 0 ? _b : null,
|
|
787
|
+
strategyMetadata: toPerfStrategyMetadata(plan)
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
finally {
|
|
791
|
+
await client.end().catch(() => undefined);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async function capturePlanWithConnectedClient(client, sql, params, analyze, timeoutMs) {
|
|
795
|
+
var _a, _b, _c, _d;
|
|
796
|
+
const explainPrefix = analyze
|
|
797
|
+
? 'EXPLAIN (ANALYZE TRUE, BUFFERS TRUE, FORMAT JSON) '
|
|
798
|
+
: 'EXPLAIN (FORMAT JSON) ';
|
|
799
|
+
try {
|
|
800
|
+
await client.query(`SET statement_timeout = ${Math.max(1, Math.trunc(timeoutMs))}`);
|
|
801
|
+
const result = await client.query(`${explainPrefix}${sql}`, params);
|
|
802
|
+
const firstRow = ((_b = (_a = result.rows) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : {});
|
|
803
|
+
return (_d = (_c = firstRow['QUERY PLAN']) !== null && _c !== void 0 ? _c : firstRow['query plan']) !== null && _d !== void 0 ? _d : null;
|
|
804
|
+
}
|
|
805
|
+
catch (error) {
|
|
806
|
+
if (isQueryTimeout(error)) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function toPerfPlannedSteps(steps) {
|
|
813
|
+
return steps
|
|
814
|
+
// scalar-filter-bind is emitted by execution tracing, not QueryPipelinePlan metadata.
|
|
815
|
+
.filter((step) => step.kind !== 'scalar-filter-bind')
|
|
816
|
+
.map((step) => ({
|
|
817
|
+
kind: mapPipelineStepKindToRole(step.kind),
|
|
818
|
+
target: step.target
|
|
819
|
+
}));
|
|
820
|
+
}
|
|
821
|
+
function mapPipelineStatements(statements, plannedSteps) {
|
|
822
|
+
return statements.map((statement, index) => {
|
|
823
|
+
var _a, _b, _c, _d, _e;
|
|
824
|
+
return ({
|
|
825
|
+
role: (_b = (_a = plannedSteps === null || plannedSteps === void 0 ? void 0 : plannedSteps[index]) === null || _a === void 0 ? void 0 : _a.kind) !== null && _b !== void 0 ? _b : (index === statements.length - 1 ? 'final-query' : 'materialize'),
|
|
826
|
+
target: (_d = (_c = plannedSteps === null || plannedSteps === void 0 ? void 0 : plannedSteps[index]) === null || _c === void 0 ? void 0 : _c.target) !== null && _d !== void 0 ? _d : (index === statements.length - 1 ? 'FINAL_QUERY' : `stage_${index + 1}`),
|
|
827
|
+
sql: statement.sql,
|
|
828
|
+
bindings: statement.bindings,
|
|
829
|
+
resolvedSqlPreview: statement.resolvedSqlPreview,
|
|
830
|
+
elapsedMs: statement.elapsedMs,
|
|
831
|
+
rowCount: statement.rowCount,
|
|
832
|
+
timedOut: statement.timedOut,
|
|
833
|
+
planJson: (_e = statement.planJson) !== null && _e !== void 0 ? _e : null
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
function mapPipelineStepKindToRole(kind) {
|
|
838
|
+
return kind === 'materialize' ? 'materialize' : 'final-query';
|
|
839
|
+
}
|
|
840
|
+
function shouldSkipPerfStatementCapture(sql) {
|
|
841
|
+
return /^\s*drop\s+table\s+if\s+exists\b/i.test(sql);
|
|
842
|
+
}
|
|
843
|
+
function toPerfStrategyMetadata(plan) {
|
|
844
|
+
return {
|
|
845
|
+
materialized_ctes: [...plan.metadata.material],
|
|
846
|
+
scalar_filter_columns: [...plan.metadata.scalarFilterColumns],
|
|
847
|
+
planned_steps: plan.steps.map((step) => ({
|
|
848
|
+
step: step.step,
|
|
849
|
+
kind: step.kind,
|
|
850
|
+
target: step.target,
|
|
851
|
+
depends_on: [...step.depends_on]
|
|
852
|
+
}))
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function toPerfStatementReports(statements) {
|
|
856
|
+
return statements.map((statement, index) => ({
|
|
857
|
+
seq: index + 1,
|
|
858
|
+
role: statement.role,
|
|
859
|
+
target: statement.target,
|
|
860
|
+
sql: statement.sql,
|
|
861
|
+
bindings: statement.bindings,
|
|
862
|
+
resolved_sql_preview: statement.resolvedSqlPreview,
|
|
863
|
+
row_count: statement.rowCount,
|
|
864
|
+
elapsed_ms: statement.elapsedMs,
|
|
865
|
+
timed_out: statement.timedOut,
|
|
866
|
+
plan_summary: summarizePlanJson(statement.planJson)
|
|
867
|
+
}));
|
|
868
|
+
}
|
|
869
|
+
function buildPerfPlanFactsFromStatements(statements) {
|
|
870
|
+
var _a;
|
|
871
|
+
const observations = [];
|
|
872
|
+
const summaries = [];
|
|
873
|
+
let hasCapturedPlan = false;
|
|
874
|
+
let hasSequentialScan = false;
|
|
875
|
+
let hasJoin = false;
|
|
876
|
+
for (const statement of statements) {
|
|
877
|
+
const facts = buildPerfPlanFacts((_a = statement.planJson) !== null && _a !== void 0 ? _a : null);
|
|
878
|
+
const prefix = `${statement.role}(${statement.target})`;
|
|
879
|
+
summaries.push(`${prefix}: ${facts.statement_summary}`);
|
|
880
|
+
observations.push(...facts.observations.map((observation) => `${prefix}: ${observation}`));
|
|
881
|
+
hasCapturedPlan = hasCapturedPlan || facts.hasCapturedPlan;
|
|
882
|
+
hasSequentialScan = hasSequentialScan || facts.hasSequentialScan;
|
|
883
|
+
hasJoin = hasJoin || facts.hasJoin;
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
observations: Array.from(new Set(observations)),
|
|
887
|
+
statement_summary: summaries.join(' | ') || '(no plan captured)',
|
|
888
|
+
hasCapturedPlan,
|
|
889
|
+
hasSequentialScan,
|
|
890
|
+
hasJoin
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Compare two saved benchmark evidence directories for AI-friendly tuning decisions.
|
|
895
|
+
*/
|
|
896
|
+
function diffPerfBenchmarkReports(baselineDir, candidateDir) {
|
|
897
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u;
|
|
898
|
+
const baseline = loadPerfBenchmarkReport(baselineDir);
|
|
899
|
+
const candidate = loadPerfBenchmarkReport(candidateDir);
|
|
900
|
+
const notes = [];
|
|
901
|
+
const modeChanged = baseline.selected_mode !== candidate.selected_mode;
|
|
902
|
+
const strategyChanged = baseline.strategy !== candidate.strategy;
|
|
903
|
+
const statementsDelta = candidate.executed_statements.length - baseline.executed_statements.length;
|
|
904
|
+
const statementDeltas = buildPerfStatementDeltas(baseline, candidate);
|
|
905
|
+
const planDeltas = buildPerfPlanDeltas(baseline, candidate);
|
|
906
|
+
let metricName = 'total_elapsed_ms';
|
|
907
|
+
let baselineMetric = (_a = baseline.total_elapsed_ms) !== null && _a !== void 0 ? _a : 0;
|
|
908
|
+
let candidateMetric = (_b = candidate.total_elapsed_ms) !== null && _b !== void 0 ? _b : 0;
|
|
909
|
+
if (baseline.selected_mode === 'latency' && candidate.selected_mode === 'latency') {
|
|
910
|
+
metricName = 'p95_ms';
|
|
911
|
+
baselineMetric = (_e = (_d = (_c = baseline.latency_metrics) === null || _c === void 0 ? void 0 : _c.p95_ms) !== null && _d !== void 0 ? _d : baseline.total_elapsed_ms) !== null && _e !== void 0 ? _e : 0;
|
|
912
|
+
candidateMetric = (_h = (_g = (_f = candidate.latency_metrics) === null || _f === void 0 ? void 0 : _f.p95_ms) !== null && _g !== void 0 ? _g : candidate.total_elapsed_ms) !== null && _h !== void 0 ? _h : 0;
|
|
913
|
+
notes.push('Compared latency-mode p95 because both runs are repeat benchmarks.');
|
|
914
|
+
}
|
|
915
|
+
else if (baseline.selected_mode === 'completion' && candidate.selected_mode === 'completion') {
|
|
916
|
+
metricName = 'wall_time_ms';
|
|
917
|
+
baselineMetric = (_l = (_k = (_j = baseline.completion_metrics) === null || _j === void 0 ? void 0 : _j.wall_time_ms) !== null && _k !== void 0 ? _k : baseline.total_elapsed_ms) !== null && _l !== void 0 ? _l : 0;
|
|
918
|
+
candidateMetric = (_p = (_o = (_m = candidate.completion_metrics) === null || _m === void 0 ? void 0 : _m.wall_time_ms) !== null && _o !== void 0 ? _o : candidate.total_elapsed_ms) !== null && _p !== void 0 ? _p : 0;
|
|
919
|
+
notes.push('Compared completion wall time because both runs are long-running benchmarks.');
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
notes.push('Modes differ, so diff falls back to total elapsed time instead of p95.');
|
|
923
|
+
}
|
|
924
|
+
if (modeChanged) {
|
|
925
|
+
notes.push(`Mode changed from ${baseline.selected_mode} to ${candidate.selected_mode}.`);
|
|
926
|
+
}
|
|
927
|
+
if (strategyChanged) {
|
|
928
|
+
notes.push(`Strategy changed from ${baseline.strategy} to ${candidate.strategy}.`);
|
|
929
|
+
}
|
|
930
|
+
if (baseline.pipeline_analysis.should_consider_pipeline || candidate.pipeline_analysis.should_consider_pipeline) {
|
|
931
|
+
notes.push('Pipeline candidacy is present in at least one run; inspect candidate_ctes before rewriting SQL.');
|
|
932
|
+
}
|
|
933
|
+
if (((_r = (_q = baseline.pipeline_analysis.scalar_filter_candidates) === null || _q === void 0 ? void 0 : _q.length) !== null && _r !== void 0 ? _r : 0) > 0 || ((_t = (_s = candidate.pipeline_analysis.scalar_filter_candidates) === null || _s === void 0 ? void 0 : _s.length) !== null && _t !== void 0 ? _t : 0) > 0) {
|
|
934
|
+
notes.push('Scalar filter binding candidates are present; inspect scalar_filter_candidates before keeping optimizer-sensitive subqueries inline.');
|
|
935
|
+
}
|
|
936
|
+
if (baseline.database_version && candidate.database_version && baseline.database_version !== candidate.database_version) {
|
|
937
|
+
notes.push(`Database version changed from ${baseline.database_version} to ${candidate.database_version}.`);
|
|
938
|
+
}
|
|
939
|
+
const candidateActions = (_u = candidate.recommended_actions) !== null && _u !== void 0 ? _u : [];
|
|
940
|
+
if (candidateActions.length > 0) {
|
|
941
|
+
notes.push('Candidate recommended actions: ' + candidateActions.map((action) => action.action).join(', '));
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
schema_version: 1,
|
|
945
|
+
command: 'perf report diff',
|
|
946
|
+
baseline_run_id: baseline.run_id,
|
|
947
|
+
candidate_run_id: candidate.run_id,
|
|
948
|
+
baseline_mode: baseline.selected_mode,
|
|
949
|
+
candidate_mode: candidate.selected_mode,
|
|
950
|
+
baseline_strategy: baseline.strategy,
|
|
951
|
+
candidate_strategy: candidate.strategy,
|
|
952
|
+
primary_metric: {
|
|
953
|
+
name: metricName,
|
|
954
|
+
baseline: baselineMetric,
|
|
955
|
+
candidate: candidateMetric,
|
|
956
|
+
improvement_percent: calculateImprovementPercent(baselineMetric, candidateMetric)
|
|
957
|
+
},
|
|
958
|
+
mode_changed: modeChanged,
|
|
959
|
+
strategy_changed: strategyChanged,
|
|
960
|
+
statements_delta: statementsDelta,
|
|
961
|
+
statement_deltas: statementDeltas,
|
|
962
|
+
plan_deltas: planDeltas,
|
|
963
|
+
notes
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Render a benchmark report in either text or JSON for humans and agents.
|
|
968
|
+
*/
|
|
969
|
+
function formatPerfBenchmarkReport(report, format) {
|
|
970
|
+
var _a, _b, _c;
|
|
971
|
+
if (format === 'json') {
|
|
972
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
973
|
+
}
|
|
974
|
+
const lines = [
|
|
975
|
+
`Query: ${report.query_file}`,
|
|
976
|
+
`Mode: ${report.selected_mode} (requested: ${report.requested_mode})`,
|
|
977
|
+
`Selection: ${report.selection_reason}`,
|
|
978
|
+
`Strategy: ${report.strategy}`,
|
|
979
|
+
`Timeout: ${Math.round(report.timeout_ms / 1000)}s`,
|
|
980
|
+
`Statements: ${report.executed_statements.length}`,
|
|
981
|
+
];
|
|
982
|
+
if (report.latency_metrics) {
|
|
983
|
+
lines.push(`Measured runs: ${report.latency_metrics.measured_runs}`);
|
|
984
|
+
lines.push(`avg: ${report.latency_metrics.avg_ms.toFixed(2)} ms`);
|
|
985
|
+
lines.push(`median: ${report.latency_metrics.median_ms.toFixed(2)} ms`);
|
|
986
|
+
lines.push(`p95: ${report.latency_metrics.p95_ms.toFixed(2)} ms`);
|
|
987
|
+
}
|
|
988
|
+
if (report.completion_metrics) {
|
|
989
|
+
lines.push(`completed: ${report.completion_metrics.completed ? 'yes' : 'no'}`);
|
|
990
|
+
lines.push(`timed_out: ${report.completion_metrics.timed_out ? 'yes' : 'no'}`);
|
|
991
|
+
lines.push(`wall_time: ${report.completion_metrics.wall_time_ms.toFixed(2)} ms`);
|
|
992
|
+
}
|
|
993
|
+
if (report.classification_probe) {
|
|
994
|
+
const probeSuffix = report.classification_probe.timed_out ? ' (timed out)' : '';
|
|
995
|
+
lines.push(`Classification probe: ${report.classification_probe.elapsed_ms.toFixed(2)} ms${probeSuffix}`);
|
|
996
|
+
}
|
|
997
|
+
if (report.tuning_summary) {
|
|
998
|
+
lines.push(`Decision summary: ${report.tuning_summary.headline}`);
|
|
999
|
+
for (const evidence of report.tuning_summary.evidence) {
|
|
1000
|
+
lines.push(` evidence: ${evidence}`);
|
|
1001
|
+
}
|
|
1002
|
+
lines.push(` next: ${report.tuning_summary.next_step}`);
|
|
1003
|
+
}
|
|
1004
|
+
lines.push('');
|
|
1005
|
+
if (report.spec_guidance) {
|
|
1006
|
+
lines.push('');
|
|
1007
|
+
lines.push('Query spec guidance:');
|
|
1008
|
+
lines.push(` spec_id: ${report.spec_guidance.spec_id}`);
|
|
1009
|
+
lines.push(` spec_file: ${report.spec_guidance.spec_file}`);
|
|
1010
|
+
lines.push(` expected_scale: ${(_a = report.spec_guidance.expected_scale) !== null && _a !== void 0 ? _a : '(unspecified)'}`);
|
|
1011
|
+
lines.push(` review_policy: ${report.spec_guidance.review_policy}`);
|
|
1012
|
+
lines.push(` evidence_status: ${report.spec_guidance.evidence_status}`);
|
|
1013
|
+
lines.push(` fixture_rows_available: ${(_b = report.spec_guidance.fixture_rows_available) !== null && _b !== void 0 ? _b : '(unknown)'}`);
|
|
1014
|
+
lines.push(` fixture_rows_status: ${report.spec_guidance.fixture_rows_status}`);
|
|
1015
|
+
if (report.spec_guidance.expected_input_rows !== undefined) {
|
|
1016
|
+
lines.push(` expected_input_rows: ${report.spec_guidance.expected_input_rows}`);
|
|
1017
|
+
}
|
|
1018
|
+
if (report.spec_guidance.expected_output_rows !== undefined) {
|
|
1019
|
+
lines.push(` expected_output_rows: ${report.spec_guidance.expected_output_rows}`);
|
|
1020
|
+
}
|
|
1021
|
+
lines.push('');
|
|
1022
|
+
}
|
|
1023
|
+
if (report.ddl_inventory) {
|
|
1024
|
+
lines.push('DDL inventory:');
|
|
1025
|
+
lines.push(` ddl_files: ${report.ddl_inventory.ddl_files}`);
|
|
1026
|
+
lines.push(` ddl_statement_count: ${report.ddl_inventory.ddl_statement_count}`);
|
|
1027
|
+
lines.push(` table_count: ${report.ddl_inventory.table_count}`);
|
|
1028
|
+
lines.push(` index_count: ${report.ddl_inventory.index_count}`);
|
|
1029
|
+
lines.push(` index_names: ${report.ddl_inventory.index_names.length > 0 ? report.ddl_inventory.index_names.join(', ') : '(none)'}`);
|
|
1030
|
+
lines.push('');
|
|
1031
|
+
}
|
|
1032
|
+
if (report.tuning_guidance) {
|
|
1033
|
+
lines.push('Tuning guidance:');
|
|
1034
|
+
lines.push(` primary_path: ${report.tuning_guidance.primary_path}`);
|
|
1035
|
+
lines.push(` requires_captured_plan: ${report.tuning_guidance.requires_captured_plan ? 'yes' : 'no'}`);
|
|
1036
|
+
lines.push(` index_branch: ${report.tuning_guidance.index_branch.recommended ? 'recommended' : 'secondary'}`);
|
|
1037
|
+
for (const rationale of report.tuning_guidance.index_branch.rationale) {
|
|
1038
|
+
lines.push(` rationale: ${rationale}`);
|
|
1039
|
+
}
|
|
1040
|
+
for (const step of report.tuning_guidance.index_branch.next_steps) {
|
|
1041
|
+
lines.push(` next: ${step}`);
|
|
1042
|
+
}
|
|
1043
|
+
lines.push(` pipeline_branch: ${report.tuning_guidance.pipeline_branch.recommended ? 'recommended' : 'secondary'}`);
|
|
1044
|
+
for (const rationale of report.tuning_guidance.pipeline_branch.rationale) {
|
|
1045
|
+
lines.push(` rationale: ${rationale}`);
|
|
1046
|
+
}
|
|
1047
|
+
for (const step of report.tuning_guidance.pipeline_branch.next_steps) {
|
|
1048
|
+
lines.push(` next: ${step}`);
|
|
1049
|
+
}
|
|
1050
|
+
lines.push('');
|
|
1051
|
+
}
|
|
1052
|
+
lines.push('Executed statements:');
|
|
1053
|
+
for (const statement of report.executed_statements) {
|
|
1054
|
+
const statementLabel = statement.target ? `${statement.seq}. ${statement.role} (${statement.target})` : `${statement.seq}. ${statement.role}`;
|
|
1055
|
+
lines.push(statementLabel);
|
|
1056
|
+
lines.push(` elapsed_ms: ${statement.elapsed_ms !== undefined ? statement.elapsed_ms.toFixed(2) : '(n/a)'}`);
|
|
1057
|
+
lines.push(` row_count: ${(_c = statement.row_count) !== null && _c !== void 0 ? _c : '(n/a)'}`);
|
|
1058
|
+
if (statement.resolved_sql_preview) {
|
|
1059
|
+
lines.push(` resolved_sql_preview: ${truncateSingleLine(statement.resolved_sql_preview, 120)}`);
|
|
1060
|
+
}
|
|
1061
|
+
lines.push(` sql: ${truncateSingleLine(statement.sql, 120)}`);
|
|
1062
|
+
if (statement.sql_file) {
|
|
1063
|
+
lines.push(` sql_file: ${statement.sql_file}`);
|
|
1064
|
+
}
|
|
1065
|
+
if (statement.resolved_sql_preview_file) {
|
|
1066
|
+
lines.push(` resolved_sql_preview_file: ${statement.resolved_sql_preview_file}`);
|
|
1067
|
+
}
|
|
1068
|
+
if (statement.plan_file) {
|
|
1069
|
+
lines.push(` plan_file: ${statement.plan_file}`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
lines.push('');
|
|
1073
|
+
lines.push('Pipeline analysis:');
|
|
1074
|
+
lines.push(` should_consider_pipeline: ${report.pipeline_analysis.should_consider_pipeline ? 'yes' : 'no'}`);
|
|
1075
|
+
if (report.pipeline_analysis.candidate_ctes.length === 0) {
|
|
1076
|
+
lines.push(' candidate_ctes: (none)');
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
for (const candidate of report.pipeline_analysis.candidate_ctes) {
|
|
1080
|
+
lines.push(` - ${candidate.name}: downstream references=${candidate.downstream_references}`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
lines.push(` scalar_filter_candidates: ${report.pipeline_analysis.scalar_filter_candidates.length > 0 ? report.pipeline_analysis.scalar_filter_candidates.join(', ') : '(none)'}`);
|
|
1084
|
+
if (report.strategy_metadata) {
|
|
1085
|
+
lines.push('');
|
|
1086
|
+
lines.push('Strategy metadata:');
|
|
1087
|
+
lines.push(` materialized_ctes: ${report.strategy_metadata.materialized_ctes.length > 0 ? report.strategy_metadata.materialized_ctes.join(', ') : '(none)'}`);
|
|
1088
|
+
lines.push(` scalar_filter_columns: ${report.strategy_metadata.scalar_filter_columns.length > 0 ? report.strategy_metadata.scalar_filter_columns.join(', ') : '(none)'}`);
|
|
1089
|
+
for (const step of report.strategy_metadata.planned_steps) {
|
|
1090
|
+
lines.push(` - step ${step.step}: ${step.kind} ${step.target} <= ${step.depends_on.length > 0 ? step.depends_on.join(', ') : '(root)'}`);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (report.plan_observations.length > 0) {
|
|
1094
|
+
lines.push('');
|
|
1095
|
+
lines.push('Plan observations:');
|
|
1096
|
+
for (const observation of report.plan_observations) {
|
|
1097
|
+
lines.push(`- ${observation}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (report.recommended_actions.length > 0) {
|
|
1101
|
+
lines.push('');
|
|
1102
|
+
lines.push('Recommended actions:');
|
|
1103
|
+
for (const action of report.recommended_actions) {
|
|
1104
|
+
lines.push(`- [${action.priority}] ${action.action}: ${action.rationale}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (report.evidence_dir) {
|
|
1108
|
+
lines.push('');
|
|
1109
|
+
lines.push(`Evidence: ${report.evidence_dir}`);
|
|
1110
|
+
}
|
|
1111
|
+
return `${lines.join('\n')}\n`;
|
|
1112
|
+
}
|
|
1113
|
+
function formatPerfDiffReport(report, format) {
|
|
1114
|
+
var _a, _b, _c;
|
|
1115
|
+
if (format === 'json') {
|
|
1116
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
1117
|
+
}
|
|
1118
|
+
const statementDeltas = (_a = report.statement_deltas) !== null && _a !== void 0 ? _a : [];
|
|
1119
|
+
const lines = [
|
|
1120
|
+
`Baseline mode: ${report.baseline_mode}`,
|
|
1121
|
+
`Candidate mode: ${report.candidate_mode}`,
|
|
1122
|
+
`Baseline strategy: ${report.baseline_strategy}`,
|
|
1123
|
+
`Candidate strategy: ${report.candidate_strategy}`,
|
|
1124
|
+
`Primary metric: ${report.primary_metric.name}`,
|
|
1125
|
+
`Baseline: ${report.primary_metric.baseline.toFixed(2)}`,
|
|
1126
|
+
`Candidate: ${report.primary_metric.candidate.toFixed(2)}`,
|
|
1127
|
+
`Improvement: ${report.primary_metric.improvement_percent.toFixed(2)}%`,
|
|
1128
|
+
`Statements delta: ${report.statements_delta}`,
|
|
1129
|
+
];
|
|
1130
|
+
if (statementDeltas.some((delta) => delta.elapsed_delta_ms !== undefined || delta.baseline_timed_out !== delta.candidate_timed_out)) {
|
|
1131
|
+
lines.push('');
|
|
1132
|
+
lines.push('Statement deltas:');
|
|
1133
|
+
for (const delta of statementDeltas) {
|
|
1134
|
+
const elapsed = delta.elapsed_delta_ms !== undefined
|
|
1135
|
+
? `${delta.elapsed_delta_ms >= 0 ? '+' : ''}${delta.elapsed_delta_ms.toFixed(2)} ms`
|
|
1136
|
+
: '(n/a)';
|
|
1137
|
+
lines.push(`- ${delta.statement_id}: baseline=${(_b = delta.baseline_elapsed_ms) !== null && _b !== void 0 ? _b : '(n/a)'} candidate=${(_c = delta.candidate_elapsed_ms) !== null && _c !== void 0 ? _c : '(n/a)'} delta=${elapsed}`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (report.plan_deltas.some((delta) => delta.changed)) {
|
|
1141
|
+
lines.push('');
|
|
1142
|
+
lines.push('Plan deltas:');
|
|
1143
|
+
for (const delta of report.plan_deltas.filter((entry) => entry.changed)) {
|
|
1144
|
+
lines.push(`- ${delta.statement_id}: ${delta.baseline_plan} -> ${delta.candidate_plan}`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (report.notes.length > 0) {
|
|
1148
|
+
lines.push('');
|
|
1149
|
+
lines.push('Notes:');
|
|
1150
|
+
for (const note of report.notes) {
|
|
1151
|
+
lines.push(`- ${note}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return `${lines.join('\n')}\n`;
|
|
1155
|
+
}
|
|
1156
|
+
function buildPerfPipelineAnalysis(sqlFile) {
|
|
1157
|
+
var _a, _b;
|
|
1158
|
+
const structure = (0, structure_1.buildQueryStructureReport)(sqlFile, 'ztd perf run');
|
|
1159
|
+
const referenceCounts = new Map(structure.ctes.map((cte) => [cte.name, 0]));
|
|
1160
|
+
for (const cte of structure.ctes) {
|
|
1161
|
+
for (const dependency of cte.depends_on) {
|
|
1162
|
+
referenceCounts.set(dependency, ((_a = referenceCounts.get(dependency)) !== null && _a !== void 0 ? _a : 0) + 1);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
for (const root of normalizeFinalQueryRoots(structure.final_query)) {
|
|
1166
|
+
referenceCounts.set(root, ((_b = referenceCounts.get(root)) !== null && _b !== void 0 ? _b : 0) + 1);
|
|
1167
|
+
}
|
|
1168
|
+
const scalarFilterCandidates = (0, scalarFilterAnalysis_1.findScalarFilterCandidates)(sqlFile);
|
|
1169
|
+
const candidateCtes = structure.ctes
|
|
1170
|
+
.map((cte) => {
|
|
1171
|
+
var _a;
|
|
1172
|
+
const downstreamReferences = (_a = referenceCounts.get(cte.name)) !== null && _a !== void 0 ? _a : 0;
|
|
1173
|
+
const reasons = [];
|
|
1174
|
+
if (downstreamReferences >= 2) {
|
|
1175
|
+
reasons.push('referenced by multiple downstream consumers');
|
|
1176
|
+
}
|
|
1177
|
+
if (!cte.unused && cte.depends_on.length >= 2) {
|
|
1178
|
+
reasons.push('merges multiple upstream dependencies');
|
|
1179
|
+
}
|
|
1180
|
+
return {
|
|
1181
|
+
name: cte.name,
|
|
1182
|
+
downstream_references: downstreamReferences,
|
|
1183
|
+
reasons
|
|
1184
|
+
};
|
|
1185
|
+
})
|
|
1186
|
+
.filter((candidate) => candidate.reasons.length > 0);
|
|
1187
|
+
const notes = [
|
|
1188
|
+
'Pipeline candidacy is heuristic in this MVP and does not yet benchmark SqlSpec runtime materialization directly.'
|
|
1189
|
+
];
|
|
1190
|
+
if (scalarFilterCandidates.length > 0) {
|
|
1191
|
+
notes.push(`Optimizer-sensitive scalar predicates detected on columns: ${scalarFilterCandidates.join(', ')}`);
|
|
1192
|
+
}
|
|
1193
|
+
if (structure.unused_ctes.length > 0) {
|
|
1194
|
+
notes.push(`Unused CTEs detected: ${structure.unused_ctes.join(', ')}`);
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
query_type: structure.query_type,
|
|
1198
|
+
cte_count: structure.cte_count,
|
|
1199
|
+
should_consider_pipeline: candidateCtes.length > 0 || scalarFilterCandidates.length > 0,
|
|
1200
|
+
candidate_ctes: candidateCtes,
|
|
1201
|
+
scalar_filter_candidates: scalarFilterCandidates,
|
|
1202
|
+
notes
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Load a saved benchmark report from summary.json.
|
|
1207
|
+
*/
|
|
1208
|
+
function summarizePerfDdlInventory(inventory) {
|
|
1209
|
+
return {
|
|
1210
|
+
ddl_files: inventory.files.length,
|
|
1211
|
+
ddl_statement_count: inventory.ddlStatementCount,
|
|
1212
|
+
table_count: inventory.tableCount,
|
|
1213
|
+
index_count: inventory.indexCount,
|
|
1214
|
+
index_names: [...inventory.indexNames]
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function buildPerfTuningGuidance(pipelineAnalysis, planFacts, specGuidance) {
|
|
1218
|
+
const indexRationale = [];
|
|
1219
|
+
const indexNextSteps = [
|
|
1220
|
+
'Capture or review EXPLAIN (ANALYZE, BUFFERS) before changing the physical design.',
|
|
1221
|
+
'Append CREATE INDEX statements to ztd/ddl/*.sql instead of making ad-hoc sandbox-only changes.',
|
|
1222
|
+
'Run `ztd perf db reset` so the perf sandbox recreates both tables and indexes from local DDL.'
|
|
1223
|
+
];
|
|
1224
|
+
const pipelineRationale = [];
|
|
1225
|
+
const pipelineNextSteps = [
|
|
1226
|
+
'Review candidate_ctes and scalar_filter_candidates before rewriting the query.',
|
|
1227
|
+
'Use PIPELINE decomposition when the same intermediate result is reused or scalar filters are optimizer-sensitive.',
|
|
1228
|
+
'After SQL changes, rerun `ztd perf db reset` and `ztd perf seed` so the benchmark uses the intended physical schema.'
|
|
1229
|
+
];
|
|
1230
|
+
const hasPipelineSignals = pipelineAnalysis.should_consider_pipeline
|
|
1231
|
+
|| pipelineAnalysis.candidate_ctes.length > 0
|
|
1232
|
+
|| pipelineAnalysis.scalar_filter_candidates.length > 0;
|
|
1233
|
+
const hasPlanSignals = planFacts.hasSequentialScan || planFacts.hasJoin;
|
|
1234
|
+
if (pipelineAnalysis.candidate_ctes.length > 0) {
|
|
1235
|
+
pipelineRationale.push('Reusable intermediate stages detected: ' + pipelineAnalysis.candidate_ctes.map((candidate) => candidate.name).join(', ') + '.');
|
|
1236
|
+
}
|
|
1237
|
+
if (pipelineAnalysis.scalar_filter_candidates.length > 0) {
|
|
1238
|
+
pipelineRationale.push('Optimizer-sensitive scalar predicates detected: ' + pipelineAnalysis.scalar_filter_candidates.join(', ') + '.');
|
|
1239
|
+
}
|
|
1240
|
+
if (planFacts.hasSequentialScan) {
|
|
1241
|
+
indexRationale.push('Captured plan shows a sequential scan, so index coverage is the first physical-design branch to review.');
|
|
1242
|
+
}
|
|
1243
|
+
if (planFacts.hasJoin) {
|
|
1244
|
+
indexRationale.push('Captured plan shows join work, so supporting indexes or join-order changes may matter.');
|
|
1245
|
+
}
|
|
1246
|
+
if ((specGuidance === null || specGuidance === void 0 ? void 0 : specGuidance.review_policy) === 'strongly-recommended') {
|
|
1247
|
+
indexRationale.push('QuerySpec marks this query as performance-sensitive, so preserve physical design changes in local DDL before benchmarking.');
|
|
1248
|
+
}
|
|
1249
|
+
let primaryPath = 'capture-plan';
|
|
1250
|
+
if (hasPipelineSignals) {
|
|
1251
|
+
primaryPath = 'pipeline';
|
|
1252
|
+
}
|
|
1253
|
+
if (hasPlanSignals && !hasPipelineSignals) {
|
|
1254
|
+
primaryPath = 'index';
|
|
1255
|
+
}
|
|
1256
|
+
if (!planFacts.hasCapturedPlan && indexRationale.length === 0) {
|
|
1257
|
+
indexRationale.push('No captured plan is available yet, so confirm whether scans or joins are the real bottleneck before adding indexes.');
|
|
1258
|
+
}
|
|
1259
|
+
if (!planFacts.hasCapturedPlan && pipelineRationale.length === 0) {
|
|
1260
|
+
pipelineRationale.push('No pipeline-specific signal is available yet, so start by capturing a representative plan and row counts.');
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
primary_path: primaryPath,
|
|
1264
|
+
requires_captured_plan: !planFacts.hasCapturedPlan,
|
|
1265
|
+
index_branch: {
|
|
1266
|
+
recommended: primaryPath === 'index',
|
|
1267
|
+
rationale: indexRationale,
|
|
1268
|
+
next_steps: indexNextSteps
|
|
1269
|
+
},
|
|
1270
|
+
pipeline_branch: {
|
|
1271
|
+
recommended: primaryPath === 'pipeline',
|
|
1272
|
+
rationale: pipelineRationale,
|
|
1273
|
+
next_steps: pipelineNextSteps
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
function buildPerfTuningSummary(guidance) {
|
|
1278
|
+
var _a, _b, _c, _d;
|
|
1279
|
+
if (guidance.primary_path === 'index') {
|
|
1280
|
+
return {
|
|
1281
|
+
headline: 'Start with index tuning.',
|
|
1282
|
+
evidence: guidance.index_branch.rationale.slice(0, 2),
|
|
1283
|
+
next_step: (_a = guidance.index_branch.next_steps[0]) !== null && _a !== void 0 ? _a : 'Review the captured plan before changing indexes.'
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
if (guidance.primary_path === 'pipeline') {
|
|
1287
|
+
return {
|
|
1288
|
+
headline: 'Start with pipeline tuning.',
|
|
1289
|
+
evidence: guidance.pipeline_branch.rationale.slice(0, 2),
|
|
1290
|
+
next_step: (_b = guidance.pipeline_branch.next_steps[0]) !== null && _b !== void 0 ? _b : 'Review candidate_ctes before rewriting the query.'
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
return guidance.requires_captured_plan
|
|
1294
|
+
? {
|
|
1295
|
+
headline: 'Capture a representative plan before choosing index or pipeline work.',
|
|
1296
|
+
evidence: [
|
|
1297
|
+
'No captured plan signal is available yet.',
|
|
1298
|
+
...guidance.index_branch.rationale.slice(0, 1)
|
|
1299
|
+
],
|
|
1300
|
+
next_step: (_c = guidance.index_branch.next_steps[0]) !== null && _c !== void 0 ? _c : 'Capture EXPLAIN (ANALYZE, BUFFERS) output first.'
|
|
1301
|
+
}
|
|
1302
|
+
: {
|
|
1303
|
+
headline: 'A representative plan is already available; compare index and pipeline evidence next.',
|
|
1304
|
+
evidence: guidance.index_branch.rationale.length > 0
|
|
1305
|
+
? guidance.index_branch.rationale.slice(0, 2)
|
|
1306
|
+
: ['A captured plan exists, but it does not yet isolate scans, joins, or pipeline hotspots.'],
|
|
1307
|
+
next_step: (_d = guidance.index_branch.next_steps[0]) !== null && _d !== void 0 ? _d : 'Review the captured plan before changing indexes.'
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function buildPerfRecommendedActions(selectedMode, completed, pipelineAnalysis, planFacts, specGuidance) {
|
|
1311
|
+
var _a;
|
|
1312
|
+
const actions = [];
|
|
1313
|
+
if (selectedMode === 'completion' && !completed) {
|
|
1314
|
+
actions.push({
|
|
1315
|
+
action: 'stabilize-completion-run',
|
|
1316
|
+
priority: 'high',
|
|
1317
|
+
rationale: 'The benchmark timed out in completion mode, so the next loop should focus on finishing the query before comparing latency percentiles.'
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
if (pipelineAnalysis.candidate_ctes.length > 0) {
|
|
1321
|
+
actions.push({
|
|
1322
|
+
action: 'consider-pipeline-materialization',
|
|
1323
|
+
priority: 'medium',
|
|
1324
|
+
rationale: `Pipeline candidates detected: ${pipelineAnalysis.candidate_ctes.map((candidate) => candidate.name).join(', ')}.`
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
if (pipelineAnalysis.scalar_filter_candidates.length > 0) {
|
|
1328
|
+
actions.push({
|
|
1329
|
+
action: 'consider-scalar-filter-binding',
|
|
1330
|
+
priority: 'medium',
|
|
1331
|
+
rationale: `Scalar filter candidates detected: ${pipelineAnalysis.scalar_filter_candidates.join(', ')}.`
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
if (planFacts.hasSequentialScan) {
|
|
1335
|
+
actions.push({
|
|
1336
|
+
action: 'review-index-coverage',
|
|
1337
|
+
priority: 'medium',
|
|
1338
|
+
rationale: 'The captured plan includes a sequential scan, so index coverage is a likely tuning branch.'
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
if (planFacts.hasJoin) {
|
|
1342
|
+
actions.push({
|
|
1343
|
+
action: 'inspect-join-strategy',
|
|
1344
|
+
priority: 'medium',
|
|
1345
|
+
rationale: 'The captured plan includes a join operator, so rewriting join shape or supporting it with indexes may help.'
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
if ((specGuidance === null || specGuidance === void 0 ? void 0 : specGuidance.evidence_status) === 'missing') {
|
|
1349
|
+
actions.push({
|
|
1350
|
+
action: 'capture-perf-evidence',
|
|
1351
|
+
priority: 'high',
|
|
1352
|
+
rationale: `QuerySpec guidance marks this query as ${(_a = specGuidance.expected_scale) !== null && _a !== void 0 ? _a : 'performance-sensitive'}, so save benchmark evidence for maintenance review.`
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
if ((specGuidance === null || specGuidance === void 0 ? void 0 : specGuidance.fixture_rows_status) === 'undersized' && specGuidance.expected_input_rows !== undefined) {
|
|
1356
|
+
actions.push({
|
|
1357
|
+
action: 'increase-perf-fixture-scale',
|
|
1358
|
+
priority: specGuidance.review_policy === 'strongly-recommended' ? 'high' : 'medium',
|
|
1359
|
+
rationale: `perf/seed.yml currently provisions ${specGuidance.fixture_rows_available} rows, below the expected input scale of ${specGuidance.expected_input_rows}.`
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
return uniqueRecommendedActions(actions);
|
|
1363
|
+
}
|
|
1364
|
+
function uniqueRecommendedActions(actions) {
|
|
1365
|
+
const deduped = new Map();
|
|
1366
|
+
for (const action of actions) {
|
|
1367
|
+
deduped.set(action.action, action);
|
|
1368
|
+
}
|
|
1369
|
+
return Array.from(deduped.values());
|
|
1370
|
+
}
|
|
1371
|
+
function buildPerfPlanFacts(planJson) {
|
|
1372
|
+
const observations = [];
|
|
1373
|
+
const statementSummaryParts = [];
|
|
1374
|
+
const hasCapturedPlan = planJson !== null && planJson !== undefined;
|
|
1375
|
+
let hasSequentialScan = false;
|
|
1376
|
+
let hasJoin = false;
|
|
1377
|
+
walkPlanNodes(planJson, (node) => {
|
|
1378
|
+
var _a;
|
|
1379
|
+
const nodeType = normalizeString(node['Node Type']);
|
|
1380
|
+
const relationName = normalizeString(node['Relation Name']);
|
|
1381
|
+
const joinType = normalizeString(node['Join Type']);
|
|
1382
|
+
const cteName = normalizeString(node['CTE Name']);
|
|
1383
|
+
const filter = normalizeString((_a = node.Filter) !== null && _a !== void 0 ? _a : node['Index Cond']);
|
|
1384
|
+
if (!nodeType) {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
statementSummaryParts.push(joinType ? `${joinType} ${nodeType}` : nodeType);
|
|
1388
|
+
if (nodeType === 'Seq Scan' && relationName) {
|
|
1389
|
+
hasSequentialScan = true;
|
|
1390
|
+
observations.push(filter
|
|
1391
|
+
? `Seq Scan on ${relationName} with filter ${truncateSingleLine(filter, 90)}`
|
|
1392
|
+
: `Seq Scan on ${relationName}`);
|
|
1393
|
+
}
|
|
1394
|
+
if (nodeType === 'Nested Loop' || nodeType.includes('Join') || Boolean(joinType)) {
|
|
1395
|
+
hasJoin = true;
|
|
1396
|
+
}
|
|
1397
|
+
if (joinType) {
|
|
1398
|
+
observations.push(`${joinType} ${nodeType} present in the captured plan`);
|
|
1399
|
+
}
|
|
1400
|
+
if (nodeType === 'CTE Scan' && cteName) {
|
|
1401
|
+
observations.push(`CTE Scan reads ${cteName}`);
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
return {
|
|
1405
|
+
observations: Array.from(new Set(observations)),
|
|
1406
|
+
statement_summary: Array.from(new Set(statementSummaryParts)).join(' -> ') || '(no plan captured)',
|
|
1407
|
+
hasCapturedPlan,
|
|
1408
|
+
hasSequentialScan,
|
|
1409
|
+
hasJoin
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
function buildPerfPlanDeltas(baseline, candidate) {
|
|
1413
|
+
const keys = collectPerfStatementDiffKeys(baseline.executed_statements, candidate.executed_statements);
|
|
1414
|
+
const baselineLookup = buildPerfStatementDiffLookup(baseline.executed_statements);
|
|
1415
|
+
const candidateLookup = buildPerfStatementDiffLookup(candidate.executed_statements);
|
|
1416
|
+
const deltas = [];
|
|
1417
|
+
for (const key of keys) {
|
|
1418
|
+
const baselineStatement = baselineLookup.get(key);
|
|
1419
|
+
const candidateStatement = candidateLookup.get(key);
|
|
1420
|
+
const statementId = formatPlanDeltaStatementId(candidateStatement !== null && candidateStatement !== void 0 ? candidateStatement : baselineStatement, key);
|
|
1421
|
+
const baselinePlan = summarizeStatementPlan(baselineStatement, baseline.plan_observations, true);
|
|
1422
|
+
const candidatePlan = summarizeStatementPlan(candidateStatement, candidate.plan_observations, true);
|
|
1423
|
+
deltas.push({
|
|
1424
|
+
statement_id: statementId,
|
|
1425
|
+
baseline_plan: baselinePlan,
|
|
1426
|
+
candidate_plan: candidatePlan,
|
|
1427
|
+
changed: baselinePlan !== candidatePlan
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
return deltas;
|
|
1431
|
+
}
|
|
1432
|
+
function buildPerfStatementDeltas(baseline, candidate) {
|
|
1433
|
+
var _a;
|
|
1434
|
+
const keys = collectPerfStatementDiffKeys(baseline.executed_statements, candidate.executed_statements);
|
|
1435
|
+
const baselineLookup = buildPerfStatementDiffLookup(baseline.executed_statements);
|
|
1436
|
+
const candidateLookup = buildPerfStatementDiffLookup(candidate.executed_statements);
|
|
1437
|
+
const deltas = [];
|
|
1438
|
+
for (const key of keys) {
|
|
1439
|
+
const baselineStatement = baselineLookup.get(key);
|
|
1440
|
+
const candidateStatement = candidateLookup.get(key);
|
|
1441
|
+
const statement = candidateStatement !== null && candidateStatement !== void 0 ? candidateStatement : baselineStatement;
|
|
1442
|
+
deltas.push({
|
|
1443
|
+
statement_id: formatPlanDeltaStatementId(statement, key),
|
|
1444
|
+
role: (_a = statement === null || statement === void 0 ? void 0 : statement.role) !== null && _a !== void 0 ? _a : 'final-query',
|
|
1445
|
+
baseline_elapsed_ms: baselineStatement === null || baselineStatement === void 0 ? void 0 : baselineStatement.elapsed_ms,
|
|
1446
|
+
candidate_elapsed_ms: candidateStatement === null || candidateStatement === void 0 ? void 0 : candidateStatement.elapsed_ms,
|
|
1447
|
+
elapsed_delta_ms: (baselineStatement === null || baselineStatement === void 0 ? void 0 : baselineStatement.elapsed_ms) !== undefined && (candidateStatement === null || candidateStatement === void 0 ? void 0 : candidateStatement.elapsed_ms) !== undefined
|
|
1448
|
+
? candidateStatement.elapsed_ms - baselineStatement.elapsed_ms
|
|
1449
|
+
: undefined,
|
|
1450
|
+
baseline_row_count: baselineStatement === null || baselineStatement === void 0 ? void 0 : baselineStatement.row_count,
|
|
1451
|
+
candidate_row_count: candidateStatement === null || candidateStatement === void 0 ? void 0 : candidateStatement.row_count,
|
|
1452
|
+
baseline_timed_out: baselineStatement === null || baselineStatement === void 0 ? void 0 : baselineStatement.timed_out,
|
|
1453
|
+
candidate_timed_out: candidateStatement === null || candidateStatement === void 0 ? void 0 : candidateStatement.timed_out
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
return deltas;
|
|
1457
|
+
}
|
|
1458
|
+
function buildPerfStatementDiffLookup(statements) {
|
|
1459
|
+
return new Map(buildPerfStatementDiffEntries(statements).map((entry) => [entry.key, entry.statement]));
|
|
1460
|
+
}
|
|
1461
|
+
function collectPerfStatementDiffKeys(baselineStatements, candidateStatements) {
|
|
1462
|
+
const keys = [];
|
|
1463
|
+
const seen = new Set();
|
|
1464
|
+
for (const entry of [...buildPerfStatementDiffEntries(baselineStatements), ...buildPerfStatementDiffEntries(candidateStatements)]) {
|
|
1465
|
+
if (seen.has(entry.key)) {
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
seen.add(entry.key);
|
|
1469
|
+
keys.push(entry.key);
|
|
1470
|
+
}
|
|
1471
|
+
return keys;
|
|
1472
|
+
}
|
|
1473
|
+
function buildPerfStatementDiffEntries(statements) {
|
|
1474
|
+
const counts = new Map();
|
|
1475
|
+
return statements.map((statement) => {
|
|
1476
|
+
var _a;
|
|
1477
|
+
const baseKey = statement.target ? `${statement.role}:${statement.target}` : `${statement.role}:statement`;
|
|
1478
|
+
const nextCount = ((_a = counts.get(baseKey)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
1479
|
+
counts.set(baseKey, nextCount);
|
|
1480
|
+
return {
|
|
1481
|
+
key: nextCount === 1 ? baseKey : `${baseKey}#${nextCount}`,
|
|
1482
|
+
statement
|
|
1483
|
+
};
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
function formatPlanDeltaStatementId(statement, fallbackKey) {
|
|
1487
|
+
if (!statement) {
|
|
1488
|
+
return fallbackKey;
|
|
1489
|
+
}
|
|
1490
|
+
return statement.target ? `${statement.seq}:${statement.role}:${statement.target}` : `${statement.seq}:${statement.role}`;
|
|
1491
|
+
}
|
|
1492
|
+
function summarizeStatementPlan(statement, planObservations, includeObservations) {
|
|
1493
|
+
var _a;
|
|
1494
|
+
if (!statement) {
|
|
1495
|
+
return '(missing statement)';
|
|
1496
|
+
}
|
|
1497
|
+
const parts = [];
|
|
1498
|
+
const summary = statement.plan_summary;
|
|
1499
|
+
if ((summary === null || summary === void 0 ? void 0 : summary.join_type) && summary.node_type) {
|
|
1500
|
+
parts.push(`${summary.join_type} ${summary.node_type}`);
|
|
1501
|
+
}
|
|
1502
|
+
else if (summary === null || summary === void 0 ? void 0 : summary.node_type) {
|
|
1503
|
+
parts.push(summary.node_type);
|
|
1504
|
+
}
|
|
1505
|
+
if (includeObservations && planObservations.length > 0) {
|
|
1506
|
+
const prefix = `${statement.role}(${(_a = statement.target) !== null && _a !== void 0 ? _a : 'statement'})`;
|
|
1507
|
+
const relevantObservations = planObservations.filter((observation) => observation.startsWith(prefix));
|
|
1508
|
+
if (relevantObservations.length > 0) {
|
|
1509
|
+
parts.push(relevantObservations.join(' | '));
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return parts.join(' :: ') || '(no plan captured)';
|
|
1513
|
+
}
|
|
1514
|
+
function walkPlanNodes(planJson, visit) {
|
|
1515
|
+
if (!Array.isArray(planJson)) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
for (const entry of planJson) {
|
|
1519
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
const plan = entry.Plan;
|
|
1523
|
+
if (typeof plan === 'object' && plan !== null) {
|
|
1524
|
+
walkSinglePlanNode(plan, visit);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function walkSinglePlanNode(node, visit) {
|
|
1529
|
+
visit(node);
|
|
1530
|
+
const plans = node.Plans;
|
|
1531
|
+
if (!Array.isArray(plans)) {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
for (const child of plans) {
|
|
1535
|
+
if (typeof child === 'object' && child !== null) {
|
|
1536
|
+
walkSinglePlanNode(child, visit);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function renderResolvedSqlPreview(sql, bindings) {
|
|
1541
|
+
if (!Array.isArray(bindings) || bindings.length === 0) {
|
|
1542
|
+
return undefined;
|
|
1543
|
+
}
|
|
1544
|
+
return sql.replace(/\$(\d+)/g, (token, rawIndex) => {
|
|
1545
|
+
const binding = bindings[Number(rawIndex) - 1];
|
|
1546
|
+
return binding === undefined ? token : renderSqlLiteral(binding);
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
function renderSqlLiteral(value) {
|
|
1550
|
+
if (value === null) {
|
|
1551
|
+
return 'null';
|
|
1552
|
+
}
|
|
1553
|
+
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
1554
|
+
return String(value);
|
|
1555
|
+
}
|
|
1556
|
+
if (typeof value === 'boolean') {
|
|
1557
|
+
return value ? 'true' : 'false';
|
|
1558
|
+
}
|
|
1559
|
+
if (value instanceof Date) {
|
|
1560
|
+
return `'${value.toISOString()}'`;
|
|
1561
|
+
}
|
|
1562
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1563
|
+
}
|
|
1564
|
+
function loadPerfBenchmarkReport(evidenceDir) {
|
|
1565
|
+
const summaryFile = node_path_1.default.join(node_path_1.default.resolve(evidenceDir), 'summary.json');
|
|
1566
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(summaryFile, 'utf8'));
|
|
1567
|
+
return validatePerfBenchmarkReport(summaryFile, parsed);
|
|
1568
|
+
}
|
|
1569
|
+
exports.PERF_BENCHMARK_DEFAULTS = {
|
|
1570
|
+
repeat: DEFAULT_REPEAT,
|
|
1571
|
+
warmup: DEFAULT_WARMUP,
|
|
1572
|
+
classifyThresholdSeconds: DEFAULT_CLASSIFY_THRESHOLD_SECONDS,
|
|
1573
|
+
timeoutMinutes: DEFAULT_TIMEOUT_MINUTES
|
|
1574
|
+
};
|
|
1575
|
+
function prepareBenchmarkQuery(rootDir, queryFile, paramsFile) {
|
|
1576
|
+
const absolutePath = node_path_1.default.resolve(rootDir, queryFile);
|
|
1577
|
+
const sourceSql = (0, node_fs_1.readFileSync)(absolutePath, 'utf8');
|
|
1578
|
+
const structure = (0, structure_1.buildQueryStructureReport)(absolutePath, 'ztd perf run');
|
|
1579
|
+
if (structure.query_type !== 'SELECT') {
|
|
1580
|
+
throw new Error('ztd perf run currently supports SELECT queries only.');
|
|
1581
|
+
}
|
|
1582
|
+
const scan = (0, modelGenScanner_1.scanModelGenSql)(sourceSql);
|
|
1583
|
+
const rawBindings = paramsFile ? loadPerfBindings(rootDir, paramsFile) : undefined;
|
|
1584
|
+
if (scan.mode === 'named') {
|
|
1585
|
+
if (!rawBindings || typeof rawBindings !== 'object' || Array.isArray(rawBindings)) {
|
|
1586
|
+
throw new Error('Named SQL placeholders require an object in --params.');
|
|
1587
|
+
}
|
|
1588
|
+
const namedBindings = rawBindings;
|
|
1589
|
+
const bound = (0, modelGenBinder_1.bindModelGenNamedSql)(sourceSql);
|
|
1590
|
+
const orderedValues = bound.orderedParamNames.map((name) => {
|
|
1591
|
+
if (!(name in namedBindings)) {
|
|
1592
|
+
throw new Error(`Missing named benchmark param: ${name}`);
|
|
1593
|
+
}
|
|
1594
|
+
return namedBindings[name];
|
|
1595
|
+
});
|
|
1596
|
+
return {
|
|
1597
|
+
absolutePath,
|
|
1598
|
+
sourceSql,
|
|
1599
|
+
boundSql: bound.boundSql,
|
|
1600
|
+
queryType: 'SELECT',
|
|
1601
|
+
paramsShape: scan.mode,
|
|
1602
|
+
orderedParamNames: bound.orderedParamNames,
|
|
1603
|
+
bindings: orderedValues,
|
|
1604
|
+
runtimeBindings: namedBindings
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
if (scan.mode === 'positional') {
|
|
1608
|
+
if (!Array.isArray(rawBindings)) {
|
|
1609
|
+
throw new Error('Positional SQL placeholders require an array in --params.');
|
|
1610
|
+
}
|
|
1611
|
+
const orderedParamNames = scan.positionalTokens.map((token) => token.token);
|
|
1612
|
+
const highestRequiredIndex = orderedParamNames.reduce((max, token) => {
|
|
1613
|
+
const parsed = Number(token.slice(1));
|
|
1614
|
+
return Number.isInteger(parsed) ? Math.max(max, parsed) : max;
|
|
1615
|
+
}, 0);
|
|
1616
|
+
if (rawBindings.length < highestRequiredIndex) {
|
|
1617
|
+
throw new Error(`Positional SQL placeholders require at least ${highestRequiredIndex} parameters for $${highestRequiredIndex}.`);
|
|
1618
|
+
}
|
|
1619
|
+
return {
|
|
1620
|
+
absolutePath,
|
|
1621
|
+
sourceSql,
|
|
1622
|
+
boundSql: sourceSql,
|
|
1623
|
+
queryType: 'SELECT',
|
|
1624
|
+
paramsShape: scan.mode,
|
|
1625
|
+
orderedParamNames,
|
|
1626
|
+
bindings: rawBindings,
|
|
1627
|
+
runtimeBindings: rawBindings
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
if (rawBindings !== undefined) {
|
|
1631
|
+
throw new Error('This SQL file has no placeholders, so --params is not needed.');
|
|
1632
|
+
}
|
|
1633
|
+
return {
|
|
1634
|
+
absolutePath,
|
|
1635
|
+
sourceSql,
|
|
1636
|
+
boundSql: sourceSql,
|
|
1637
|
+
queryType: 'SELECT',
|
|
1638
|
+
paramsShape: 'none',
|
|
1639
|
+
orderedParamNames: [],
|
|
1640
|
+
bindings: undefined,
|
|
1641
|
+
runtimeBindings: undefined
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
function loadPerfBindings(rootDir, paramsFile) {
|
|
1645
|
+
const absolutePath = node_path_1.default.resolve(rootDir, paramsFile);
|
|
1646
|
+
const rawContents = (0, node_fs_1.readFileSync)(absolutePath, 'utf8');
|
|
1647
|
+
try {
|
|
1648
|
+
if (node_path_1.default.extname(absolutePath).toLowerCase() === '.json') {
|
|
1649
|
+
return JSON.parse(rawContents);
|
|
1650
|
+
}
|
|
1651
|
+
const parsed = (0, yaml_1.parse)(rawContents);
|
|
1652
|
+
if (isPerfParamsEnvelope(parsed)) {
|
|
1653
|
+
return parsed.params;
|
|
1654
|
+
}
|
|
1655
|
+
return parsed !== null && parsed !== void 0 ? parsed : {};
|
|
1656
|
+
}
|
|
1657
|
+
catch (error) {
|
|
1658
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1659
|
+
throw new Error(`Failed to parse perf params file ${absolutePath}: ${message}`);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
function isPerfParamsEnvelope(value) {
|
|
1663
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && 'params' in value;
|
|
1664
|
+
}
|
|
1665
|
+
async function fetchPerfDatabaseVersion(rootDir) {
|
|
1666
|
+
var _a, _b;
|
|
1667
|
+
const pg = await (0, optionalDependencies_1.ensurePgModule)();
|
|
1668
|
+
const sandboxConfig = (0, sandbox_1.loadPerfSandboxConfig)(rootDir);
|
|
1669
|
+
const resolvedConnection = await (0, sandbox_1.ensurePerfConnection)(rootDir, sandboxConfig);
|
|
1670
|
+
const client = new pg.Client({
|
|
1671
|
+
connectionString: resolvedConnection.connectionUrl,
|
|
1672
|
+
connectionTimeoutMillis: 3000
|
|
1673
|
+
});
|
|
1674
|
+
try {
|
|
1675
|
+
await client.connect();
|
|
1676
|
+
const result = await client.query('SHOW server_version');
|
|
1677
|
+
return (_b = (_a = result.rows) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.server_version;
|
|
1678
|
+
}
|
|
1679
|
+
finally {
|
|
1680
|
+
await client.end().catch(() => undefined);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
async function classifyPerfBenchmarkMode(rootDir, prepared, strategy, material, classifyThresholdMs) {
|
|
1684
|
+
const probe = await executePerfBenchmarkOnce(rootDir, prepared, strategy, material, classifyThresholdMs, true);
|
|
1685
|
+
if (probe.timedOut || probe.elapsedMs >= classifyThresholdMs) {
|
|
1686
|
+
return {
|
|
1687
|
+
selectedMode: 'completion',
|
|
1688
|
+
reason: `classification probe exceeded ${classifyThresholdMs} ms`,
|
|
1689
|
+
probe
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
return {
|
|
1693
|
+
selectedMode: 'latency',
|
|
1694
|
+
reason: `classification probe completed within ${classifyThresholdMs} ms`,
|
|
1695
|
+
probe
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function buildLatencyMetrics(runs, warmupRuns) {
|
|
1699
|
+
var _a, _b;
|
|
1700
|
+
const sorted = [...runs].sort((left, right) => left - right);
|
|
1701
|
+
return {
|
|
1702
|
+
measured_runs: runs.length,
|
|
1703
|
+
warmup_runs: warmupRuns,
|
|
1704
|
+
min_ms: (_a = sorted[0]) !== null && _a !== void 0 ? _a : 0,
|
|
1705
|
+
max_ms: (_b = sorted[sorted.length - 1]) !== null && _b !== void 0 ? _b : 0,
|
|
1706
|
+
avg_ms: runs.reduce((sum, value) => sum + value, 0) / Math.max(runs.length, 1),
|
|
1707
|
+
median_ms: percentile(sorted, 0.5),
|
|
1708
|
+
p95_ms: percentile(sorted, 0.95)
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
function percentile(sorted, ratio) {
|
|
1712
|
+
var _a;
|
|
1713
|
+
if (sorted.length === 0) {
|
|
1714
|
+
return 0;
|
|
1715
|
+
}
|
|
1716
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1));
|
|
1717
|
+
return (_a = sorted[index]) !== null && _a !== void 0 ? _a : 0;
|
|
1718
|
+
}
|
|
1719
|
+
function summarizePlanJson(planJson) {
|
|
1720
|
+
var _a;
|
|
1721
|
+
const topLevel = Array.isArray(planJson) ? planJson[0] : null;
|
|
1722
|
+
if (!topLevel || typeof topLevel !== 'object') {
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
const record = topLevel;
|
|
1726
|
+
const plan = ((_a = record.Plan) !== null && _a !== void 0 ? _a : null);
|
|
1727
|
+
if (!plan) {
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
return {
|
|
1731
|
+
node_type: normalizeString(plan['Node Type']),
|
|
1732
|
+
join_type: normalizeString(plan['Join Type']),
|
|
1733
|
+
total_cost: normalizeNumber(plan['Total Cost']),
|
|
1734
|
+
plan_rows: normalizeNumber(plan['Plan Rows']),
|
|
1735
|
+
actual_rows: normalizeNumber(plan['Actual Rows']),
|
|
1736
|
+
actual_total_time: normalizeNumber(plan['Actual Total Time'])
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
function savePerfBenchmarkEvidence(rootDir, report, statements) {
|
|
1740
|
+
const evidenceRoot = node_path_1.default.join(rootDir, 'perf', 'evidence');
|
|
1741
|
+
(0, node_fs_1.mkdirSync)(evidenceRoot, { recursive: true });
|
|
1742
|
+
const reserved = reservePerfEvidenceDir(evidenceRoot, report.label);
|
|
1743
|
+
const plansDir = node_path_1.default.join(reserved.evidenceDir, 'plans');
|
|
1744
|
+
const sqlDir = node_path_1.default.join(reserved.evidenceDir, 'executed-sql');
|
|
1745
|
+
(0, node_fs_1.mkdirSync)(plansDir, { recursive: true });
|
|
1746
|
+
(0, node_fs_1.mkdirSync)(sqlDir, { recursive: true });
|
|
1747
|
+
(0, node_fs_1.copyFileSync)(report.source_sql_file, node_path_1.default.join(reserved.evidenceDir, 'source.sql'));
|
|
1748
|
+
if (report.params_file && (0, node_fs_1.existsSync)(report.params_file)) {
|
|
1749
|
+
(0, node_fs_1.copyFileSync)(report.params_file, node_path_1.default.join(reserved.evidenceDir, node_path_1.default.basename(report.params_file)));
|
|
1750
|
+
}
|
|
1751
|
+
const planFiles = [];
|
|
1752
|
+
const sqlFiles = [];
|
|
1753
|
+
const resolvedSqlPreviewFiles = [];
|
|
1754
|
+
for (const [index, statement] of report.executed_statements.entries()) {
|
|
1755
|
+
const trace = statements[index];
|
|
1756
|
+
const targetSuffix = statement.target ? `-${sanitizeLabel(statement.target)}` : '';
|
|
1757
|
+
const baseName = `${String(statement.seq).padStart(3, '0')}-${statement.role}${targetSuffix}`;
|
|
1758
|
+
const sqlFileName = `${baseName}.bound.sql`;
|
|
1759
|
+
const relativeSqlPath = node_path_1.default.join('executed-sql', sqlFileName).replace(/\\/g, '/');
|
|
1760
|
+
(0, node_fs_1.writeFileSync)(node_path_1.default.join(sqlDir, sqlFileName), `${statement.sql.trimEnd()}\n`, 'utf8');
|
|
1761
|
+
sqlFiles.push(relativeSqlPath);
|
|
1762
|
+
if (statement.resolved_sql_preview) {
|
|
1763
|
+
const resolvedFileName = `${baseName}.resolved-preview.sql`;
|
|
1764
|
+
const relativeResolvedPath = node_path_1.default.join('executed-sql', resolvedFileName).replace(/\\/g, '/');
|
|
1765
|
+
(0, node_fs_1.writeFileSync)(node_path_1.default.join(sqlDir, resolvedFileName), `${statement.resolved_sql_preview.trimEnd()}\n`, 'utf8');
|
|
1766
|
+
resolvedSqlPreviewFiles.push(relativeResolvedPath);
|
|
1767
|
+
}
|
|
1768
|
+
else {
|
|
1769
|
+
resolvedSqlPreviewFiles.push('');
|
|
1770
|
+
}
|
|
1771
|
+
if ((trace === null || trace === void 0 ? void 0 : trace.planJson) !== undefined && trace.planJson !== null) {
|
|
1772
|
+
const planFileName = `${baseName}.plan.json`;
|
|
1773
|
+
const relativePlanPath = node_path_1.default.join('plans', planFileName).replace(/\\/g, '/');
|
|
1774
|
+
(0, node_fs_1.writeFileSync)(node_path_1.default.join(plansDir, planFileName), `${JSON.stringify(trace.planJson, null, 2)}\n`, 'utf8');
|
|
1775
|
+
planFiles.push(relativePlanPath);
|
|
1776
|
+
}
|
|
1777
|
+
else {
|
|
1778
|
+
planFiles.push('');
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
const summary = {
|
|
1782
|
+
...report,
|
|
1783
|
+
run_id: reserved.runId,
|
|
1784
|
+
evidence_dir: reserved.evidenceDir,
|
|
1785
|
+
saved: true,
|
|
1786
|
+
executed_statements: report.executed_statements.map((statement, index) => ({
|
|
1787
|
+
...statement,
|
|
1788
|
+
sql_file: sqlFiles[index] || undefined,
|
|
1789
|
+
resolved_sql_preview_file: resolvedSqlPreviewFiles[index] || undefined,
|
|
1790
|
+
plan_file: planFiles[index] || undefined
|
|
1791
|
+
}))
|
|
1792
|
+
};
|
|
1793
|
+
(0, node_fs_1.writeFileSync)(node_path_1.default.join(reserved.evidenceDir, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
|
|
1794
|
+
(0, node_fs_1.writeFileSync)(node_path_1.default.join(reserved.evidenceDir, 'executed-statements.json'), `${JSON.stringify(summary.executed_statements, null, 2)}\n`, 'utf8');
|
|
1795
|
+
return {
|
|
1796
|
+
runId: reserved.runId,
|
|
1797
|
+
evidenceDir: reserved.evidenceDir,
|
|
1798
|
+
planFiles,
|
|
1799
|
+
sqlFiles,
|
|
1800
|
+
resolvedSqlPreviewFiles
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function reservePerfEvidenceDir(evidenceRoot, label) {
|
|
1804
|
+
const suffix = label ? `_${sanitizeLabel(label)}` : '';
|
|
1805
|
+
let nextRun = readHighestPerfRunIndex(evidenceRoot) + 1;
|
|
1806
|
+
// Reserve the run directory atomically so concurrent perf runs cannot collide.
|
|
1807
|
+
for (let attempts = 0; attempts < 1024; attempts += 1) {
|
|
1808
|
+
const runId = `run_${String(nextRun).padStart(3, '0')}${suffix}`;
|
|
1809
|
+
const evidenceDir = node_path_1.default.join(evidenceRoot, runId);
|
|
1810
|
+
try {
|
|
1811
|
+
(0, node_fs_1.mkdirSync)(evidenceDir);
|
|
1812
|
+
return { runId, evidenceDir };
|
|
1813
|
+
}
|
|
1814
|
+
catch (error) {
|
|
1815
|
+
if (isAlreadyExistsError(error)) {
|
|
1816
|
+
nextRun += 1;
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
throw error;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
throw new Error('Unable to allocate a perf evidence directory after repeated collisions.');
|
|
1823
|
+
}
|
|
1824
|
+
function readHighestPerfRunIndex(evidenceRoot) {
|
|
1825
|
+
const existing = (0, node_fs_1.readdirSync)(evidenceRoot, { withFileTypes: true })
|
|
1826
|
+
.filter((entry) => entry.isDirectory())
|
|
1827
|
+
.map((entry) => entry.name);
|
|
1828
|
+
return existing.reduce((max, name) => {
|
|
1829
|
+
const match = /^run_(\d+)/.exec(name);
|
|
1830
|
+
return match ? Math.max(max, Number(match[1])) : max;
|
|
1831
|
+
}, 0);
|
|
1832
|
+
}
|
|
1833
|
+
function sanitizeLabel(label) {
|
|
1834
|
+
return label.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1835
|
+
}
|
|
1836
|
+
function isAlreadyExistsError(error) {
|
|
1837
|
+
return error instanceof Error && 'code' in error && error.code === 'EEXIST';
|
|
1838
|
+
}
|
|
1839
|
+
function toPerfClassificationProbe(result) {
|
|
1840
|
+
return {
|
|
1841
|
+
elapsed_ms: result.elapsedMs,
|
|
1842
|
+
timed_out: result.timedOut,
|
|
1843
|
+
row_count: result.rowCount
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
function validatePerfBenchmarkReport(summaryFile, value) {
|
|
1847
|
+
if (!isPerfBenchmarkReport(value)) {
|
|
1848
|
+
throw new Error(`Invalid perf benchmark summary: ${summaryFile}`);
|
|
1849
|
+
}
|
|
1850
|
+
return value;
|
|
1851
|
+
}
|
|
1852
|
+
function isPerfBenchmarkReport(value) {
|
|
1853
|
+
if (typeof value !== 'object' || value === null) {
|
|
1854
|
+
return false;
|
|
1855
|
+
}
|
|
1856
|
+
const report = value;
|
|
1857
|
+
if (report.schema_version !== 1 || report.command !== 'perf run') {
|
|
1858
|
+
return false;
|
|
1859
|
+
}
|
|
1860
|
+
if (report.query_type !== 'SELECT' || !isPerfExecutionStrategy(report.strategy)) {
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
if (!isPerfSelectedMode(report.selected_mode) || !isPerfRequestedMode(report.requested_mode)) {
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
if (!isStringArray(report.ordered_param_names) || !isPerfStatementReportArray(report.executed_statements)) {
|
|
1867
|
+
return false;
|
|
1868
|
+
}
|
|
1869
|
+
if (!isStringArray(report.plan_observations) || !isPerfRecommendedActionArray(report.recommended_actions)) {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
if (!isPerfPipelineAnalysis(report.pipeline_analysis)
|
|
1873
|
+
|| !isOptionalPerfClassificationProbe(report.classification_probe)
|
|
1874
|
+
|| !isOptionalPerfQuerySpecGuidance(report.spec_guidance)
|
|
1875
|
+
|| !isOptionalPerfDdlInventorySummary(report.ddl_inventory)
|
|
1876
|
+
|| !isOptionalPerfTuningGuidance(report.tuning_guidance)
|
|
1877
|
+
|| !isOptionalPerfTuningSummary(report.tuning_summary)) {
|
|
1878
|
+
return false;
|
|
1879
|
+
}
|
|
1880
|
+
return typeof report.query_file === 'string'
|
|
1881
|
+
&& typeof report.source_sql_file === 'string'
|
|
1882
|
+
&& typeof report.source_sql === 'string'
|
|
1883
|
+
&& typeof report.bound_sql === 'string'
|
|
1884
|
+
&& typeof report.selection_reason === 'string'
|
|
1885
|
+
&& typeof report.classify_threshold_ms === 'number'
|
|
1886
|
+
&& typeof report.timeout_ms === 'number'
|
|
1887
|
+
&& typeof report.dry_run === 'boolean'
|
|
1888
|
+
&& typeof report.saved === 'boolean'
|
|
1889
|
+
&& isOptionalString(report.params_file)
|
|
1890
|
+
&& isOptionalString(report.run_id)
|
|
1891
|
+
&& isOptionalString(report.label)
|
|
1892
|
+
&& isOptionalString(report.evidence_dir)
|
|
1893
|
+
&& isOptionalString(report.database_version)
|
|
1894
|
+
&& isOptionalNumber(report.total_elapsed_ms)
|
|
1895
|
+
&& isOptionalPerfPlanSummary(report.plan_summary)
|
|
1896
|
+
&& isOptionalLatencyMetrics(report.latency_metrics)
|
|
1897
|
+
&& isOptionalCompletionMetrics(report.completion_metrics)
|
|
1898
|
+
&& isOptionalPerfStrategyMetadata(report.strategy_metadata);
|
|
1899
|
+
}
|
|
1900
|
+
function isOptionalPerfQuerySpecGuidance(value) {
|
|
1901
|
+
if (value === undefined) {
|
|
1902
|
+
return true;
|
|
1903
|
+
}
|
|
1904
|
+
if (typeof value !== 'object' || value === null) {
|
|
1905
|
+
return false;
|
|
1906
|
+
}
|
|
1907
|
+
const guidance = value;
|
|
1908
|
+
return typeof guidance.spec_id === 'string'
|
|
1909
|
+
&& typeof guidance.spec_file === 'string'
|
|
1910
|
+
&& isOptionalPerfExpectedScale(guidance.expected_scale)
|
|
1911
|
+
&& isOptionalNumber(guidance.expected_input_rows)
|
|
1912
|
+
&& isOptionalNumber(guidance.expected_output_rows)
|
|
1913
|
+
&& isPerfReviewPolicy(guidance.review_policy)
|
|
1914
|
+
&& isPerfEvidenceStatus(guidance.evidence_status)
|
|
1915
|
+
&& isOptionalNumber(guidance.fixture_rows_available)
|
|
1916
|
+
&& isPerfFixtureRowsStatus(guidance.fixture_rows_status);
|
|
1917
|
+
}
|
|
1918
|
+
function isOptionalPerfDdlInventorySummary(value) {
|
|
1919
|
+
if (value === undefined) {
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1922
|
+
if (typeof value !== 'object' || value === null) {
|
|
1923
|
+
return false;
|
|
1924
|
+
}
|
|
1925
|
+
const inventory = value;
|
|
1926
|
+
return typeof inventory.ddl_files === 'number'
|
|
1927
|
+
&& typeof inventory.ddl_statement_count === 'number'
|
|
1928
|
+
&& typeof inventory.table_count === 'number'
|
|
1929
|
+
&& typeof inventory.index_count === 'number'
|
|
1930
|
+
&& isStringArray(inventory.index_names);
|
|
1931
|
+
}
|
|
1932
|
+
function isOptionalPerfTuningGuidance(value) {
|
|
1933
|
+
if (value === undefined) {
|
|
1934
|
+
return true;
|
|
1935
|
+
}
|
|
1936
|
+
if (typeof value !== 'object' || value === null) {
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
const guidance = value;
|
|
1940
|
+
return isPerfTuningPrimaryPath(guidance.primary_path)
|
|
1941
|
+
&& typeof guidance.requires_captured_plan === 'boolean'
|
|
1942
|
+
&& isPerfTuningBranchGuidance(guidance.index_branch)
|
|
1943
|
+
&& isPerfTuningBranchGuidance(guidance.pipeline_branch);
|
|
1944
|
+
}
|
|
1945
|
+
function isPerfTuningPrimaryPath(value) {
|
|
1946
|
+
return value === 'index' || value === 'pipeline' || value === 'capture-plan';
|
|
1947
|
+
}
|
|
1948
|
+
function isOptionalPerfTuningSummary(value) {
|
|
1949
|
+
if (value === undefined) {
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
1952
|
+
if (typeof value !== 'object' || value === null) {
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
const summary = value;
|
|
1956
|
+
return typeof summary.headline === 'string'
|
|
1957
|
+
&& isStringArray(summary.evidence)
|
|
1958
|
+
&& typeof summary.next_step === 'string';
|
|
1959
|
+
}
|
|
1960
|
+
function isPerfTuningBranchGuidance(value) {
|
|
1961
|
+
if (typeof value !== 'object' || value === null) {
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
const guidance = value;
|
|
1965
|
+
return typeof guidance.recommended === 'boolean'
|
|
1966
|
+
&& isStringArray(guidance.rationale)
|
|
1967
|
+
&& isStringArray(guidance.next_steps);
|
|
1968
|
+
}
|
|
1969
|
+
function isOptionalPerfExpectedScale(value) {
|
|
1970
|
+
return value === undefined || value === 'tiny' || value === 'small' || value === 'medium' || value === 'large' || value === 'batch';
|
|
1971
|
+
}
|
|
1972
|
+
function isPerfReviewPolicy(value) {
|
|
1973
|
+
return value === 'none' || value === 'recommended' || value === 'strongly-recommended';
|
|
1974
|
+
}
|
|
1975
|
+
function isPerfEvidenceStatus(value) {
|
|
1976
|
+
return value === 'captured' || value === 'missing' || value === 'not-required';
|
|
1977
|
+
}
|
|
1978
|
+
function isPerfFixtureRowsStatus(value) {
|
|
1979
|
+
return value === 'sufficient' || value === 'undersized' || value === 'unknown';
|
|
1980
|
+
}
|
|
1981
|
+
function isPerfExecutionStrategy(value) {
|
|
1982
|
+
return value === 'direct' || value === 'decomposed';
|
|
1983
|
+
}
|
|
1984
|
+
function isPerfSelectedMode(value) {
|
|
1985
|
+
return value === 'latency' || value === 'completion';
|
|
1986
|
+
}
|
|
1987
|
+
function isPerfRequestedMode(value) {
|
|
1988
|
+
return value === 'auto' || value === 'latency' || value === 'completion';
|
|
1989
|
+
}
|
|
1990
|
+
function isPerfPipelineAnalysis(value) {
|
|
1991
|
+
if (typeof value !== 'object' || value === null) {
|
|
1992
|
+
return false;
|
|
1993
|
+
}
|
|
1994
|
+
const analysis = value;
|
|
1995
|
+
// Older saved summaries may not include scalar_filter_candidates.
|
|
1996
|
+
// Treat the field as optional so perf diff remains backward-compatible.
|
|
1997
|
+
return typeof analysis.query_type === 'string'
|
|
1998
|
+
&& typeof analysis.cte_count === 'number'
|
|
1999
|
+
&& typeof analysis.should_consider_pipeline === 'boolean'
|
|
2000
|
+
&& isPerfPipelineCandidateArray(analysis.candidate_ctes)
|
|
2001
|
+
&& (analysis.scalar_filter_candidates === undefined || isStringArray(analysis.scalar_filter_candidates))
|
|
2002
|
+
&& isStringArray(analysis.notes);
|
|
2003
|
+
}
|
|
2004
|
+
function isPerfPipelineCandidateArray(value) {
|
|
2005
|
+
return Array.isArray(value) && value.every((candidate) => {
|
|
2006
|
+
if (typeof candidate !== 'object' || candidate === null) {
|
|
2007
|
+
return false;
|
|
2008
|
+
}
|
|
2009
|
+
const record = candidate;
|
|
2010
|
+
return typeof record.name === 'string'
|
|
2011
|
+
&& typeof record.downstream_references === 'number'
|
|
2012
|
+
&& isStringArray(record.reasons);
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
function isPerfStatementReportArray(value) {
|
|
2016
|
+
return Array.isArray(value) && value.every((statement) => isPerfStatementReport(statement));
|
|
2017
|
+
}
|
|
2018
|
+
function isPerfStatementReport(value) {
|
|
2019
|
+
if (typeof value !== 'object' || value === null) {
|
|
2020
|
+
return false;
|
|
2021
|
+
}
|
|
2022
|
+
const statement = value;
|
|
2023
|
+
return typeof statement.seq === 'number'
|
|
2024
|
+
&& isPerfStatementRole(statement.role)
|
|
2025
|
+
&& typeof statement.sql === 'string'
|
|
2026
|
+
&& isOptionalString(statement.target)
|
|
2027
|
+
&& isOptionalBindings(statement.bindings)
|
|
2028
|
+
&& isOptionalString(statement.resolved_sql_preview)
|
|
2029
|
+
&& isOptionalNumber(statement.row_count)
|
|
2030
|
+
&& isOptionalNumber(statement.elapsed_ms)
|
|
2031
|
+
&& isOptionalBoolean(statement.timed_out)
|
|
2032
|
+
&& isOptionalPerfPlanSummary(statement.plan_summary)
|
|
2033
|
+
&& isOptionalString(statement.sql_file)
|
|
2034
|
+
&& isOptionalString(statement.resolved_sql_preview_file)
|
|
2035
|
+
&& isOptionalString(statement.plan_file);
|
|
2036
|
+
}
|
|
2037
|
+
function isPerfStatementRole(value) {
|
|
2038
|
+
return value === 'materialize' || value === 'scalar-filter-bind' || value === 'final-query';
|
|
2039
|
+
}
|
|
2040
|
+
function isPerfRecommendedActionArray(value) {
|
|
2041
|
+
return Array.isArray(value) && value.every((action) => isPerfRecommendedAction(action));
|
|
2042
|
+
}
|
|
2043
|
+
function isPerfRecommendedAction(value) {
|
|
2044
|
+
if (typeof value !== 'object' || value === null) {
|
|
2045
|
+
return false;
|
|
2046
|
+
}
|
|
2047
|
+
const action = value;
|
|
2048
|
+
return typeof action.action === 'string'
|
|
2049
|
+
&& (action.priority === 'high' || action.priority === 'medium')
|
|
2050
|
+
&& typeof action.rationale === 'string';
|
|
2051
|
+
}
|
|
2052
|
+
function isOptionalPerfClassificationProbe(value) {
|
|
2053
|
+
if (value === undefined) {
|
|
2054
|
+
return true;
|
|
2055
|
+
}
|
|
2056
|
+
if (typeof value !== 'object' || value === null) {
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
const probe = value;
|
|
2060
|
+
return typeof probe.elapsed_ms === 'number'
|
|
2061
|
+
&& typeof probe.timed_out === 'boolean'
|
|
2062
|
+
&& isOptionalNumber(probe.row_count)
|
|
2063
|
+
&& isOptionalBoolean(probe.reused_as_warmup)
|
|
2064
|
+
&& isOptionalBoolean(probe.reused_as_measured_run);
|
|
2065
|
+
}
|
|
2066
|
+
function isOptionalPerfPlanSummary(value) {
|
|
2067
|
+
if (value === undefined || value === null) {
|
|
2068
|
+
return true;
|
|
2069
|
+
}
|
|
2070
|
+
if (typeof value !== 'object') {
|
|
2071
|
+
return false;
|
|
2072
|
+
}
|
|
2073
|
+
const summary = value;
|
|
2074
|
+
return isOptionalString(summary.node_type)
|
|
2075
|
+
&& isOptionalString(summary.join_type)
|
|
2076
|
+
&& isOptionalNumber(summary.total_cost)
|
|
2077
|
+
&& isOptionalNumber(summary.plan_rows)
|
|
2078
|
+
&& isOptionalNumber(summary.actual_rows)
|
|
2079
|
+
&& isOptionalNumber(summary.actual_total_time);
|
|
2080
|
+
}
|
|
2081
|
+
function isOptionalPerfStrategyMetadata(value) {
|
|
2082
|
+
if (value === undefined) {
|
|
2083
|
+
return true;
|
|
2084
|
+
}
|
|
2085
|
+
if (typeof value !== 'object' || value === null) {
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
const metadata = value;
|
|
2089
|
+
return isStringArray(metadata.materialized_ctes)
|
|
2090
|
+
&& isStringArray(metadata.scalar_filter_columns)
|
|
2091
|
+
&& Array.isArray(metadata.planned_steps)
|
|
2092
|
+
&& metadata.planned_steps.every((step) => {
|
|
2093
|
+
if (typeof step !== 'object' || step === null) {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
const record = step;
|
|
2097
|
+
return typeof record.step === 'number'
|
|
2098
|
+
&& (record.kind === 'materialize' || record.kind === 'final-query')
|
|
2099
|
+
&& typeof record.target === 'string'
|
|
2100
|
+
&& isStringArray(record.depends_on);
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
function isOptionalLatencyMetrics(value) {
|
|
2104
|
+
if (value === undefined) {
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2107
|
+
if (typeof value !== 'object' || value === null) {
|
|
2108
|
+
return false;
|
|
2109
|
+
}
|
|
2110
|
+
const metrics = value;
|
|
2111
|
+
return typeof metrics.measured_runs === 'number'
|
|
2112
|
+
&& typeof metrics.warmup_runs === 'number'
|
|
2113
|
+
&& typeof metrics.min_ms === 'number'
|
|
2114
|
+
&& typeof metrics.max_ms === 'number'
|
|
2115
|
+
&& typeof metrics.avg_ms === 'number'
|
|
2116
|
+
&& typeof metrics.median_ms === 'number'
|
|
2117
|
+
&& typeof metrics.p95_ms === 'number';
|
|
2118
|
+
}
|
|
2119
|
+
function isOptionalCompletionMetrics(value) {
|
|
2120
|
+
if (value === undefined) {
|
|
2121
|
+
return true;
|
|
2122
|
+
}
|
|
2123
|
+
if (typeof value !== 'object' || value === null) {
|
|
2124
|
+
return false;
|
|
2125
|
+
}
|
|
2126
|
+
const metrics = value;
|
|
2127
|
+
return typeof metrics.completed === 'boolean'
|
|
2128
|
+
&& typeof metrics.timed_out === 'boolean'
|
|
2129
|
+
&& typeof metrics.wall_time_ms === 'number';
|
|
2130
|
+
}
|
|
2131
|
+
function isOptionalBindings(value) {
|
|
2132
|
+
return value === undefined || Array.isArray(value) || (typeof value === 'object' && value !== null);
|
|
2133
|
+
}
|
|
2134
|
+
function isStringArray(value) {
|
|
2135
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
2136
|
+
}
|
|
2137
|
+
function isOptionalString(value) {
|
|
2138
|
+
return value === undefined || typeof value === 'string';
|
|
2139
|
+
}
|
|
2140
|
+
function isOptionalNumber(value) {
|
|
2141
|
+
return value === undefined || typeof value === 'number';
|
|
2142
|
+
}
|
|
2143
|
+
function isOptionalBoolean(value) {
|
|
2144
|
+
return value === undefined || typeof value === 'boolean';
|
|
2145
|
+
}
|
|
2146
|
+
function normalizeFinalQueryRoots(finalQuery) {
|
|
2147
|
+
if (Array.isArray(finalQuery)) {
|
|
2148
|
+
return finalQuery.map((value) => value.trim()).filter(Boolean);
|
|
2149
|
+
}
|
|
2150
|
+
if (typeof finalQuery === 'string') {
|
|
2151
|
+
return finalQuery.split(',').map((value) => value.trim()).filter(Boolean);
|
|
2152
|
+
}
|
|
2153
|
+
return [];
|
|
2154
|
+
}
|
|
2155
|
+
function calculateImprovementPercent(baseline, candidate) {
|
|
2156
|
+
if (baseline === 0) {
|
|
2157
|
+
return 0;
|
|
2158
|
+
}
|
|
2159
|
+
return ((baseline - candidate) / baseline) * 100;
|
|
2160
|
+
}
|
|
2161
|
+
function extractRowCount(result) {
|
|
2162
|
+
if (typeof result.rowCount === 'number') {
|
|
2163
|
+
return result.rowCount;
|
|
2164
|
+
}
|
|
2165
|
+
return Array.isArray(result.rows) ? result.rows.length : undefined;
|
|
2166
|
+
}
|
|
2167
|
+
function isQueryTimeout(error) {
|
|
2168
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === '57014';
|
|
2169
|
+
}
|
|
2170
|
+
function nowMs() {
|
|
2171
|
+
return Number(process.hrtime.bigint()) / 1000000;
|
|
2172
|
+
}
|
|
2173
|
+
function normalizeNumber(value) {
|
|
2174
|
+
return typeof value === 'number' ? value : undefined;
|
|
2175
|
+
}
|
|
2176
|
+
function normalizeString(value) {
|
|
2177
|
+
return typeof value === 'string' ? value : undefined;
|
|
2178
|
+
}
|
|
2179
|
+
function truncateSingleLine(value, limit) {
|
|
2180
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
2181
|
+
if (normalized.length <= limit) {
|
|
2182
|
+
return normalized;
|
|
2183
|
+
}
|
|
2184
|
+
return `${normalized.slice(0, limit - 3)}...`;
|
|
2185
|
+
}
|
|
2186
|
+
//# sourceMappingURL=benchmark.js.map
|