@prisma-next/runtime-executor 0.3.0-pr.94.3 → 0.3.0-pr.95.2
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/dist/async-iterable-result.d.ts +17 -0
- package/dist/async-iterable-result.d.ts.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/exports/index.d.ts +17 -0
- package/dist/exports/index.d.ts.map +1 -0
- package/dist/fingerprint.d.ts +2 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/guardrails/raw.d.ts +28 -0
- package/dist/guardrails/raw.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +746 -0
- package/dist/index.js.map +1 -0
- package/dist/marker.d.ts +13 -0
- package/dist/marker.d.ts.map +1 -0
- package/dist/plugins/budgets.d.ts +16 -0
- package/dist/plugins/budgets.d.ts.map +1 -0
- package/dist/plugins/lints.d.ts +11 -0
- package/dist/plugins/lints.d.ts.map +1 -0
- package/dist/plugins/types.d.ts +27 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/runtime-core.d.ts +37 -0
- package/dist/runtime-core.d.ts.map +1 -0
- package/dist/runtime-spi.d.ts +14 -0
- package/dist/runtime-spi.d.ts.map +1 -0
- package/package.json +11 -16
- package/dist/index.d.mts +0 -167
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -563
- package/dist/index.mjs.map +0 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
function runtimeError(code, message, details) {
|
|
3
|
+
const error = new Error(message);
|
|
4
|
+
Object.defineProperty(error, "name", {
|
|
5
|
+
value: "RuntimeError",
|
|
6
|
+
configurable: true
|
|
7
|
+
});
|
|
8
|
+
return Object.assign(error, {
|
|
9
|
+
code,
|
|
10
|
+
category: resolveCategory(code),
|
|
11
|
+
severity: "error",
|
|
12
|
+
message,
|
|
13
|
+
details
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function resolveCategory(code) {
|
|
17
|
+
const prefix = code.split(".")[0] ?? "RUNTIME";
|
|
18
|
+
switch (prefix) {
|
|
19
|
+
case "PLAN":
|
|
20
|
+
case "CONTRACT":
|
|
21
|
+
case "LINT":
|
|
22
|
+
case "BUDGET":
|
|
23
|
+
return prefix;
|
|
24
|
+
default:
|
|
25
|
+
return "RUNTIME";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/async-iterable-result.ts
|
|
30
|
+
var AsyncIterableResult = class {
|
|
31
|
+
generator;
|
|
32
|
+
consumed = false;
|
|
33
|
+
consumedBy;
|
|
34
|
+
constructor(generator) {
|
|
35
|
+
this.generator = generator;
|
|
36
|
+
}
|
|
37
|
+
[Symbol.asyncIterator]() {
|
|
38
|
+
if (this.consumed) {
|
|
39
|
+
throw runtimeError(
|
|
40
|
+
"RUNTIME.ITERATOR_CONSUMED",
|
|
41
|
+
`AsyncIterableResult iterator has already been consumed via ${this.consumedBy === "toArray" ? "toArray()" : "for-await loop"}. Each AsyncIterableResult can only be iterated once.`,
|
|
42
|
+
{
|
|
43
|
+
consumedBy: this.consumedBy,
|
|
44
|
+
suggestion: this.consumedBy === "toArray" ? "If you need to iterate multiple times, store the results from toArray() in a variable and reuse that." : "If you need to iterate multiple times, use toArray() to collect all results first."
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
this.consumed = true;
|
|
49
|
+
this.consumedBy = "iterator";
|
|
50
|
+
return this.generator;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Collects all values from the async iterator into an array.
|
|
54
|
+
* Once called, the iterator is consumed and cannot be reused.
|
|
55
|
+
*/
|
|
56
|
+
async toArray() {
|
|
57
|
+
if (this.consumed) {
|
|
58
|
+
throw runtimeError(
|
|
59
|
+
"RUNTIME.ITERATOR_CONSUMED",
|
|
60
|
+
`AsyncIterableResult iterator has already been consumed via ${this.consumedBy === "toArray" ? "toArray()" : "for-await loop"}. Each AsyncIterableResult can only be iterated once.`,
|
|
61
|
+
{
|
|
62
|
+
consumedBy: this.consumedBy,
|
|
63
|
+
suggestion: this.consumedBy === "toArray" ? "You cannot call toArray() twice on the same AsyncIterableResult. Store the result from the first call in a variable and reuse that." : "The iterator was already consumed by a for-await loop. Use toArray() to collect all results before iterating."
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
this.consumed = true;
|
|
68
|
+
this.consumedBy = "toArray";
|
|
69
|
+
const out = [];
|
|
70
|
+
for await (const item of this.generator) {
|
|
71
|
+
out.push(item);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/fingerprint.ts
|
|
78
|
+
import { createHash } from "crypto";
|
|
79
|
+
var STRING_LITERAL_REGEX = /'(?:''|[^'])*'/g;
|
|
80
|
+
var NUMERIC_LITERAL_REGEX = /\b\d+(?:\.\d+)?\b/g;
|
|
81
|
+
var WHITESPACE_REGEX = /\s+/g;
|
|
82
|
+
function computeSqlFingerprint(sql) {
|
|
83
|
+
const withoutStrings = sql.replace(STRING_LITERAL_REGEX, "?");
|
|
84
|
+
const withoutNumbers = withoutStrings.replace(NUMERIC_LITERAL_REGEX, "?");
|
|
85
|
+
const normalized = withoutNumbers.replace(WHITESPACE_REGEX, " ").trim().toLowerCase();
|
|
86
|
+
const hash = createHash("sha256").update(normalized).digest("hex");
|
|
87
|
+
return `sha256:${hash}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/guardrails/raw.ts
|
|
91
|
+
var SELECT_STAR_REGEX = /select\s+\*/i;
|
|
92
|
+
var LIMIT_REGEX = /\blimit\b/i;
|
|
93
|
+
var MUTATION_PREFIX_REGEX = /^(insert|update|delete|create|alter|drop|truncate)\b/i;
|
|
94
|
+
var READ_ONLY_INTENTS = /* @__PURE__ */ new Set(["read", "report", "readonly"]);
|
|
95
|
+
function evaluateRawGuardrails(plan, config) {
|
|
96
|
+
const lints2 = [];
|
|
97
|
+
const budgets2 = [];
|
|
98
|
+
const normalized = normalizeWhitespace(plan.sql);
|
|
99
|
+
const statementType = classifyStatement(normalized);
|
|
100
|
+
if (statementType === "select") {
|
|
101
|
+
if (SELECT_STAR_REGEX.test(normalized)) {
|
|
102
|
+
lints2.push(
|
|
103
|
+
createLint("LINT.SELECT_STAR", "error", "Raw SQL plan selects all columns via *", {
|
|
104
|
+
sql: snippet(plan.sql)
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (!LIMIT_REGEX.test(normalized)) {
|
|
109
|
+
const severity = config?.budgets?.unboundedSelectSeverity ?? "error";
|
|
110
|
+
lints2.push(
|
|
111
|
+
createLint("LINT.NO_LIMIT", "warn", "Raw SQL plan omits LIMIT clause", {
|
|
112
|
+
sql: snippet(plan.sql)
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
budgets2.push(
|
|
116
|
+
createBudget(
|
|
117
|
+
"BUDGET.ROWS_EXCEEDED",
|
|
118
|
+
severity,
|
|
119
|
+
"Raw SQL plan is unbounded and may exceed row budget",
|
|
120
|
+
{
|
|
121
|
+
sql: snippet(plan.sql),
|
|
122
|
+
...config?.budgets?.estimatedRows !== void 0 ? { estimatedRows: config.budgets.estimatedRows } : {}
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (isMutationStatement(statementType) && isReadOnlyIntent(plan.meta)) {
|
|
129
|
+
lints2.push(
|
|
130
|
+
createLint(
|
|
131
|
+
"LINT.READ_ONLY_MUTATION",
|
|
132
|
+
"error",
|
|
133
|
+
"Raw SQL plan mutates data despite read-only intent",
|
|
134
|
+
{
|
|
135
|
+
sql: snippet(plan.sql),
|
|
136
|
+
intent: plan.meta.annotations?.["intent"]
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const refs = plan.meta.refs;
|
|
142
|
+
if (refs) {
|
|
143
|
+
evaluateIndexCoverage(refs, lints2);
|
|
144
|
+
}
|
|
145
|
+
return { lints: lints2, budgets: budgets2, statement: statementType };
|
|
146
|
+
}
|
|
147
|
+
function evaluateIndexCoverage(refs, lints2) {
|
|
148
|
+
const predicateColumns = refs.columns ?? [];
|
|
149
|
+
if (predicateColumns.length === 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const indexes = refs.indexes ?? [];
|
|
153
|
+
if (indexes.length === 0) {
|
|
154
|
+
lints2.push(
|
|
155
|
+
createLint(
|
|
156
|
+
"LINT.UNINDEXED_PREDICATE",
|
|
157
|
+
"warn",
|
|
158
|
+
"Raw SQL plan predicates lack supporting indexes",
|
|
159
|
+
{
|
|
160
|
+
predicates: predicateColumns
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const hasSupportingIndex = predicateColumns.every(
|
|
167
|
+
(column) => indexes.some(
|
|
168
|
+
(index) => index.table === column.table && index.columns.some((col) => col.toLowerCase() === column.column.toLowerCase())
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
if (!hasSupportingIndex) {
|
|
172
|
+
lints2.push(
|
|
173
|
+
createLint(
|
|
174
|
+
"LINT.UNINDEXED_PREDICATE",
|
|
175
|
+
"warn",
|
|
176
|
+
"Raw SQL plan predicates lack supporting indexes",
|
|
177
|
+
{
|
|
178
|
+
predicates: predicateColumns
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function classifyStatement(sql) {
|
|
185
|
+
const trimmed = sql.trim();
|
|
186
|
+
const lower = trimmed.toLowerCase();
|
|
187
|
+
if (lower.startsWith("with")) {
|
|
188
|
+
if (lower.includes("select")) {
|
|
189
|
+
return "select";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (lower.startsWith("select")) {
|
|
193
|
+
return "select";
|
|
194
|
+
}
|
|
195
|
+
if (MUTATION_PREFIX_REGEX.test(trimmed)) {
|
|
196
|
+
return "mutation";
|
|
197
|
+
}
|
|
198
|
+
return "other";
|
|
199
|
+
}
|
|
200
|
+
function isMutationStatement(statement) {
|
|
201
|
+
return statement === "mutation";
|
|
202
|
+
}
|
|
203
|
+
function isReadOnlyIntent(meta) {
|
|
204
|
+
const annotations = meta.annotations;
|
|
205
|
+
const intent = typeof annotations?.intent === "string" ? annotations.intent.toLowerCase() : void 0;
|
|
206
|
+
return intent !== void 0 && READ_ONLY_INTENTS.has(intent);
|
|
207
|
+
}
|
|
208
|
+
function normalizeWhitespace(value) {
|
|
209
|
+
return value.replace(/\s+/g, " ").trim();
|
|
210
|
+
}
|
|
211
|
+
function snippet(sql) {
|
|
212
|
+
return normalizeWhitespace(sql).slice(0, 200);
|
|
213
|
+
}
|
|
214
|
+
function createLint(code, severity, message, details) {
|
|
215
|
+
return { code, severity, message, ...details ? { details } : {} };
|
|
216
|
+
}
|
|
217
|
+
function createBudget(code, severity, message, details) {
|
|
218
|
+
return { code, severity, message, ...details ? { details } : {} };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/marker.ts
|
|
222
|
+
import { type } from "arktype";
|
|
223
|
+
var MetaSchema = type({ "[string]": "unknown" });
|
|
224
|
+
function parseMeta(meta) {
|
|
225
|
+
if (meta === null || meta === void 0) {
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
228
|
+
let parsed;
|
|
229
|
+
if (typeof meta === "string") {
|
|
230
|
+
try {
|
|
231
|
+
parsed = JSON.parse(meta);
|
|
232
|
+
} catch {
|
|
233
|
+
return {};
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
parsed = meta;
|
|
237
|
+
}
|
|
238
|
+
const result = MetaSchema(parsed);
|
|
239
|
+
if (result instanceof type.errors) {
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
var ContractMarkerRowSchema = type({
|
|
245
|
+
core_hash: "string",
|
|
246
|
+
profile_hash: "string",
|
|
247
|
+
"contract_json?": "unknown | null",
|
|
248
|
+
"canonical_version?": "number | null",
|
|
249
|
+
"updated_at?": "Date | string",
|
|
250
|
+
"app_tag?": "string | null",
|
|
251
|
+
"meta?": "unknown | null"
|
|
252
|
+
});
|
|
253
|
+
function parseContractMarkerRow(row) {
|
|
254
|
+
const result = ContractMarkerRowSchema(row);
|
|
255
|
+
if (result instanceof type.errors) {
|
|
256
|
+
const messages = result.map((p) => p.message).join("; ");
|
|
257
|
+
throw new Error(`Invalid contract marker row: ${messages}`);
|
|
258
|
+
}
|
|
259
|
+
const validatedRow = result;
|
|
260
|
+
const updatedAt = validatedRow.updated_at ? validatedRow.updated_at instanceof Date ? validatedRow.updated_at : new Date(validatedRow.updated_at) : /* @__PURE__ */ new Date();
|
|
261
|
+
return {
|
|
262
|
+
coreHash: validatedRow.core_hash,
|
|
263
|
+
profileHash: validatedRow.profile_hash,
|
|
264
|
+
contractJson: validatedRow.contract_json ?? null,
|
|
265
|
+
canonicalVersion: validatedRow.canonical_version ?? null,
|
|
266
|
+
updatedAt,
|
|
267
|
+
appTag: validatedRow.app_tag ?? null,
|
|
268
|
+
meta: parseMeta(validatedRow.meta)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/plugins/budgets.ts
|
|
273
|
+
async function computeEstimatedRows(plan, driver) {
|
|
274
|
+
if (typeof driver.explain !== "function") {
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const result = await driver.explain(plan.sql, [...plan.params]);
|
|
279
|
+
return extractEstimatedRows(result.rows);
|
|
280
|
+
} catch {
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function extractEstimatedRows(rows) {
|
|
285
|
+
for (const row of rows) {
|
|
286
|
+
const estimate = findPlanRows(row);
|
|
287
|
+
if (estimate !== void 0) {
|
|
288
|
+
return estimate;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return void 0;
|
|
292
|
+
}
|
|
293
|
+
function findPlanRows(node) {
|
|
294
|
+
if (!node || typeof node !== "object") {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
const explainNode = node;
|
|
298
|
+
const planRows = explainNode["Plan Rows"];
|
|
299
|
+
if (typeof planRows === "number") {
|
|
300
|
+
return planRows;
|
|
301
|
+
}
|
|
302
|
+
if ("Plan" in explainNode && explainNode.Plan !== void 0) {
|
|
303
|
+
const nested = findPlanRows(explainNode.Plan);
|
|
304
|
+
if (nested !== void 0) {
|
|
305
|
+
return nested;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (Array.isArray(explainNode.Plans)) {
|
|
309
|
+
for (const child of explainNode.Plans) {
|
|
310
|
+
const nested = findPlanRows(child);
|
|
311
|
+
if (nested !== void 0) {
|
|
312
|
+
return nested;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const value of Object.values(node)) {
|
|
317
|
+
if (typeof value === "object" && value !== null) {
|
|
318
|
+
const nested = findPlanRows(value);
|
|
319
|
+
if (nested !== void 0) {
|
|
320
|
+
return nested;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return void 0;
|
|
325
|
+
}
|
|
326
|
+
function budgetError(code, message, details) {
|
|
327
|
+
const error = new Error(message);
|
|
328
|
+
Object.defineProperty(error, "name", {
|
|
329
|
+
value: "RuntimeError",
|
|
330
|
+
configurable: true
|
|
331
|
+
});
|
|
332
|
+
return Object.assign(error, {
|
|
333
|
+
code,
|
|
334
|
+
category: "BUDGET",
|
|
335
|
+
severity: "error",
|
|
336
|
+
details
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function estimateRows(plan, tableRows, defaultTableRows) {
|
|
340
|
+
if (!plan.ast) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const table = plan.meta.refs?.tables?.[0];
|
|
344
|
+
if (!table) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const tableEstimate = tableRows[table] ?? defaultTableRows;
|
|
348
|
+
if (plan.ast && typeof plan.ast === "object" && "kind" in plan.ast && plan.ast.kind === "select" && "limit" in plan.ast && typeof plan.ast.limit === "number") {
|
|
349
|
+
return Math.min(plan.ast.limit, tableEstimate);
|
|
350
|
+
}
|
|
351
|
+
return tableEstimate;
|
|
352
|
+
}
|
|
353
|
+
function hasDetectableLimit(plan) {
|
|
354
|
+
if (plan.ast && typeof plan.ast === "object" && "kind" in plan.ast && plan.ast.kind === "select" && "limit" in plan.ast && typeof plan.ast.limit === "number") {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
const annotations = plan.meta.annotations;
|
|
358
|
+
return typeof annotations?.limit === "number" || typeof annotations?.LIMIT === "number";
|
|
359
|
+
}
|
|
360
|
+
function budgets(options) {
|
|
361
|
+
const maxRows = options?.maxRows ?? 1e4;
|
|
362
|
+
const defaultTableRows = options?.defaultTableRows ?? 1e4;
|
|
363
|
+
const tableRows = options?.tableRows ?? {};
|
|
364
|
+
const maxLatencyMs = options?.maxLatencyMs ?? 1e3;
|
|
365
|
+
const rowSeverity = options?.severities?.rowCount ?? "error";
|
|
366
|
+
const latencySeverity = options?.severities?.latency ?? "warn";
|
|
367
|
+
let observedRows = 0;
|
|
368
|
+
return Object.freeze({
|
|
369
|
+
name: "budgets",
|
|
370
|
+
async beforeExecute(plan, ctx) {
|
|
371
|
+
observedRows = 0;
|
|
372
|
+
void ctx.now();
|
|
373
|
+
const estimated = estimateRows(plan, tableRows, defaultTableRows);
|
|
374
|
+
const isUnbounded = !hasDetectableLimit(plan);
|
|
375
|
+
const sqlUpper = plan.sql.trimStart().toUpperCase();
|
|
376
|
+
const isSelect = sqlUpper.startsWith("SELECT");
|
|
377
|
+
if (isSelect && isUnbounded) {
|
|
378
|
+
if (estimated !== null && estimated >= maxRows) {
|
|
379
|
+
const error2 = budgetError(
|
|
380
|
+
"BUDGET.ROWS_EXCEEDED",
|
|
381
|
+
"Unbounded SELECT query exceeds budget",
|
|
382
|
+
{
|
|
383
|
+
source: "heuristic",
|
|
384
|
+
estimatedRows: estimated,
|
|
385
|
+
maxRows
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
const shouldBlock2 = rowSeverity === "error" || ctx.mode === "strict";
|
|
389
|
+
if (shouldBlock2) {
|
|
390
|
+
throw error2;
|
|
391
|
+
}
|
|
392
|
+
ctx.log.warn({
|
|
393
|
+
code: error2.code,
|
|
394
|
+
message: error2.message,
|
|
395
|
+
details: error2.details
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const error = budgetError("BUDGET.ROWS_EXCEEDED", "Unbounded SELECT query exceeds budget", {
|
|
400
|
+
source: "heuristic",
|
|
401
|
+
maxRows
|
|
402
|
+
});
|
|
403
|
+
const shouldBlock = rowSeverity === "error" || ctx.mode === "strict";
|
|
404
|
+
if (shouldBlock) {
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
ctx.log.warn({
|
|
408
|
+
code: error.code,
|
|
409
|
+
message: error.message,
|
|
410
|
+
details: error.details
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (estimated !== null) {
|
|
415
|
+
if (estimated > maxRows) {
|
|
416
|
+
const error = budgetError("BUDGET.ROWS_EXCEEDED", "Estimated row count exceeds budget", {
|
|
417
|
+
source: "heuristic",
|
|
418
|
+
estimatedRows: estimated,
|
|
419
|
+
maxRows
|
|
420
|
+
});
|
|
421
|
+
const shouldBlock = rowSeverity === "error" || ctx.mode === "strict";
|
|
422
|
+
if (shouldBlock) {
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
ctx.log.warn({
|
|
426
|
+
code: error.code,
|
|
427
|
+
message: error.message,
|
|
428
|
+
details: error.details
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (!plan.ast) {
|
|
434
|
+
const explainEnabled = options?.explain?.enabled === true;
|
|
435
|
+
if (explainEnabled && isSelect && typeof ctx.driver === "object" && ctx.driver !== null) {
|
|
436
|
+
const estimatedRows = await computeEstimatedRows(plan, ctx.driver);
|
|
437
|
+
if (estimatedRows !== void 0) {
|
|
438
|
+
if (estimatedRows > maxRows) {
|
|
439
|
+
const error = budgetError(
|
|
440
|
+
"BUDGET.ROWS_EXCEEDED",
|
|
441
|
+
"Estimated row count exceeds budget",
|
|
442
|
+
{
|
|
443
|
+
source: "explain",
|
|
444
|
+
estimatedRows,
|
|
445
|
+
maxRows
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
const shouldBlock = rowSeverity === "error" || ctx.mode === "strict";
|
|
449
|
+
if (shouldBlock) {
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
ctx.log.warn({
|
|
453
|
+
code: error.code,
|
|
454
|
+
message: error.message,
|
|
455
|
+
details: error.details
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
async onRow(_row, _plan, _ctx) {
|
|
464
|
+
void _row;
|
|
465
|
+
void _plan;
|
|
466
|
+
void _ctx;
|
|
467
|
+
observedRows += 1;
|
|
468
|
+
if (observedRows > maxRows) {
|
|
469
|
+
throw budgetError("BUDGET.ROWS_EXCEEDED", "Observed row count exceeds budget", {
|
|
470
|
+
source: "observed",
|
|
471
|
+
observedRows,
|
|
472
|
+
maxRows
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
async afterExecute(_plan, result, ctx) {
|
|
477
|
+
const latencyMs = result.latencyMs;
|
|
478
|
+
if (latencyMs > maxLatencyMs) {
|
|
479
|
+
const error = budgetError("BUDGET.TIME_EXCEEDED", "Query latency exceeds budget", {
|
|
480
|
+
latencyMs,
|
|
481
|
+
maxLatencyMs
|
|
482
|
+
});
|
|
483
|
+
const shouldBlock = latencySeverity === "error" && ctx.mode === "strict";
|
|
484
|
+
if (shouldBlock) {
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
ctx.log.warn({
|
|
488
|
+
code: error.code,
|
|
489
|
+
message: error.message,
|
|
490
|
+
details: error.details
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/plugins/lints.ts
|
|
498
|
+
function lintError(code, message, details) {
|
|
499
|
+
const error = new Error(message);
|
|
500
|
+
Object.defineProperty(error, "name", {
|
|
501
|
+
value: "RuntimeError",
|
|
502
|
+
configurable: true
|
|
503
|
+
});
|
|
504
|
+
return Object.assign(error, {
|
|
505
|
+
code,
|
|
506
|
+
category: "LINT",
|
|
507
|
+
severity: "error",
|
|
508
|
+
details
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function lints(options) {
|
|
512
|
+
return Object.freeze({
|
|
513
|
+
name: "lints",
|
|
514
|
+
async beforeExecute(plan, ctx) {
|
|
515
|
+
if (plan.ast) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const evaluation = evaluateRawGuardrails(plan);
|
|
519
|
+
for (const lint of evaluation.lints) {
|
|
520
|
+
const configuredSeverity = getConfiguredSeverity(lint.code, options);
|
|
521
|
+
const effectiveSeverity = configuredSeverity ?? lint.severity;
|
|
522
|
+
if (effectiveSeverity === "error") {
|
|
523
|
+
throw lintError(lint.code, lint.message, lint.details);
|
|
524
|
+
}
|
|
525
|
+
if (effectiveSeverity === "warn") {
|
|
526
|
+
ctx.log.warn({
|
|
527
|
+
code: lint.code,
|
|
528
|
+
message: lint.message,
|
|
529
|
+
details: lint.details
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function getConfiguredSeverity(code, options) {
|
|
537
|
+
const severities = options?.severities;
|
|
538
|
+
if (!severities) {
|
|
539
|
+
return void 0;
|
|
540
|
+
}
|
|
541
|
+
if (code === "LINT.SELECT_STAR") {
|
|
542
|
+
return severities.selectStar;
|
|
543
|
+
}
|
|
544
|
+
if (code === "LINT.NO_LIMIT") {
|
|
545
|
+
return severities.noLimit;
|
|
546
|
+
}
|
|
547
|
+
if (code === "LINT.READ_ONLY_MUTATION") {
|
|
548
|
+
return severities.readOnlyMutation;
|
|
549
|
+
}
|
|
550
|
+
if (code === "LINT.UNINDEXED_PREDICATE") {
|
|
551
|
+
return severities.unindexedPredicate;
|
|
552
|
+
}
|
|
553
|
+
return void 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/runtime-core.ts
|
|
557
|
+
var RuntimeCoreImpl = class {
|
|
558
|
+
_typeContract;
|
|
559
|
+
_typeAdapter;
|
|
560
|
+
_typeDriver;
|
|
561
|
+
contract;
|
|
562
|
+
familyAdapter;
|
|
563
|
+
driver;
|
|
564
|
+
plugins;
|
|
565
|
+
mode;
|
|
566
|
+
verify;
|
|
567
|
+
operationRegistry;
|
|
568
|
+
pluginContext;
|
|
569
|
+
verified;
|
|
570
|
+
startupVerified;
|
|
571
|
+
_telemetry;
|
|
572
|
+
constructor(options) {
|
|
573
|
+
const { familyAdapter, driver } = options;
|
|
574
|
+
this.contract = familyAdapter.contract;
|
|
575
|
+
this.familyAdapter = familyAdapter;
|
|
576
|
+
this.driver = driver;
|
|
577
|
+
this.plugins = options.plugins ?? [];
|
|
578
|
+
this.mode = options.mode ?? "strict";
|
|
579
|
+
this.verify = options.verify;
|
|
580
|
+
this.operationRegistry = options.operationRegistry;
|
|
581
|
+
this.verified = options.verify.mode === "startup" ? false : options.verify.mode === "always";
|
|
582
|
+
this.startupVerified = false;
|
|
583
|
+
this._telemetry = null;
|
|
584
|
+
this.pluginContext = {
|
|
585
|
+
contract: this.contract,
|
|
586
|
+
adapter: options.familyAdapter,
|
|
587
|
+
driver: this.driver,
|
|
588
|
+
mode: this.mode,
|
|
589
|
+
now: () => Date.now(),
|
|
590
|
+
log: options.log ?? {
|
|
591
|
+
info: () => {
|
|
592
|
+
},
|
|
593
|
+
warn: () => {
|
|
594
|
+
},
|
|
595
|
+
error: () => {
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
async verifyPlanIfNeeded(_plan) {
|
|
601
|
+
void _plan;
|
|
602
|
+
if (this.verify.mode === "always") {
|
|
603
|
+
this.verified = false;
|
|
604
|
+
}
|
|
605
|
+
if (this.verified) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
|
|
609
|
+
const driver = this.driver;
|
|
610
|
+
const result = await driver.query(readStatement.sql, readStatement.params);
|
|
611
|
+
if (result.rows.length === 0) {
|
|
612
|
+
if (this.verify.requireMarker) {
|
|
613
|
+
throw runtimeError("CONTRACT.MARKER_MISSING", "Contract marker not found in database");
|
|
614
|
+
}
|
|
615
|
+
this.verified = true;
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const marker = parseContractMarkerRow(result.rows[0]);
|
|
619
|
+
const contract = this.contract;
|
|
620
|
+
if (marker.coreHash !== contract.coreHash) {
|
|
621
|
+
throw runtimeError("CONTRACT.MARKER_MISMATCH", "Database core hash does not match contract", {
|
|
622
|
+
expected: contract.coreHash,
|
|
623
|
+
actual: marker.coreHash
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
const expectedProfile = contract.profileHash ?? null;
|
|
627
|
+
if (expectedProfile !== null && marker.profileHash !== expectedProfile) {
|
|
628
|
+
throw runtimeError(
|
|
629
|
+
"CONTRACT.MARKER_MISMATCH",
|
|
630
|
+
"Database profile hash does not match contract",
|
|
631
|
+
{
|
|
632
|
+
expectedProfile,
|
|
633
|
+
actualProfile: marker.profileHash
|
|
634
|
+
}
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
this.verified = true;
|
|
638
|
+
this.startupVerified = true;
|
|
639
|
+
}
|
|
640
|
+
validatePlan(plan) {
|
|
641
|
+
this.familyAdapter.validatePlan(plan, this.contract);
|
|
642
|
+
}
|
|
643
|
+
recordTelemetry(plan, outcome, durationMs) {
|
|
644
|
+
const contract = this.contract;
|
|
645
|
+
this._telemetry = Object.freeze({
|
|
646
|
+
lane: plan.meta.lane,
|
|
647
|
+
target: contract.target,
|
|
648
|
+
fingerprint: computeSqlFingerprint(plan.sql),
|
|
649
|
+
outcome,
|
|
650
|
+
...durationMs !== void 0 ? { durationMs } : {}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
execute(plan) {
|
|
654
|
+
this.validatePlan(plan);
|
|
655
|
+
this._telemetry = null;
|
|
656
|
+
const iterator = async function* (self) {
|
|
657
|
+
const startedAt = Date.now();
|
|
658
|
+
let rowCount = 0;
|
|
659
|
+
let completed = false;
|
|
660
|
+
if (!self.startupVerified && self.verify.mode === "startup") {
|
|
661
|
+
await self.verifyPlanIfNeeded(plan);
|
|
662
|
+
}
|
|
663
|
+
if (self.verify.mode === "onFirstUse") {
|
|
664
|
+
await self.verifyPlanIfNeeded(plan);
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
if (self.verify.mode === "always") {
|
|
668
|
+
await self.verifyPlanIfNeeded(plan);
|
|
669
|
+
}
|
|
670
|
+
for (const plugin of self.plugins) {
|
|
671
|
+
if (plugin.beforeExecute) {
|
|
672
|
+
await plugin.beforeExecute(plan, self.pluginContext);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const driver = self.driver;
|
|
676
|
+
const encodedParams = plan.params;
|
|
677
|
+
for await (const row of driver.execute({
|
|
678
|
+
sql: plan.sql,
|
|
679
|
+
params: encodedParams
|
|
680
|
+
})) {
|
|
681
|
+
for (const plugin of self.plugins) {
|
|
682
|
+
if (plugin.onRow) {
|
|
683
|
+
await plugin.onRow(row, plan, self.pluginContext);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
rowCount++;
|
|
687
|
+
yield row;
|
|
688
|
+
}
|
|
689
|
+
completed = true;
|
|
690
|
+
self.recordTelemetry(plan, "success", Date.now() - startedAt);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (self._telemetry === null) {
|
|
693
|
+
self.recordTelemetry(plan, "runtime-error", Date.now() - startedAt);
|
|
694
|
+
}
|
|
695
|
+
const latencyMs2 = Date.now() - startedAt;
|
|
696
|
+
for (const plugin of self.plugins) {
|
|
697
|
+
if (plugin.afterExecute) {
|
|
698
|
+
try {
|
|
699
|
+
await plugin.afterExecute(
|
|
700
|
+
plan,
|
|
701
|
+
{ rowCount, latencyMs: latencyMs2, completed },
|
|
702
|
+
self.pluginContext
|
|
703
|
+
);
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
const latencyMs = Date.now() - startedAt;
|
|
711
|
+
for (const plugin of self.plugins) {
|
|
712
|
+
if (plugin.afterExecute) {
|
|
713
|
+
await plugin.afterExecute(plan, { rowCount, latencyMs, completed }, self.pluginContext);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
return new AsyncIterableResult(iterator(this));
|
|
718
|
+
}
|
|
719
|
+
telemetry() {
|
|
720
|
+
return this._telemetry;
|
|
721
|
+
}
|
|
722
|
+
operations() {
|
|
723
|
+
return this.operationRegistry;
|
|
724
|
+
}
|
|
725
|
+
close() {
|
|
726
|
+
const driver = this.driver;
|
|
727
|
+
if (typeof driver.close === "function") {
|
|
728
|
+
return driver.close();
|
|
729
|
+
}
|
|
730
|
+
return Promise.resolve();
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
function createRuntimeCore(options) {
|
|
734
|
+
return new RuntimeCoreImpl(options);
|
|
735
|
+
}
|
|
736
|
+
export {
|
|
737
|
+
AsyncIterableResult,
|
|
738
|
+
budgets,
|
|
739
|
+
computeSqlFingerprint,
|
|
740
|
+
createRuntimeCore,
|
|
741
|
+
evaluateRawGuardrails,
|
|
742
|
+
lints,
|
|
743
|
+
parseContractMarkerRow,
|
|
744
|
+
runtimeError
|
|
745
|
+
};
|
|
746
|
+
//# sourceMappingURL=index.js.map
|