@prisma-next/sql-runtime 0.5.0-dev.3 → 0.5.0-dev.31
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/README.md +29 -21
- package/dist/exports-CrHMfIKo.mjs +1564 -0
- package/dist/exports-CrHMfIKo.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-_dXSGeho.d.mts} +78 -25
- package/dist/index-_dXSGeho.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/test/utils.d.mts +6 -5
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +11 -5
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +11 -13
- package/src/codecs/decoding.ts +294 -173
- package/src/codecs/encoding.ts +162 -37
- package/src/codecs/validation.ts +22 -3
- package/src/exports/index.ts +11 -7
- package/src/fingerprint.ts +22 -0
- package/src/guardrails/raw.ts +165 -0
- package/src/lower-sql-plan.ts +3 -3
- package/src/marker.ts +75 -0
- package/src/middleware/before-compile-chain.ts +1 -0
- package/src/middleware/budgets.ts +26 -96
- package/src/middleware/lints.ts +3 -3
- package/src/middleware/sql-middleware.ts +6 -5
- package/src/runtime-spi.ts +44 -0
- package/src/sql-context.ts +332 -78
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +62 -47
- package/src/sql-runtime.ts +332 -113
- package/dist/exports-BQZSVXXt.mjs +0 -981
- package/dist/exports-BQZSVXXt.mjs.map +0 -1
- package/dist/index-yb51L_1h.d.mts.map +0 -1
- package/test/async-iterable-result.test.ts +0 -141
- package/test/before-compile-chain.test.ts +0 -223
- package/test/budgets.test.ts +0 -431
- package/test/context.types.test-d.ts +0 -68
- package/test/execution-stack.test.ts +0 -161
- package/test/json-schema-validation.test.ts +0 -571
- package/test/lints.test.ts +0 -160
- package/test/mutation-default-generators.test.ts +0 -254
- package/test/parameterized-types.test.ts +0 -529
- package/test/sql-context.test.ts +0 -384
- package/test/sql-family-adapter.test.ts +0 -103
- package/test/sql-runtime.test.ts +0 -792
- package/test/utils.ts +0 -297
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
type AfterExecuteResult,
|
|
3
|
+
type RuntimeErrorEnvelope,
|
|
4
|
+
runtimeError,
|
|
5
|
+
} from '@prisma-next/framework-components/runtime';
|
|
4
6
|
import { isQueryAst, type SelectAst } from '@prisma-next/sql-relational-core/ast';
|
|
7
|
+
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
5
8
|
import type { SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
|
|
6
9
|
|
|
7
10
|
export interface BudgetsOptions {
|
|
@@ -22,18 +25,28 @@ function hasAggregateWithoutGroupBy(ast: SelectAst): boolean {
|
|
|
22
25
|
return ast.projection.some((item) => item.expr.kind === 'aggregate');
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
function primaryTableFromAst(ast: SelectAst): string | undefined {
|
|
29
|
+
switch (ast.from.kind) {
|
|
30
|
+
case 'table-source':
|
|
31
|
+
return ast.from.name;
|
|
32
|
+
case 'derived-table-source':
|
|
33
|
+
return ast.from.alias;
|
|
34
|
+
default:
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
25
39
|
function estimateRowsFromAst(
|
|
26
40
|
ast: SelectAst,
|
|
27
41
|
tableRows: Record<string, number>,
|
|
28
42
|
defaultTableRows: number,
|
|
29
|
-
refs: { tables?: readonly string[] } | undefined,
|
|
30
43
|
hasAggregateWithoutGroup: boolean,
|
|
31
44
|
): number | null {
|
|
32
45
|
if (hasAggregateWithoutGroup) {
|
|
33
46
|
return 1;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
const table =
|
|
49
|
+
const table = primaryTableFromAst(ast);
|
|
37
50
|
if (!table) {
|
|
38
51
|
return null;
|
|
39
52
|
}
|
|
@@ -47,30 +60,6 @@ function estimateRowsFromAst(
|
|
|
47
60
|
return tableEstimate;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
|
-
function estimateRowsFromHeuristics(
|
|
51
|
-
plan: ExecutionPlan,
|
|
52
|
-
tableRows: Record<string, number>,
|
|
53
|
-
defaultTableRows: number,
|
|
54
|
-
): number | null {
|
|
55
|
-
const table = plan.meta.refs?.tables?.[0];
|
|
56
|
-
if (!table) {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const tableEstimate = tableRows[table] ?? defaultTableRows;
|
|
61
|
-
|
|
62
|
-
const limit = plan.meta.annotations?.['limit'];
|
|
63
|
-
if (typeof limit === 'number') {
|
|
64
|
-
return Math.min(limit, tableEstimate);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return tableEstimate;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function hasDetectableLimitFromHeuristics(plan: ExecutionPlan): boolean {
|
|
71
|
-
return typeof plan.meta.annotations?.['limit'] === 'number';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
63
|
function emitBudgetViolation(
|
|
75
64
|
error: RuntimeErrorEnvelope,
|
|
76
65
|
shouldBlock: boolean,
|
|
@@ -93,26 +82,21 @@ export function budgets(options?: BudgetsOptions): SqlMiddleware {
|
|
|
93
82
|
const maxLatencyMs = options?.maxLatencyMs ?? 1_000;
|
|
94
83
|
const rowSeverity = options?.severities?.rowCount ?? 'error';
|
|
95
84
|
|
|
96
|
-
const observedRowsByPlan = new WeakMap<
|
|
85
|
+
const observedRowsByPlan = new WeakMap<SqlExecutionPlan, { count: number }>();
|
|
97
86
|
|
|
98
87
|
return Object.freeze({
|
|
99
88
|
name: 'budgets',
|
|
100
89
|
familyId: 'sql' as const,
|
|
101
90
|
|
|
102
|
-
async beforeExecute(plan:
|
|
91
|
+
async beforeExecute(plan: SqlExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
103
92
|
observedRowsByPlan.set(plan, { count: 0 });
|
|
104
93
|
|
|
105
|
-
if (isQueryAst(plan.ast)) {
|
|
106
|
-
|
|
107
|
-
return evaluateSelectAst(plan, plan.ast, ctx);
|
|
108
|
-
}
|
|
109
|
-
return;
|
|
94
|
+
if (isQueryAst(plan.ast) && plan.ast.kind === 'select') {
|
|
95
|
+
return evaluateSelectAst(plan.ast, ctx);
|
|
110
96
|
}
|
|
111
|
-
|
|
112
|
-
return evaluateWithHeuristics(plan, ctx);
|
|
113
97
|
},
|
|
114
98
|
|
|
115
|
-
async onRow(_row: Record<string, unknown>, plan:
|
|
99
|
+
async onRow(_row: Record<string, unknown>, plan: SqlExecutionPlan, _ctx: SqlMiddlewareContext) {
|
|
116
100
|
const state = observedRowsByPlan.get(plan);
|
|
117
101
|
if (!state) return;
|
|
118
102
|
state.count += 1;
|
|
@@ -126,7 +110,7 @@ export function budgets(options?: BudgetsOptions): SqlMiddleware {
|
|
|
126
110
|
},
|
|
127
111
|
|
|
128
112
|
async afterExecute(
|
|
129
|
-
_plan:
|
|
113
|
+
_plan: SqlExecutionPlan,
|
|
130
114
|
result: AfterExecuteResult,
|
|
131
115
|
ctx: SqlMiddlewareContext,
|
|
132
116
|
) {
|
|
@@ -145,15 +129,9 @@ export function budgets(options?: BudgetsOptions): SqlMiddleware {
|
|
|
145
129
|
},
|
|
146
130
|
});
|
|
147
131
|
|
|
148
|
-
function evaluateSelectAst(
|
|
132
|
+
function evaluateSelectAst(ast: SelectAst, ctx: SqlMiddlewareContext) {
|
|
149
133
|
const hasAggNoGroup = hasAggregateWithoutGroupBy(ast);
|
|
150
|
-
const estimated = estimateRowsFromAst(
|
|
151
|
-
ast,
|
|
152
|
-
tableRows,
|
|
153
|
-
defaultTableRows,
|
|
154
|
-
plan.meta.refs,
|
|
155
|
-
hasAggNoGroup,
|
|
156
|
-
);
|
|
134
|
+
const estimated = estimateRowsFromAst(ast, tableRows, defaultTableRows, hasAggNoGroup);
|
|
157
135
|
const isUnbounded = ast.limit === undefined && !hasAggNoGroup;
|
|
158
136
|
const shouldBlock = rowSeverity === 'error' || ctx.mode === 'strict';
|
|
159
137
|
|
|
@@ -194,52 +172,4 @@ export function budgets(options?: BudgetsOptions): SqlMiddleware {
|
|
|
194
172
|
);
|
|
195
173
|
}
|
|
196
174
|
}
|
|
197
|
-
|
|
198
|
-
async function evaluateWithHeuristics(plan: ExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
199
|
-
const estimated = estimateRowsFromHeuristics(plan, tableRows, defaultTableRows);
|
|
200
|
-
const isUnbounded = !hasDetectableLimitFromHeuristics(plan);
|
|
201
|
-
const sqlUpper = plan.sql.trimStart().toUpperCase();
|
|
202
|
-
const isSelect = sqlUpper.startsWith('SELECT');
|
|
203
|
-
const shouldBlock = rowSeverity === 'error' || ctx.mode === 'strict';
|
|
204
|
-
|
|
205
|
-
if (isSelect && isUnbounded) {
|
|
206
|
-
if (estimated !== null && estimated >= maxRows) {
|
|
207
|
-
emitBudgetViolation(
|
|
208
|
-
runtimeError('BUDGET.ROWS_EXCEEDED', 'Unbounded SELECT query exceeds budget', {
|
|
209
|
-
source: 'heuristic',
|
|
210
|
-
estimatedRows: estimated,
|
|
211
|
-
maxRows,
|
|
212
|
-
}),
|
|
213
|
-
shouldBlock,
|
|
214
|
-
ctx,
|
|
215
|
-
);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
emitBudgetViolation(
|
|
220
|
-
runtimeError('BUDGET.ROWS_EXCEEDED', 'Unbounded SELECT query exceeds budget', {
|
|
221
|
-
source: 'heuristic',
|
|
222
|
-
maxRows,
|
|
223
|
-
}),
|
|
224
|
-
shouldBlock,
|
|
225
|
-
ctx,
|
|
226
|
-
);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (estimated !== null) {
|
|
231
|
-
if (estimated > maxRows) {
|
|
232
|
-
emitBudgetViolation(
|
|
233
|
-
runtimeError('BUDGET.ROWS_EXCEEDED', 'Estimated row count exceeds budget', {
|
|
234
|
-
source: 'heuristic',
|
|
235
|
-
estimatedRows: estimated,
|
|
236
|
-
maxRows,
|
|
237
|
-
}),
|
|
238
|
-
shouldBlock,
|
|
239
|
-
ctx,
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
175
|
}
|
package/src/middleware/lints.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { ExecutionPlan } from '@prisma-next/contract/types';
|
|
2
1
|
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
3
|
-
import { evaluateRawGuardrails } from '@prisma-next/runtime-executor';
|
|
4
2
|
import {
|
|
5
3
|
type AnyFromSource,
|
|
6
4
|
type AnyQueryAst,
|
|
7
5
|
isQueryAst,
|
|
8
6
|
} from '@prisma-next/sql-relational-core/ast';
|
|
7
|
+
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
9
8
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
9
|
+
import { evaluateRawGuardrails } from '../guardrails/raw';
|
|
10
10
|
import type { SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
|
|
11
11
|
|
|
12
12
|
export interface LintsOptions {
|
|
@@ -145,7 +145,7 @@ export function lints(options?: LintsOptions): SqlMiddleware {
|
|
|
145
145
|
name: 'lints',
|
|
146
146
|
familyId: 'sql' as const,
|
|
147
147
|
|
|
148
|
-
async beforeExecute(plan:
|
|
148
|
+
async beforeExecute(plan: SqlExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
149
149
|
if (isQueryAst(plan.ast)) {
|
|
150
150
|
const findings = evaluateAstLints(plan.ast);
|
|
151
151
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Contract,
|
|
1
|
+
import type { Contract, PlanMeta } from '@prisma-next/contract/types';
|
|
2
2
|
import type {
|
|
3
3
|
AfterExecuteResult,
|
|
4
4
|
RuntimeMiddleware,
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from '@prisma-next/framework-components/runtime';
|
|
7
7
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
8
8
|
import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';
|
|
9
|
+
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
9
10
|
|
|
10
11
|
export interface SqlMiddlewareContext extends RuntimeMiddlewareContext {
|
|
11
12
|
readonly contract: Contract<SqlStorage>;
|
|
@@ -20,7 +21,7 @@ export interface DraftPlan {
|
|
|
20
21
|
readonly meta: PlanMeta;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export interface SqlMiddleware extends RuntimeMiddleware {
|
|
24
|
+
export interface SqlMiddleware extends RuntimeMiddleware<SqlExecutionPlan> {
|
|
24
25
|
readonly familyId?: 'sql';
|
|
25
26
|
/**
|
|
26
27
|
* Rewrite the query AST before it is lowered to SQL. Middlewares run in
|
|
@@ -41,14 +42,14 @@ export interface SqlMiddleware extends RuntimeMiddleware {
|
|
|
41
42
|
* See `docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md`.
|
|
42
43
|
*/
|
|
43
44
|
beforeCompile?(draft: DraftPlan, ctx: SqlMiddlewareContext): Promise<DraftPlan | undefined>;
|
|
44
|
-
beforeExecute?(plan:
|
|
45
|
+
beforeExecute?(plan: SqlExecutionPlan, ctx: SqlMiddlewareContext): Promise<void>;
|
|
45
46
|
onRow?(
|
|
46
47
|
row: Record<string, unknown>,
|
|
47
|
-
plan:
|
|
48
|
+
plan: SqlExecutionPlan,
|
|
48
49
|
ctx: SqlMiddlewareContext,
|
|
49
50
|
): Promise<void>;
|
|
50
51
|
afterExecute?(
|
|
51
|
-
plan:
|
|
52
|
+
plan: SqlExecutionPlan,
|
|
52
53
|
result: AfterExecuteResult,
|
|
53
54
|
ctx: SqlMiddlewareContext,
|
|
54
55
|
): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ContractMarkerRecord } from '@prisma-next/contract/types';
|
|
2
|
+
import type { ExecutionPlan } from '@prisma-next/framework-components/runtime';
|
|
3
|
+
import type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reader of the SQL contract marker. SQL runtimes verify the database's
|
|
7
|
+
* `prisma_contract.marker` row against the runtime's contract by issuing
|
|
8
|
+
* this statement before executing user queries (when `verify` is enabled).
|
|
9
|
+
* Each adapter is responsible for any target-specific row decoding before
|
|
10
|
+
* delegating to the shared row schema.
|
|
11
|
+
*/
|
|
12
|
+
export interface MarkerReader {
|
|
13
|
+
readMarkerStatement(): MarkerStatement;
|
|
14
|
+
parseMarkerRow(row: unknown): ContractMarkerRecord;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* SQL family adapter SPI consumed by `SqlRuntime`. Encapsulates the
|
|
19
|
+
* runtime contract, marker reader, and plan validation logic so the
|
|
20
|
+
* runtime can be unit-tested without a concrete SQL adapter profile.
|
|
21
|
+
*
|
|
22
|
+
* Implemented by `SqlFamilyAdapter` for production and by mock classes
|
|
23
|
+
* in tests.
|
|
24
|
+
*/
|
|
25
|
+
export interface RuntimeFamilyAdapter<TContract = unknown> {
|
|
26
|
+
readonly contract: TContract;
|
|
27
|
+
readonly markerReader: MarkerReader;
|
|
28
|
+
validatePlan(plan: ExecutionPlan, contract: TContract): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RuntimeVerifyOptions {
|
|
32
|
+
readonly mode: 'onFirstUse' | 'startup' | 'always';
|
|
33
|
+
readonly requireMarker: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type TelemetryOutcome = 'success' | 'runtime-error';
|
|
37
|
+
|
|
38
|
+
export interface RuntimeTelemetryEvent {
|
|
39
|
+
readonly lane: string;
|
|
40
|
+
readonly target: string;
|
|
41
|
+
readonly fingerprint: string;
|
|
42
|
+
readonly outcome: TelemetryOutcome;
|
|
43
|
+
readonly durationMs?: number;
|
|
44
|
+
}
|