@prisma-next/sql-runtime 0.5.0-dev.3 → 0.5.0-dev.30

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.
Files changed (42) hide show
  1. package/README.md +29 -21
  2. package/dist/{exports-BQZSVXXt.mjs → exports-Cq_9ZrU4.mjs} +649 -275
  3. package/dist/exports-Cq_9ZrU4.mjs.map +1 -0
  4. package/dist/{index-yb51L_1h.d.mts → index-Df2GsLSH.d.mts} +65 -16
  5. package/dist/index-Df2GsLSH.d.mts.map +1 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +2 -2
  8. package/dist/test/utils.d.mts +6 -5
  9. package/dist/test/utils.d.mts.map +1 -1
  10. package/dist/test/utils.mjs +11 -5
  11. package/dist/test/utils.mjs.map +1 -1
  12. package/package.json +10 -12
  13. package/src/codecs/decoding.ts +256 -173
  14. package/src/codecs/encoding.ts +123 -39
  15. package/src/exports/index.ts +11 -7
  16. package/src/fingerprint.ts +22 -0
  17. package/src/guardrails/raw.ts +165 -0
  18. package/src/lower-sql-plan.ts +3 -3
  19. package/src/marker.ts +75 -0
  20. package/src/middleware/before-compile-chain.ts +1 -0
  21. package/src/middleware/budgets.ts +26 -96
  22. package/src/middleware/lints.ts +3 -3
  23. package/src/middleware/sql-middleware.ts +6 -5
  24. package/src/runtime-spi.ts +44 -0
  25. package/src/sql-family-adapter.ts +3 -2
  26. package/src/sql-marker.ts +62 -47
  27. package/src/sql-runtime.ts +321 -111
  28. package/dist/exports-BQZSVXXt.mjs.map +0 -1
  29. package/dist/index-yb51L_1h.d.mts.map +0 -1
  30. package/test/async-iterable-result.test.ts +0 -141
  31. package/test/before-compile-chain.test.ts +0 -223
  32. package/test/budgets.test.ts +0 -431
  33. package/test/context.types.test-d.ts +0 -68
  34. package/test/execution-stack.test.ts +0 -161
  35. package/test/json-schema-validation.test.ts +0 -571
  36. package/test/lints.test.ts +0 -160
  37. package/test/mutation-default-generators.test.ts +0 -254
  38. package/test/parameterized-types.test.ts +0 -529
  39. package/test/sql-context.test.ts +0 -384
  40. package/test/sql-family-adapter.test.ts +0 -103
  41. package/test/sql-runtime.test.ts +0 -792
  42. package/test/utils.ts +0 -297
