@prisma-next/sql-runtime 0.5.0-dev.9 → 0.5.0
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 +2 -1
- package/dist/exports-BSTHn_rH.mjs +1516 -0
- package/dist/exports-BSTHn_rH.mjs.map +1 -0
- package/dist/{index-CZmC2kD3.d.mts → index-CTCvZOWI.d.mts} +87 -44
- package/dist/index-CTCvZOWI.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -3
- package/dist/test/utils.d.mts +33 -29
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +104 -64
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +17 -16
- package/src/codecs/alias-resolver.ts +37 -0
- package/src/codecs/decoding.ts +163 -129
- package/src/codecs/encoding.ts +121 -46
- package/src/codecs/validation.ts +4 -4
- package/src/content-hash.ts +44 -0
- package/src/exports/index.ts +4 -1
- package/src/guardrails/raw.ts +1 -50
- package/src/marker.ts +13 -20
- package/src/middleware/before-compile-chain.ts +1 -31
- package/src/middleware/budgets.ts +26 -113
- package/src/middleware/lints.ts +17 -23
- package/src/middleware/sql-middleware.ts +21 -2
- package/src/runtime-spi.ts +3 -8
- package/src/sql-context.ts +320 -109
- package/src/sql-marker.ts +88 -50
- package/src/sql-runtime.ts +108 -90
- package/dist/exports-BOHa3Emo.mjs +0 -1334
- package/dist/exports-BOHa3Emo.mjs.map +0 -1
- package/dist/index-CZmC2kD3.d.mts.map +0 -1
- package/src/codecs/json-schema-validation.ts +0 -61
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
2
|
+
import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
|
|
3
|
+
import { hashContent } from '@prisma-next/utils/hash-content';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Computes a stable content hash for a lowered SQL execution plan.
|
|
7
|
+
*
|
|
8
|
+
* Internally builds an unambiguous canonical-stringified preimage from
|
|
9
|
+
* three components:
|
|
10
|
+
*
|
|
11
|
+
* 1. `meta.storageHash` — discriminates by schema. A migration changes the
|
|
12
|
+
* storage hash, which invalidates cached entries automatically.
|
|
13
|
+
* 2. `exec.sql` — the raw lowered SQL text. Two queries with different
|
|
14
|
+
* structure produce different keys. Note that we deliberately do **not**
|
|
15
|
+
* use `computeSqlFingerprint` here: that helper strips literals to group
|
|
16
|
+
* executions by statement shape (used by telemetry), which is the
|
|
17
|
+
* opposite of what a content hash needs — we want per-execution
|
|
18
|
+
* discrimination, not per-statement-shape grouping.
|
|
19
|
+
* 3. `exec.params` — the bound parameters. `canonicalStringify` produces a
|
|
20
|
+
* deterministic serialization that is stable across object key
|
|
21
|
+
* insertion order and that distinguishes types JSON would otherwise
|
|
22
|
+
* conflate (e.g. `BigInt(1)` vs `1`).
|
|
23
|
+
*
|
|
24
|
+
* The components are wrapped in an object and canonicalized as a single
|
|
25
|
+
* unit (rather than concatenated with a delimiter) so component
|
|
26
|
+
* boundaries are unambiguous: any character appearing inside `sql` or
|
|
27
|
+
* `storageHash` cannot bleed across components and produce a collision
|
|
28
|
+
* with a different split of the same characters.
|
|
29
|
+
*
|
|
30
|
+
* The canonical string is then piped through `hashContent` to produce a
|
|
31
|
+
* bounded, opaque digest. See `@prisma-next/utils/hash-content` for the
|
|
32
|
+
* rationale.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export function computeSqlContentHash(exec: SqlExecutionPlan): Promise<string> {
|
|
37
|
+
return hashContent(
|
|
38
|
+
canonicalStringify({
|
|
39
|
+
storageHash: exec.meta.storageHash,
|
|
40
|
+
sql: exec.sql,
|
|
41
|
+
params: exec.params,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
}
|
package/src/exports/index.ts
CHANGED
|
@@ -2,13 +2,14 @@ export type {
|
|
|
2
2
|
AfterExecuteResult,
|
|
3
3
|
RuntimeLog as Log,
|
|
4
4
|
} from '@prisma-next/framework-components/runtime';
|
|
5
|
-
export type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
|
|
5
|
+
export type { MarkerReadResult, MarkerStatement } from '@prisma-next/sql-relational-core/ast';
|
|
6
6
|
export {
|
|
7
7
|
extractCodecIds,
|
|
8
8
|
validateCodecRegistryCompleteness,
|
|
9
9
|
validateContractCodecMappings,
|
|
10
10
|
} from '../codecs/validation';
|
|
11
11
|
export { lowerSqlPlan } from '../lower-sql-plan';
|
|
12
|
+
export { parseContractMarkerRow } from '../marker';
|
|
12
13
|
export type { BudgetsOptions } from '../middleware/budgets';
|
|
13
14
|
export { budgets } from '../middleware/budgets';
|
|
14
15
|
export type { LintsOptions } from '../middleware/lints';
|
|
@@ -23,6 +24,7 @@ export type {
|
|
|
23
24
|
} from '../runtime-spi';
|
|
24
25
|
export type {
|
|
25
26
|
ExecutionContext,
|
|
27
|
+
GeneratorStability,
|
|
26
28
|
RuntimeMutationDefaultGenerator,
|
|
27
29
|
RuntimeParameterizedCodecDescriptor,
|
|
28
30
|
SqlExecutionStack,
|
|
@@ -42,6 +44,7 @@ export {
|
|
|
42
44
|
} from '../sql-context';
|
|
43
45
|
export type { SqlStatement } from '../sql-marker';
|
|
44
46
|
export {
|
|
47
|
+
APP_SPACE_ID,
|
|
45
48
|
ensureSchemaStatement,
|
|
46
49
|
ensureTableStatement,
|
|
47
50
|
readContractMarker,
|
package/src/guardrails/raw.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PlanMeta
|
|
1
|
+
import type { PlanMeta } from '@prisma-next/contract/types';
|
|
2
2
|
|
|
3
3
|
export type LintSeverity = 'error' | 'warn';
|
|
4
4
|
export type BudgetSeverity = 'error' | 'warn';
|
|
@@ -103,58 +103,9 @@ export function evaluateRawGuardrails(
|
|
|
103
103
|
);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const refs = plan.meta.refs;
|
|
107
|
-
if (refs) {
|
|
108
|
-
evaluateIndexCoverage(refs, lints);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
106
|
return { lints, budgets, statement: statementType };
|
|
112
107
|
}
|
|
113
108
|
|
|
114
|
-
function evaluateIndexCoverage(refs: PlanRefs, lints: LintFinding[]) {
|
|
115
|
-
const predicateColumns = refs.columns ?? [];
|
|
116
|
-
if (predicateColumns.length === 0) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const indexes = refs.indexes ?? [];
|
|
121
|
-
|
|
122
|
-
if (indexes.length === 0) {
|
|
123
|
-
lints.push(
|
|
124
|
-
createLint(
|
|
125
|
-
'LINT.UNINDEXED_PREDICATE',
|
|
126
|
-
'warn',
|
|
127
|
-
'Raw SQL plan predicates lack supporting indexes',
|
|
128
|
-
{
|
|
129
|
-
predicates: predicateColumns,
|
|
130
|
-
},
|
|
131
|
-
),
|
|
132
|
-
);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const hasSupportingIndex = predicateColumns.every((column) =>
|
|
137
|
-
indexes.some(
|
|
138
|
-
(index) =>
|
|
139
|
-
index.table === column.table &&
|
|
140
|
-
index.columns.some((col) => col.toLowerCase() === column.column.toLowerCase()),
|
|
141
|
-
),
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
if (!hasSupportingIndex) {
|
|
145
|
-
lints.push(
|
|
146
|
-
createLint(
|
|
147
|
-
'LINT.UNINDEXED_PREDICATE',
|
|
148
|
-
'warn',
|
|
149
|
-
'Raw SQL plan predicates lack supporting indexes',
|
|
150
|
-
{
|
|
151
|
-
predicates: predicateColumns,
|
|
152
|
-
},
|
|
153
|
-
),
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
109
|
function classifyStatement(sql: string): 'select' | 'mutation' | 'other' {
|
|
159
110
|
const trimmed = sql.trim();
|
|
160
111
|
const lower = trimmed.toLowerCase();
|
package/src/marker.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface ContractMarkerRow {
|
|
|
9
9
|
updated_at: Date;
|
|
10
10
|
app_tag: string | null;
|
|
11
11
|
meta: unknown | null;
|
|
12
|
+
invariants: readonly string[];
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const MetaSchema = type({ '[string]': 'unknown' });
|
|
@@ -45,6 +46,7 @@ const ContractMarkerRowSchema = type({
|
|
|
45
46
|
'updated_at?': 'Date | string',
|
|
46
47
|
'app_tag?': 'string | null',
|
|
47
48
|
'meta?': 'unknown | null',
|
|
49
|
+
invariants: type('string').array(),
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
export function parseContractMarkerRow(row: unknown): ContractMarkerRecord {
|
|
@@ -54,29 +56,20 @@ export function parseContractMarkerRow(row: unknown): ContractMarkerRecord {
|
|
|
54
56
|
throw new Error(`Invalid contract marker row: ${messages}`);
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
canonical_version?: number | null;
|
|
62
|
-
updated_at?: Date | string;
|
|
63
|
-
app_tag?: string | null;
|
|
64
|
-
meta?: unknown | null;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const updatedAt = validatedRow.updated_at
|
|
68
|
-
? validatedRow.updated_at instanceof Date
|
|
69
|
-
? validatedRow.updated_at
|
|
70
|
-
: new Date(validatedRow.updated_at)
|
|
59
|
+
const updatedAt = result.updated_at
|
|
60
|
+
? result.updated_at instanceof Date
|
|
61
|
+
? result.updated_at
|
|
62
|
+
: new Date(result.updated_at)
|
|
71
63
|
: new Date();
|
|
72
64
|
|
|
73
65
|
return {
|
|
74
|
-
storageHash:
|
|
75
|
-
profileHash:
|
|
76
|
-
contractJson:
|
|
77
|
-
canonicalVersion:
|
|
66
|
+
storageHash: result.core_hash,
|
|
67
|
+
profileHash: result.profile_hash,
|
|
68
|
+
contractJson: result.contract_json ?? null,
|
|
69
|
+
canonicalVersion: result.canonical_version ?? null,
|
|
78
70
|
updatedAt,
|
|
79
|
-
appTag:
|
|
80
|
-
meta: parseMeta(
|
|
71
|
+
appTag: result.app_tag ?? null,
|
|
72
|
+
meta: parseMeta(result.meta),
|
|
73
|
+
invariants: result.invariants,
|
|
81
74
|
};
|
|
82
75
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { ParamDescriptor, PlanMeta } from '@prisma-next/contract/types';
|
|
2
|
-
import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';
|
|
3
1
|
import type { DraftPlan, SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
|
|
4
2
|
|
|
5
3
|
export async function runBeforeCompileChain(
|
|
@@ -27,33 +25,5 @@ export async function runBeforeCompileChain(
|
|
|
27
25
|
current = result;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
return current;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// The rewritten AST may have introduced, removed, or replaced ParamRefs, so
|
|
35
|
-
// the descriptors collected at lane build time no longer line up with what
|
|
36
|
-
// the adapter will emit when it walks the new AST. Re-derive descriptors
|
|
37
|
-
// from the rewritten AST so `params` and `paramDescriptors` stay in lockstep
|
|
38
|
-
// by the time `encodeParams` runs.
|
|
39
|
-
const paramDescriptors = deriveParamDescriptorsFromAst(current.ast);
|
|
40
|
-
const meta: PlanMeta = { ...current.meta, paramDescriptors };
|
|
41
|
-
return { ast: current.ast, meta };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function deriveParamDescriptorsFromAst(ast: AnyQueryAst): ReadonlyArray<ParamDescriptor> {
|
|
45
|
-
const refs = ast.collectParamRefs();
|
|
46
|
-
const seen = new Set<unknown>();
|
|
47
|
-
const descriptors: ParamDescriptor[] = [];
|
|
48
|
-
for (const ref of refs) {
|
|
49
|
-
if (seen.has(ref)) continue;
|
|
50
|
-
seen.add(ref);
|
|
51
|
-
descriptors.push({
|
|
52
|
-
index: descriptors.length + 1,
|
|
53
|
-
...(ref.name !== undefined ? { name: ref.name } : {}),
|
|
54
|
-
source: 'dsl',
|
|
55
|
-
...(ref.codecId !== undefined ? { codecId: ref.codecId } : {}),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
return descriptors;
|
|
28
|
+
return current;
|
|
59
29
|
}
|
|
@@ -25,23 +25,31 @@ function hasAggregateWithoutGroupBy(ast: SelectAst): boolean {
|
|
|
25
25
|
return ast.projection.some((item) => item.expr.kind === 'aggregate');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function primaryTableFromAst(ast: SelectAst): string {
|
|
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
|
+
// v8 ignore next 4
|
|
35
|
+
default:
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Unsupported source kind: ${(ast.from satisfies never as { kind: string }).kind}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
function estimateRowsFromAst(
|
|
29
43
|
ast: SelectAst,
|
|
30
44
|
tableRows: Record<string, number>,
|
|
31
45
|
defaultTableRows: number,
|
|
32
|
-
refs: { tables?: readonly string[] } | undefined,
|
|
33
46
|
hasAggregateWithoutGroup: boolean,
|
|
34
|
-
): number
|
|
47
|
+
): number {
|
|
35
48
|
if (hasAggregateWithoutGroup) {
|
|
36
49
|
return 1;
|
|
37
50
|
}
|
|
38
51
|
|
|
39
|
-
const
|
|
40
|
-
if (!table) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const tableEstimate = tableRows[table] ?? defaultTableRows;
|
|
52
|
+
const tableEstimate = tableRows[primaryTableFromAst(ast)] ?? defaultTableRows;
|
|
45
53
|
|
|
46
54
|
if (ast.limit !== undefined) {
|
|
47
55
|
return Math.min(ast.limit, tableEstimate);
|
|
@@ -50,30 +58,6 @@ function estimateRowsFromAst(
|
|
|
50
58
|
return tableEstimate;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
function estimateRowsFromHeuristics(
|
|
54
|
-
plan: SqlExecutionPlan,
|
|
55
|
-
tableRows: Record<string, number>,
|
|
56
|
-
defaultTableRows: number,
|
|
57
|
-
): number | null {
|
|
58
|
-
const table = plan.meta.refs?.tables?.[0];
|
|
59
|
-
if (!table) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const tableEstimate = tableRows[table] ?? defaultTableRows;
|
|
64
|
-
|
|
65
|
-
const limit = plan.meta.annotations?.['limit'];
|
|
66
|
-
if (typeof limit === 'number') {
|
|
67
|
-
return Math.min(limit, tableEstimate);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return tableEstimate;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function hasDetectableLimitFromHeuristics(plan: SqlExecutionPlan): boolean {
|
|
74
|
-
return typeof plan.meta.annotations?.['limit'] === 'number';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
61
|
function emitBudgetViolation(
|
|
78
62
|
error: RuntimeErrorEnvelope,
|
|
79
63
|
shouldBlock: boolean,
|
|
@@ -105,14 +89,9 @@ export function budgets(options?: BudgetsOptions): SqlMiddleware {
|
|
|
105
89
|
async beforeExecute(plan: SqlExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
106
90
|
observedRowsByPlan.set(plan, { count: 0 });
|
|
107
91
|
|
|
108
|
-
if (isQueryAst(plan.ast)) {
|
|
109
|
-
|
|
110
|
-
return evaluateSelectAst(plan, plan.ast, ctx);
|
|
111
|
-
}
|
|
112
|
-
return;
|
|
92
|
+
if (isQueryAst(plan.ast) && plan.ast.kind === 'select') {
|
|
93
|
+
return evaluateSelectAst(plan.ast, ctx);
|
|
113
94
|
}
|
|
114
|
-
|
|
115
|
-
return evaluateWithHeuristics(plan, ctx);
|
|
116
95
|
},
|
|
117
96
|
|
|
118
97
|
async onRow(_row: Record<string, unknown>, plan: SqlExecutionPlan, _ctx: SqlMiddlewareContext) {
|
|
@@ -148,44 +127,26 @@ export function budgets(options?: BudgetsOptions): SqlMiddleware {
|
|
|
148
127
|
},
|
|
149
128
|
});
|
|
150
129
|
|
|
151
|
-
function evaluateSelectAst(
|
|
130
|
+
function evaluateSelectAst(ast: SelectAst, ctx: SqlMiddlewareContext) {
|
|
152
131
|
const hasAggNoGroup = hasAggregateWithoutGroupBy(ast);
|
|
153
|
-
const estimated = estimateRowsFromAst(
|
|
154
|
-
ast,
|
|
155
|
-
tableRows,
|
|
156
|
-
defaultTableRows,
|
|
157
|
-
plan.meta.refs,
|
|
158
|
-
hasAggNoGroup,
|
|
159
|
-
);
|
|
132
|
+
const estimated = estimateRowsFromAst(ast, tableRows, defaultTableRows, hasAggNoGroup);
|
|
160
133
|
const isUnbounded = ast.limit === undefined && !hasAggNoGroup;
|
|
161
134
|
const shouldBlock = rowSeverity === 'error' || ctx.mode === 'strict';
|
|
162
135
|
|
|
163
136
|
if (isUnbounded) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
estimatedRows: estimated,
|
|
169
|
-
maxRows,
|
|
170
|
-
}),
|
|
171
|
-
shouldBlock,
|
|
172
|
-
ctx,
|
|
173
|
-
);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
137
|
+
const details =
|
|
138
|
+
estimated >= maxRows
|
|
139
|
+
? { source: 'ast', estimatedRows: estimated, maxRows }
|
|
140
|
+
: { source: 'ast', maxRows };
|
|
177
141
|
emitBudgetViolation(
|
|
178
|
-
runtimeError('BUDGET.ROWS_EXCEEDED', 'Unbounded SELECT query exceeds budget',
|
|
179
|
-
source: 'ast',
|
|
180
|
-
maxRows,
|
|
181
|
-
}),
|
|
142
|
+
runtimeError('BUDGET.ROWS_EXCEEDED', 'Unbounded SELECT query exceeds budget', details),
|
|
182
143
|
shouldBlock,
|
|
183
144
|
ctx,
|
|
184
145
|
);
|
|
185
146
|
return;
|
|
186
147
|
}
|
|
187
148
|
|
|
188
|
-
if (estimated
|
|
149
|
+
if (estimated > maxRows) {
|
|
189
150
|
emitBudgetViolation(
|
|
190
151
|
runtimeError('BUDGET.ROWS_EXCEEDED', 'Estimated row count exceeds budget', {
|
|
191
152
|
source: 'ast',
|
|
@@ -197,52 +158,4 @@ export function budgets(options?: BudgetsOptions): SqlMiddleware {
|
|
|
197
158
|
);
|
|
198
159
|
}
|
|
199
160
|
}
|
|
200
|
-
|
|
201
|
-
async function evaluateWithHeuristics(plan: SqlExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
202
|
-
const estimated = estimateRowsFromHeuristics(plan, tableRows, defaultTableRows);
|
|
203
|
-
const isUnbounded = !hasDetectableLimitFromHeuristics(plan);
|
|
204
|
-
const sqlUpper = plan.sql.trimStart().toUpperCase();
|
|
205
|
-
const isSelect = sqlUpper.startsWith('SELECT');
|
|
206
|
-
const shouldBlock = rowSeverity === 'error' || ctx.mode === 'strict';
|
|
207
|
-
|
|
208
|
-
if (isSelect && isUnbounded) {
|
|
209
|
-
if (estimated !== null && estimated >= maxRows) {
|
|
210
|
-
emitBudgetViolation(
|
|
211
|
-
runtimeError('BUDGET.ROWS_EXCEEDED', 'Unbounded SELECT query exceeds budget', {
|
|
212
|
-
source: 'heuristic',
|
|
213
|
-
estimatedRows: estimated,
|
|
214
|
-
maxRows,
|
|
215
|
-
}),
|
|
216
|
-
shouldBlock,
|
|
217
|
-
ctx,
|
|
218
|
-
);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
emitBudgetViolation(
|
|
223
|
-
runtimeError('BUDGET.ROWS_EXCEEDED', 'Unbounded SELECT query exceeds budget', {
|
|
224
|
-
source: 'heuristic',
|
|
225
|
-
maxRows,
|
|
226
|
-
}),
|
|
227
|
-
shouldBlock,
|
|
228
|
-
ctx,
|
|
229
|
-
);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (estimated !== null) {
|
|
234
|
-
if (estimated > maxRows) {
|
|
235
|
-
emitBudgetViolation(
|
|
236
|
-
runtimeError('BUDGET.ROWS_EXCEEDED', 'Estimated row count exceeds budget', {
|
|
237
|
-
source: 'heuristic',
|
|
238
|
-
estimatedRows: estimated,
|
|
239
|
-
maxRows,
|
|
240
|
-
}),
|
|
241
|
-
shouldBlock,
|
|
242
|
-
ctx,
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
161
|
}
|
package/src/middleware/lints.ts
CHANGED
|
@@ -94,6 +94,12 @@ function evaluateAstLints(ast: AnyQueryAst): LintFinding[] {
|
|
|
94
94
|
case 'insert':
|
|
95
95
|
break;
|
|
96
96
|
|
|
97
|
+
case 'raw-sql':
|
|
98
|
+
// Raw-SQL ASTs opt out of structural lints (LIMIT / WHERE etc.) —
|
|
99
|
+
// the embedded SQL fragments are caller-authored and the lint's
|
|
100
|
+
// shape-based heuristics don't apply.
|
|
101
|
+
break;
|
|
102
|
+
|
|
97
103
|
// v8 ignore next 2
|
|
98
104
|
default:
|
|
99
105
|
throw new Error(`Unsupported AST kind: ${(ast satisfies never as { kind: string }).kind}`);
|
|
@@ -146,33 +152,21 @@ export function lints(options?: LintsOptions): SqlMiddleware {
|
|
|
146
152
|
familyId: 'sql' as const,
|
|
147
153
|
|
|
148
154
|
async beforeExecute(plan: SqlExecutionPlan, ctx: SqlMiddlewareContext) {
|
|
155
|
+
const findings: LintFinding[] = [];
|
|
149
156
|
if (isQueryAst(plan.ast)) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
throw runtimeError(lint.code, lint.message, lint.details);
|
|
158
|
-
}
|
|
159
|
-
if (effectiveSeverity === 'warn') {
|
|
160
|
-
ctx.log.warn({
|
|
161
|
-
code: lint.code,
|
|
162
|
-
message: lint.message,
|
|
163
|
-
details: lint.details,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
157
|
+
findings.push(...evaluateAstLints(plan.ast));
|
|
158
|
+
// Raw-SQL ASTs opt out of structural AST lints (no LIMIT /
|
|
159
|
+
// WHERE shape to inspect) but the embedded SQL text still
|
|
160
|
+
// wants the raw-heuristic guardrails. Without this the lint
|
|
161
|
+
// middleware would silently disable both for raw plans.
|
|
162
|
+
if (plan.ast.kind === 'raw-sql') {
|
|
163
|
+
findings.push(...evaluateRawGuardrails(plan).lints);
|
|
166
164
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (fallback === 'skip') {
|
|
171
|
-
return;
|
|
165
|
+
} else if (fallback !== 'skip') {
|
|
166
|
+
findings.push(...evaluateRawGuardrails(plan).lints);
|
|
172
167
|
}
|
|
173
168
|
|
|
174
|
-
const
|
|
175
|
-
for (const lint of evaluation.lints) {
|
|
169
|
+
for (const lint of findings) {
|
|
176
170
|
const configuredSeverity = getConfiguredSeverity(lint.code, options);
|
|
177
171
|
const effectiveSeverity = configuredSeverity ?? lint.severity;
|
|
178
172
|
|
|
@@ -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 { SqlParamRefMutator } from '@prisma-next/sql-relational-core/middleware';
|
|
9
10
|
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
10
11
|
|
|
11
12
|
export interface SqlMiddlewareContext extends RuntimeMiddlewareContext {
|
|
@@ -21,7 +22,8 @@ export interface DraftPlan {
|
|
|
21
22
|
readonly meta: PlanMeta;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export interface SqlMiddleware extends
|
|
25
|
+
export interface SqlMiddleware<TCodecMap extends Record<string, unknown> = Record<string, unknown>>
|
|
26
|
+
extends RuntimeMiddleware<SqlExecutionPlan, SqlParamRefMutator<TCodecMap>> {
|
|
25
27
|
readonly familyId?: 'sql';
|
|
26
28
|
/**
|
|
27
29
|
* Rewrite the query AST before it is lowered to SQL. Middlewares run in
|
|
@@ -42,7 +44,24 @@ export interface SqlMiddleware extends RuntimeMiddleware<SqlExecutionPlan> {
|
|
|
42
44
|
* See `docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md`.
|
|
43
45
|
*/
|
|
44
46
|
beforeCompile?(draft: DraftPlan, ctx: SqlMiddlewareContext): Promise<DraftPlan | undefined>;
|
|
45
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Mutate `ParamRef.value` slots before encode runs. The third `params`
|
|
49
|
+
* argument is a {@link SqlParamRefMutator} scoped to value slots only —
|
|
50
|
+
* SQL strings, projections, and `ParamRef` membership are not mutable.
|
|
51
|
+
* Existing `(plan)` and `(plan, ctx)` middleware bodies that ignore the
|
|
52
|
+
* additional argument continue to compile and run unchanged.
|
|
53
|
+
*
|
|
54
|
+
* `ctx.signal` carries the per-query `AbortSignal` (ADR 207); middleware
|
|
55
|
+
* that wraps a network SDK forwards `ctx.signal` to that SDK.
|
|
56
|
+
* Cooperative cancellation: a body that ignores the signal still
|
|
57
|
+
* surfaces `RUNTIME.ABORTED { phase: 'beforeExecute' }` promptly via
|
|
58
|
+
* the runtime's race against the signal.
|
|
59
|
+
*/
|
|
60
|
+
beforeExecute?(
|
|
61
|
+
plan: SqlExecutionPlan,
|
|
62
|
+
ctx: SqlMiddlewareContext,
|
|
63
|
+
params?: SqlParamRefMutator<TCodecMap>,
|
|
64
|
+
): void | Promise<void>;
|
|
46
65
|
onRow?(
|
|
47
66
|
row: Record<string, unknown>,
|
|
48
67
|
plan: SqlExecutionPlan,
|
package/src/runtime-spi.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import type { ExecutionPlan } from '@prisma-next/framework-components/runtime';
|
|
2
|
-
import type {
|
|
2
|
+
import type { MarkerReadResult, SqlQueryable } from '@prisma-next/sql-relational-core/ast';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Reader of the SQL contract marker. SQL runtimes verify the
|
|
6
|
-
* `prisma_contract.marker` row against the runtime's contract by issuing
|
|
7
|
-
* this statement before executing user queries (when `verify` is enabled).
|
|
8
|
-
*
|
|
9
|
-
* Structurally satisfied by `AdapterProfile`, which already exposes
|
|
10
|
-
* `readMarkerStatement(): MarkerStatement` for adapter-level introspection.
|
|
5
|
+
* Reader of the SQL contract marker. SQL runtimes call `readMarker` before executing user queries (when `verify` is enabled). The adapter owns the full marker-read flow — probing for storage, issuing the read, decoding the row — and returns a tagged result so callers can distinguish "marker storage missing", "no row for this space", and "present".
|
|
11
6
|
*/
|
|
12
7
|
export interface MarkerReader {
|
|
13
|
-
|
|
8
|
+
readMarker(queryable: SqlQueryable): Promise<MarkerReadResult>;
|
|
14
9
|
}
|
|
15
10
|
|
|
16
11
|
/**
|