@@ -1,7 +1,10 @@
1
- import type { ExecutionPlan } from '@prisma-next/contract/types';
2
- import { type RuntimeErrorEnvelope, runtimeError } from '@prisma-next/framework-components/runtime';
3
- import type { AfterExecuteResult } from '@prisma-next/runtime-executor';
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 = refs?.tables?.[0];
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<ExecutionPlan, { count: number }>();
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: ExecutionPlan, ctx: SqlMiddlewareContext) {
91
+ async beforeExecute(plan: SqlExecutionPlan, ctx: SqlMiddlewareContext) {
103
92
  observedRowsByPlan.set(plan, { count: 0 });
104
93
 
105
- if (isQueryAst(plan.ast)) {
106
- if (plan.ast.kind === 'select') {
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: ExecutionPlan, _ctx: SqlMiddlewareContext) {
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: ExecutionPlan,
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(plan: ExecutionPlan, ast: SelectAst, ctx: SqlMiddlewareContext) {
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
  }
@@ -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: ExecutionPlan, ctx: SqlMiddlewareContext) {
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, ExecutionPlan, PlanMeta } from '@prisma-next/contract/types';
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: ExecutionPlan, ctx: SqlMiddlewareContext): Promise<void>;
45
+ beforeExecute?(plan: SqlExecutionPlan, ctx: SqlMiddlewareContext): Promise<void>;
45
46
  onRow?(
46
47
  row: Record<string, unknown>,
47
- plan: ExecutionPlan,
48
+ plan: SqlExecutionPlan,
48
49
  ctx: SqlMiddlewareContext,
49
50
  ): Promise<void>;
50
51
  afterExecute?(
51
- plan: ExecutionPlan,
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
+ }
@@ -1,8 +1,9 @@
1
- import type { Contract, ExecutionPlan } from '@prisma-next/contract/types';
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import type { ExecutionPlan } from '@prisma-next/framework-components/runtime';
2
3
  import { runtimeError } from '@prisma-next/framework-components/runtime';
3
- import type { MarkerReader, RuntimeFamilyAdapter } from '@prisma-next/runtime-executor';
4
4
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
5
5
  import type { AdapterProfile } from '@prisma-next/sql-relational-core/ast';
6
+ import type { MarkerReader, RuntimeFamilyAdapter } from './runtime-spi';
6
7
 
7
8
  export class SqlFamilyAdapter<TContract extends Contract<SqlStorage>>
8
9
  implements RuntimeFamilyAdapter<TContract>
package/src/sql-marker.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MarkerStatement } from '@prisma-next/runtime-executor';
1
+ import type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
2
2
 
3
3
  export interface SqlStatement {
4
4
  readonly sql: string;
@@ -12,6 +12,15 @@ export interface WriteMarkerInput {
12
12
  readonly canonicalVersion?: number;
13
13
  readonly appTag?: string;
14
14
  readonly meta?: Record<string, unknown>;
15
+ /**
16
+ * Applied-invariants set on the marker.
17
+ *
18
+ * - `undefined` → existing column left untouched. Sign and
19
+ * verify-database paths use this; they don't accumulate invariants.
20
+ * - explicit value (including `[]`) → column overwritten with
21
+ * exactly that value.
22
+ */
23
+ readonly invariants?: readonly string[];
15
24
  }
16
25
 
17
26
  export const ensureSchemaStatement: SqlStatement = {
@@ -28,7 +37,8 @@ export const ensureTableStatement: SqlStatement = {
28
37
  canonical_version int,
29
38
  updated_at timestamptz not null default now(),
30
39
  app_tag text,
31
- meta jsonb not null default '{}'
40
+ meta jsonb not null default '{}',
41
+ invariants text[] not null default '{}'
32
42
  )`,
33
43
  params: [],
34
44
  };
@@ -42,7 +52,8 @@ export function readContractMarker(): MarkerStatement {
42
52
  canonical_version,
43
53
  updated_at,
44
54
  app_tag,
45
- meta
55
+ meta,
56
+ invariants
46
57
  from prisma_contract.marker
47
58
  where id = $1`,
48
59
  params: [1],
@@ -54,52 +65,56 @@ export interface WriteContractMarkerStatements {
54
65
  readonly update: SqlStatement;
55
66
  }
56
67
 
57
- export function writeContractMarker(input: WriteMarkerInput): WriteContractMarkerStatements {
58
- const baseParams: readonly unknown[] = [
59
- 1,
60
- input.storageHash,
61
- input.profileHash,
62
- input.contractJson ?? null,
63
- input.canonicalVersion ?? null,
64
- input.appTag ?? null,
65
- JSON.stringify(input.meta ?? {}),
68
+ /**
69
+ * Variable columns that participate in INSERT/UPDATE alongside the
70
+ * always-on `id = $1` and `updated_at = now()`. Each column declares
71
+ * its name, optional cast type, and parameter value; the placeholder
72
+ * (`$N`) is computed positionally below — adding or reordering a
73
+ * column doesn't desync indices. `invariants` only appears when the
74
+ * caller supplies it — see `WriteMarkerInput.invariants`.
75
+ */
76
+ function markerColumns(
77
+ input: WriteMarkerInput,
78
+ ): ReadonlyArray<{ readonly name: string; readonly type?: string; readonly param: unknown }> {
79
+ return [
80
+ { name: 'core_hash', param: input.storageHash },
81
+ { name: 'profile_hash', param: input.profileHash },
82
+ { name: 'contract_json', type: 'jsonb', param: input.contractJson ?? null },
83
+ { name: 'canonical_version', param: input.canonicalVersion ?? null },
84
+ { name: 'app_tag', param: input.appTag ?? null },
85
+ { name: 'meta', type: 'jsonb', param: JSON.stringify(input.meta ?? {}) },
86
+ ...(input.invariants !== undefined
87
+ ? [{ name: 'invariants' as const, type: 'text[]' as const, param: input.invariants }]
88
+ : []),
66
89
  ];
90
+ }
67
91
 
68
- const insert: SqlStatement = {
69
- sql: `insert into prisma_contract.marker (
70
- id,
71
- core_hash,
72
- profile_hash,
73
- contract_json,
74
- canonical_version,
75
- updated_at,
76
- app_tag,
77
- meta
78
- ) values (
79
- $1,
80
- $2,
81
- $3,
82
- $4::jsonb,
83
- $5,
84
- now(),
85
- $6,
86
- $7::jsonb
87
- )`,
88
- params: baseParams,
89
- };
92
+ export function writeContractMarker(input: WriteMarkerInput): WriteContractMarkerStatements {
93
+ const cols = markerColumns(input);
94
+ // $1 is reserved for `id`; subsequent positions follow the order of cols.
95
+ const placed = cols.map((c, i) => ({
96
+ name: c.name,
97
+ expr: c.type ? `$${i + 2}::${c.type}` : `$${i + 2}`,
98
+ param: c.param,
99
+ }));
100
+ const params: readonly unknown[] = [1, ...placed.map((c) => c.param)];
90
101
 
91
- const update: SqlStatement = {
92
- sql: `update prisma_contract.marker set
93
- core_hash = $2,
94
- profile_hash = $3,
95
- contract_json = $4::jsonb,
96
- canonical_version = $5,
97
- updated_at = now(),
98
- app_tag = $6,
99
- meta = $7::jsonb
100
- where id = $1`,
101
- params: baseParams,
102
- };
102
+ // `updated_at = now()` is a SQL literal with no parameter slot, so it
103
+ // sits outside `placed` and is appended directly to each statement.
104
+ const insertColumns = ['id', ...placed.map((c) => c.name), 'updated_at'].join(', ');
105
+ const insertValues = ['$1', ...placed.map((c) => c.expr), 'now()'].join(', ');
106
+ const setClauses = [...placed.map((c) => `${c.name} = ${c.expr}`), 'updated_at = now()'].join(
107
+ ', ',
108
+ );
103
109
 
104
- return { insert, update };
110
+ return {
111
+ insert: {
112
+ sql: `insert into prisma_contract.marker (${insertColumns}) values (${insertValues})`,
113
+ params,
114
+ },
115
+ update: {
116
+ sql: `update prisma_contract.marker set ${setClauses} where id = $1`,
117
+ params,
118
+ },
119
+ };
105
120
  }