@prisma-next/sql-runtime 0.5.0-dev.4 → 0.5.0-dev.41

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 (45) hide show
  1. package/README.md +29 -21
  2. package/dist/exports-CrHMfIKo.mjs +1564 -0
  3. package/dist/exports-CrHMfIKo.mjs.map +1 -0
  4. package/dist/{index-yb51L_1h.d.mts → index-_dXSGeho.d.mts} +78 -25
  5. package/dist/index-_dXSGeho.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 +294 -173
  14. package/src/codecs/encoding.ts +162 -37
  15. package/src/codecs/validation.ts +22 -3
  16. package/src/exports/index.ts +11 -7
  17. package/src/fingerprint.ts +22 -0
  18. package/src/guardrails/raw.ts +165 -0
  19. package/src/lower-sql-plan.ts +3 -3
  20. package/src/marker.ts +75 -0
  21. package/src/middleware/before-compile-chain.ts +1 -0
  22. package/src/middleware/budgets.ts +26 -96
  23. package/src/middleware/lints.ts +3 -3
  24. package/src/middleware/sql-middleware.ts +6 -5
  25. package/src/runtime-spi.ts +44 -0
  26. package/src/sql-context.ts +332 -78
  27. package/src/sql-family-adapter.ts +3 -2
  28. package/src/sql-marker.ts +62 -47
  29. package/src/sql-runtime.ts +332 -113
  30. package/dist/exports-BQZSVXXt.mjs +0 -981
  31. package/dist/exports-BQZSVXXt.mjs.map +0 -1
  32. package/dist/index-yb51L_1h.d.mts.map +0 -1
  33. package/test/async-iterable-result.test.ts +0 -141
  34. package/test/before-compile-chain.test.ts +0 -223
  35. package/test/budgets.test.ts +0 -431
  36. package/test/context.types.test-d.ts +0 -68
  37. package/test/execution-stack.test.ts +0 -161
  38. package/test/json-schema-validation.test.ts +0 -571
  39. package/test/lints.test.ts +0 -160
  40. package/test/mutation-default-generators.test.ts +0 -254
  41. package/test/parameterized-types.test.ts +0 -529
  42. package/test/sql-context.test.ts +0 -384
  43. package/test/sql-family-adapter.test.ts +0 -103
  44. package/test/sql-runtime.test.ts +0 -792
  45. package/test/utils.ts +0 -297
@@ -1,66 +1,191 @@
1
- import type { ExecutionPlan, ParamDescriptor } from '@prisma-next/contract/types';
2
- import type { Codec, CodecRegistry } from '@prisma-next/sql-relational-core/ast';
1
+ import {
2
+ checkAborted,
3
+ raceAgainstAbort,
4
+ runtimeError,
5
+ } from '@prisma-next/framework-components/runtime';
6
+ import {
7
+ type Codec,
8
+ type CodecRegistry,
9
+ type ContractCodecRegistry,
10
+ collectOrderedParamRefs,
11
+ type SqlCodecCallContext,
12
+ } from '@prisma-next/sql-relational-core/ast';
13
+ import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
3
14
 
15
+ interface ParamMetadata {
16
+ readonly codecId: string | undefined;
17
+ readonly name: string | undefined;
18
+ }
19
+
20
+ const NO_METADATA: ParamMetadata = Object.freeze({ codecId: undefined, name: undefined });
21
+
22
+ /**
23
+ * Resolve the codec for an outgoing param.
24
+ *
25
+ * Phase B (and AC-5-deferred carve-out): `ParamRef` does not carry a
26
+ * `(table, column)` ref today — every `ParamRef` carries `codecId` but
27
+ * not the column it relates to. Encode-side dispatch therefore consults
28
+ * `contractCodecs.forCodecId(codecId)` (which itself prefers the
29
+ * contract-walk-derived shared codec, falling back to the legacy
30
+ * `CodecRegistry.get` for parameterized codec ids whose contracts don't
31
+ * have a column the walk could resolve through).
32
+ *
33
+ * For the parameterized codecs shipped at Phase B (pgvector, postgres
34
+ * json/jsonb), encode is per-instance-stateless w.r.t. params:
35
+ * - pgvector formats `[v1,v2,...]` regardless of declared length;
36
+ * - postgres json/jsonb encode is `JSON.stringify` regardless of schema.
37
+ *
38
+ * So the codec-id-keyed lookup yields a structurally equivalent encoder
39
+ * even when the resolved per-instance codec carries extra state (e.g. a
40
+ * compiled JSON-Schema validator used only by `decode`). TML-2357 retires
41
+ * the fallback by threading `ParamRef.refs` through column-bound
42
+ * construction sites.
43
+ */
4
44
  function resolveParamCodec(
5
- paramDescriptor: ParamDescriptor,
45
+ metadata: ParamMetadata,
6
46
  registry: CodecRegistry,
7
- ): Codec | null {
8
- if (paramDescriptor.codecId) {
9
- const codec = registry.get(paramDescriptor.codecId);
10
- if (codec) {
11
- return codec;
12
- }
13
- }
47
+ contractCodecs: ContractCodecRegistry | undefined,
48
+ ): Codec | undefined {
49
+ if (!metadata.codecId) return undefined;
50
+ const fromContract = contractCodecs?.forCodecId(metadata.codecId);
51
+ if (fromContract) return fromContract;
52
+ return registry.get(metadata.codecId);
53
+ }
14
54
 
15
- return null;
55
+ function paramLabel(metadata: ParamMetadata, paramIndex: number): string {
56
+ return metadata.name ?? `param[${paramIndex}]`;
16
57
  }
17
58
 
18
- export function encodeParam(
59
+ function wrapEncodeFailure(
60
+ error: unknown,
61
+ metadata: ParamMetadata,
62
+ paramIndex: number,
63
+ codecId: string,
64
+ ): never {
65
+ const label = paramLabel(metadata, paramIndex);
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ const wrapped = runtimeError(
68
+ 'RUNTIME.ENCODE_FAILED',
69
+ `Failed to encode parameter ${label} with codec '${codecId}': ${message}`,
70
+ { label, codec: codecId, paramIndex },
71
+ );
72
+ wrapped.cause = error;
73
+ throw wrapped;
74
+ }
75
+
76
+ /**
77
+ * Encodes a single parameter through its codec. Always awaits codec.encode so
78
+ * a Promise can never leak into the driver, even if a sync-authored codec is
79
+ * lifted to async by the codec() factory. Failures are wrapped in
80
+ * `RUNTIME.ENCODE_FAILED` with `{ label, codec, paramIndex }` and the original
81
+ * error attached on `cause`.
82
+ *
83
+ * `ctx` is forwarded verbatim to `codec.encode` so codec authors who opt
84
+ * into the `(value, ctx)` arity see the same `SqlCodecCallContext` the
85
+ * runtime built for the surrounding `runtime.execute()` call. The ctx is
86
+ * always present; its `signal` field may be `undefined`. Encode call
87
+ * sites do not populate `ctx.column` — encode-time column context is the
88
+ * middleware's domain.
89
+ */
90
+ export async function encodeParam(
19
91
  value: unknown,
20
- paramDescriptor: ParamDescriptor,
92
+ paramRef: { readonly codecId?: string; readonly name?: string },
21
93
  paramIndex: number,
22
94
  registry: CodecRegistry,
23
- ): unknown {
95
+ ctx: SqlCodecCallContext,
96
+ contractCodecs?: ContractCodecRegistry,
97
+ ): Promise<unknown> {
98
+ return encodeParamValue(
99
+ value,
100
+ { codecId: paramRef.codecId, name: paramRef.name },
101
+ paramIndex,
102
+ registry,
103
+ ctx,
104
+ contractCodecs,
105
+ );
106
+ }
107
+
108
+ async function encodeParamValue(
109
+ value: unknown,
110
+ metadata: ParamMetadata,
111
+ paramIndex: number,
112
+ registry: CodecRegistry,
113
+ ctx: SqlCodecCallContext,
114
+ contractCodecs: ContractCodecRegistry | undefined,
115
+ ): Promise<unknown> {
24
116
  if (value === null || value === undefined) {
25
117
  return null;
26
118
  }
27
119
 
28
- const codec = resolveParamCodec(paramDescriptor, registry);
120
+ const codec = resolveParamCodec(metadata, registry, contractCodecs);
29
121
  if (!codec) {
30
122
  return value;
31
123
  }
32
124
 
33
- if (codec.encode) {
34
- try {
35
- return codec.encode(value);
36
- } catch (error) {
37
- const label = paramDescriptor.name ?? `param[${paramIndex}]`;
38
- throw new Error(
39
- `Failed to encode parameter ${label}: ${error instanceof Error ? error.message : String(error)}`,
40
- );
41
- }
125
+ try {
126
+ return await codec.encode(value, ctx);
127
+ } catch (error) {
128
+ wrapEncodeFailure(error, metadata, paramIndex, codec.id);
42
129
  }
43
-
44
- return value;
45
130
  }
46
131
 
47
- export function encodeParams(plan: ExecutionPlan, registry: CodecRegistry): readonly unknown[] {
132
+ /**
133
+ * Encodes all parameters concurrently via `Promise.all`. Per parameter, sync-
134
+ * and async-authored codecs share the same path: `codec.encode → await →
135
+ * return`. Param-level failures are wrapped in `RUNTIME.ENCODE_FAILED`.
136
+ *
137
+ * When `ctx.signal` is provided:
138
+ *
139
+ * - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED`
140
+ * (`{ phase: 'encode' }`) before any `codec.encode` call is made — codecs
141
+ * can pin this with a per-call counter that stays at zero.
142
+ * - **Mid-flight abort** races the per-param `Promise.all` against
143
+ * `abortable(ctx.signal)`. The runtime returns `RUNTIME.ABORTED` promptly
144
+ * even if codec bodies ignore the signal; the in-flight bodies are
145
+ * abandoned and run to completion in the background (cooperative
146
+ * cancellation, see ADR 204).
147
+ * - Existing `RUNTIME.ENCODE_FAILED` envelopes that surface from a codec
148
+ * body before the runtime observes the abort pass through unchanged
149
+ * (no double wrap).
150
+ */
151
+ export async function encodeParams(
152
+ plan: SqlExecutionPlan,
153
+ registry: CodecRegistry,
154
+ ctx: SqlCodecCallContext,
155
+ contractCodecs?: ContractCodecRegistry,
156
+ ): Promise<readonly unknown[]> {
157
+ checkAborted(ctx, 'encode');
158
+ const signal = ctx.signal;
159
+
48
160
  if (plan.params.length === 0) {
49
161
  return plan.params;
50
162
  }
51
163
 
52
- const encoded: unknown[] = [];
53
-
54
- for (let i = 0; i < plan.params.length; i++) {
55
- const paramValue = plan.params[i];
56
- const paramDescriptor = plan.meta.paramDescriptors[i];
164
+ const paramCount = plan.params.length;
165
+ const metadata: ParamMetadata[] = new Array(paramCount).fill(NO_METADATA);
57
166
 
58
- if (paramDescriptor) {
59
- encoded.push(encodeParam(paramValue, paramDescriptor, i, registry));
60
- } else {
61
- encoded.push(paramValue);
167
+ if (plan.ast) {
168
+ const refs = collectOrderedParamRefs(plan.ast);
169
+ for (let i = 0; i < paramCount && i < refs.length; i++) {
170
+ const ref = refs[i];
171
+ if (ref) {
172
+ metadata[i] = { codecId: ref.codecId, name: ref.name };
173
+ }
62
174
  }
63
175
  }
64
176
 
65
- return Object.freeze(encoded);
177
+ const tasks: Promise<unknown>[] = new Array(paramCount);
178
+ for (let i = 0; i < paramCount; i++) {
179
+ tasks[i] = encodeParamValue(
180
+ plan.params[i],
181
+ metadata[i] ?? NO_METADATA,
182
+ i,
183
+ registry,
184
+ ctx,
185
+ contractCodecs,
186
+ );
187
+ }
188
+
189
+ const settled = await raceAgainstAbort(Promise.all(tasks), signal, 'encode');
190
+ return Object.freeze(settled);
66
191
  }
@@ -2,6 +2,7 @@ import type { Contract } from '@prisma-next/contract/types';
2
2
  import { runtimeError } from '@prisma-next/framework-components/runtime';
3
3
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
4
4
  import type { CodecRegistry } from '@prisma-next/sql-relational-core/ast';
5
+ import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
5
6
 
6
7
  export function extractCodecIds(contract: Contract<SqlStorage>): Set<string> {
7
8
  const codecIds = new Set<string>();
@@ -30,15 +31,33 @@ function extractCodecIdsFromColumns(contract: Contract<SqlStorage>): Map<string,
30
31
  return codecIds;
31
32
  }
32
33
 
34
+ interface CodecLookupForValidation {
35
+ has(id: string): boolean;
36
+ }
37
+
38
+ function adaptDescriptorRegistry(registry: CodecDescriptorRegistry): CodecLookupForValidation {
39
+ return { has: (id: string) => registry.descriptorFor(id) !== undefined };
40
+ }
41
+
42
+ function isDescriptorRegistry(
43
+ registry: CodecRegistry | CodecDescriptorRegistry,
44
+ ): registry is CodecDescriptorRegistry {
45
+ return 'descriptorFor' in registry;
46
+ }
47
+
33
48
  export function validateContractCodecMappings(
34
- registry: CodecRegistry,
49
+ registry: CodecRegistry | CodecDescriptorRegistry,
35
50
  contract: Contract<SqlStorage>,
36
51
  ): void {
52
+ const lookup: CodecLookupForValidation = isDescriptorRegistry(registry)
53
+ ? adaptDescriptorRegistry(registry)
54
+ : registry;
55
+
37
56
  const codecIds = extractCodecIdsFromColumns(contract);
38
57
  const invalidCodecs: Array<{ table: string; column: string; codecId: string }> = [];
39
58
 
40
59
  for (const [key, codecId] of codecIds.entries()) {
41
- if (!registry.has(codecId)) {
60
+ if (!lookup.has(codecId)) {
42
61
  const parts = key.split('.');
43
62
  const table = parts[0] ?? '';
44
63
  const column = parts[1] ?? '';
@@ -61,7 +80,7 @@ export function validateContractCodecMappings(
61
80
  }
62
81
 
63
82
  export function validateCodecRegistryCompleteness(
64
- registry: CodecRegistry,
83
+ registry: CodecRegistry | CodecDescriptorRegistry,
65
84
  contract: Contract<SqlStorage>,
66
85
  ): void {
67
86
  validateContractCodecMappings(registry, contract);
@@ -1,20 +1,27 @@
1
1
  export type {
2
2
  AfterExecuteResult,
3
- Log,
4
- Middleware,
5
- MiddlewareContext,
6
- } from '@prisma-next/runtime-executor';
3
+ RuntimeLog as Log,
4
+ } from '@prisma-next/framework-components/runtime';
5
+ export type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
7
6
  export {
8
7
  extractCodecIds,
9
8
  validateCodecRegistryCompleteness,
10
9
  validateContractCodecMappings,
11
10
  } from '../codecs/validation';
12
11
  export { lowerSqlPlan } from '../lower-sql-plan';
12
+ export { parseContractMarkerRow } from '../marker';
13
13
  export type { BudgetsOptions } from '../middleware/budgets';
14
14
  export { budgets } from '../middleware/budgets';
15
15
  export type { LintsOptions } from '../middleware/lints';
16
16
  export { lints } from '../middleware/lints';
17
17
  export type { SqlMiddleware, SqlMiddlewareContext } from '../middleware/sql-middleware';
18
+ export type {
19
+ MarkerReader,
20
+ RuntimeFamilyAdapter,
21
+ RuntimeTelemetryEvent,
22
+ RuntimeVerifyOptions,
23
+ TelemetryOutcome,
24
+ } from '../runtime-spi';
18
25
  export type {
19
26
  ExecutionContext,
20
27
  RuntimeMutationDefaultGenerator,
@@ -46,10 +53,7 @@ export type {
46
53
  Runtime,
47
54
  RuntimeConnection,
48
55
  RuntimeQueryable,
49
- RuntimeTelemetryEvent,
50
56
  RuntimeTransaction,
51
- RuntimeVerifyOptions,
52
- TelemetryOutcome,
53
57
  TransactionContext,
54
58
  } from '../sql-runtime';
55
59
  export { createRuntime, withTransaction } from '../sql-runtime';
@@ -0,0 +1,22 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const STRING_LITERAL_REGEX = /'(?:''|[^'])*'/g;
4
+ const NUMERIC_LITERAL_REGEX = /\b\d+(?:\.\d+)?\b/g;
5
+ const WHITESPACE_REGEX = /\s+/g;
6
+
7
+ /**
8
+ * Computes a literal-stripped, normalized fingerprint of a SQL statement.
9
+ *
10
+ * The function strips string and numeric literals, collapses whitespace, and
11
+ * lowercases the result before hashing — so two structurally equivalent
12
+ * statements (with different parameter values) produce the same fingerprint.
13
+ * Used by SQL telemetry to group queries.
14
+ */
15
+ export function computeSqlFingerprint(sql: string): string {
16
+ const withoutStrings = sql.replace(STRING_LITERAL_REGEX, '?');
17
+ const withoutNumbers = withoutStrings.replace(NUMERIC_LITERAL_REGEX, '?');
18
+ const normalized = withoutNumbers.replace(WHITESPACE_REGEX, ' ').trim().toLowerCase();
19
+
20
+ const hash = createHash('sha256').update(normalized).digest('hex');
21
+ return `sha256:${hash}`;
22
+ }
@@ -0,0 +1,165 @@
1
+ import type { PlanMeta } from '@prisma-next/contract/types';
2
+
3
+ export type LintSeverity = 'error' | 'warn';
4
+ export type BudgetSeverity = 'error' | 'warn';
5
+
6
+ export interface LintFinding {
7
+ readonly code: `LINT.${string}`;
8
+ readonly severity: LintSeverity;
9
+ readonly message: string;
10
+ readonly details?: Record<string, unknown>;
11
+ }
12
+
13
+ export interface BudgetFinding {
14
+ readonly code: `BUDGET.${string}`;
15
+ readonly severity: BudgetSeverity;
16
+ readonly message: string;
17
+ readonly details?: Record<string, unknown>;
18
+ }
19
+
20
+ export interface RawGuardrailConfig {
21
+ readonly budgets?: {
22
+ readonly unboundedSelectSeverity?: BudgetSeverity;
23
+ readonly estimatedRows?: number;
24
+ };
25
+ }
26
+
27
+ export interface RawGuardrailResult {
28
+ readonly lints: LintFinding[];
29
+ readonly budgets: BudgetFinding[];
30
+ readonly statement: 'select' | 'mutation' | 'other';
31
+ }
32
+
33
+ /**
34
+ * Minimal plan view consumed by raw-SQL guardrails. Structurally satisfied
35
+ * by `SqlExecutionPlan`; declared inline so this module stays decoupled
36
+ * from a specific plan type at the call site.
37
+ */
38
+ interface RawGuardrailPlan {
39
+ readonly sql: string;
40
+ readonly meta: PlanMeta;
41
+ }
42
+
43
+ const SELECT_STAR_REGEX = /select\s+\*/i;
44
+ const LIMIT_REGEX = /\blimit\b/i;
45
+ const MUTATION_PREFIX_REGEX = /^(insert|update|delete|create|alter|drop|truncate)\b/i;
46
+
47
+ const READ_ONLY_INTENTS = new Set(['read', 'report', 'readonly']);
48
+
49
+ export function evaluateRawGuardrails(
50
+ plan: RawGuardrailPlan,
51
+ config?: RawGuardrailConfig,
52
+ ): RawGuardrailResult {
53
+ const lints: LintFinding[] = [];
54
+ const budgets: BudgetFinding[] = [];
55
+
56
+ const normalized = normalizeWhitespace(plan.sql);
57
+ const statementType = classifyStatement(normalized);
58
+
59
+ if (statementType === 'select') {
60
+ if (SELECT_STAR_REGEX.test(normalized)) {
61
+ lints.push(
62
+ createLint('LINT.SELECT_STAR', 'error', 'Raw SQL plan selects all columns via *', {
63
+ sql: snippet(plan.sql),
64
+ }),
65
+ );
66
+ }
67
+
68
+ if (!LIMIT_REGEX.test(normalized)) {
69
+ const severity = config?.budgets?.unboundedSelectSeverity ?? 'error';
70
+ lints.push(
71
+ createLint('LINT.NO_LIMIT', 'warn', 'Raw SQL plan omits LIMIT clause', {
72
+ sql: snippet(plan.sql),
73
+ }),
74
+ );
75
+
76
+ budgets.push(
77
+ createBudget(
78
+ 'BUDGET.ROWS_EXCEEDED',
79
+ severity,
80
+ 'Raw SQL plan is unbounded and may exceed row budget',
81
+ {
82
+ sql: snippet(plan.sql),
83
+ ...(config?.budgets?.estimatedRows !== undefined
84
+ ? { estimatedRows: config.budgets.estimatedRows }
85
+ : {}),
86
+ },
87
+ ),
88
+ );
89
+ }
90
+ }
91
+
92
+ if (isMutationStatement(statementType) && isReadOnlyIntent(plan.meta)) {
93
+ lints.push(
94
+ createLint(
95
+ 'LINT.READ_ONLY_MUTATION',
96
+ 'error',
97
+ 'Raw SQL plan mutates data despite read-only intent',
98
+ {
99
+ sql: snippet(plan.sql),
100
+ intent: plan.meta.annotations?.['intent'],
101
+ },
102
+ ),
103
+ );
104
+ }
105
+
106
+ return { lints, budgets, statement: statementType };
107
+ }
108
+
109
+ function classifyStatement(sql: string): 'select' | 'mutation' | 'other' {
110
+ const trimmed = sql.trim();
111
+ const lower = trimmed.toLowerCase();
112
+
113
+ if (lower.startsWith('with')) {
114
+ if (lower.includes('select')) {
115
+ return 'select';
116
+ }
117
+ }
118
+
119
+ if (lower.startsWith('select')) {
120
+ return 'select';
121
+ }
122
+
123
+ if (MUTATION_PREFIX_REGEX.test(trimmed)) {
124
+ return 'mutation';
125
+ }
126
+
127
+ return 'other';
128
+ }
129
+
130
+ function isMutationStatement(statement: 'select' | 'mutation' | 'other'): boolean {
131
+ return statement === 'mutation';
132
+ }
133
+
134
+ function isReadOnlyIntent(meta: PlanMeta): boolean {
135
+ const annotations = meta.annotations as { intent?: string } | undefined;
136
+ const intent =
137
+ typeof annotations?.intent === 'string' ? annotations.intent.toLowerCase() : undefined;
138
+ return intent !== undefined && READ_ONLY_INTENTS.has(intent);
139
+ }
140
+
141
+ function normalizeWhitespace(value: string): string {
142
+ return value.replace(/\s+/g, ' ').trim();
143
+ }
144
+
145
+ function snippet(sql: string): string {
146
+ return normalizeWhitespace(sql).slice(0, 200);
147
+ }
148
+
149
+ function createLint(
150
+ code: LintFinding['code'],
151
+ severity: LintFinding['severity'],
152
+ message: string,
153
+ details?: Record<string, unknown>,
154
+ ): LintFinding {
155
+ return { code, severity, message, ...(details ? { details } : {}) };
156
+ }
157
+
158
+ function createBudget(
159
+ code: BudgetFinding['code'],
160
+ severity: BudgetFinding['severity'],
161
+ message: string,
162
+ details?: Record<string, unknown>,
163
+ ): BudgetFinding {
164
+ return { code, severity, message, ...(details ? { details } : {}) };
165
+ }
@@ -1,7 +1,7 @@
1
- import type { Contract, ExecutionPlan } from '@prisma-next/contract/types';
1
+ import type { Contract } from '@prisma-next/contract/types';
2
2
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
3
3
  import type { Adapter, AnyQueryAst, LoweredStatement } from '@prisma-next/sql-relational-core/ast';
4
- import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
4
+ import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
5
5
 
6
6
  /**
7
7
  * Lowers a SQL query plan to an executable Plan by calling the adapter's lower method.
@@ -15,7 +15,7 @@ export function lowerSqlPlan<Row>(
15
15
  adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>,
16
16
  contract: Contract<SqlStorage>,
17
17
  queryPlan: SqlQueryPlan<Row>,
18
- ): ExecutionPlan<Row> {
18
+ ): SqlExecutionPlan<Row> {
19
19
  const lowered = adapter.lower(queryPlan.ast, {
20
20
  contract,
21
21
  params: queryPlan.params,
package/src/marker.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { ContractMarkerRecord } from '@prisma-next/contract/types';
2
+ import { type } from 'arktype';
3
+
4
+ export interface ContractMarkerRow {
5
+ core_hash: string;
6
+ profile_hash: string;
7
+ contract_json: unknown | null;
8
+ canonical_version: number | null;
9
+ updated_at: Date;
10
+ app_tag: string | null;
11
+ meta: unknown | null;
12
+ invariants: readonly string[];
13
+ }
14
+
15
+ const MetaSchema = type({ '[string]': 'unknown' });
16
+
17
+ function parseMeta(meta: unknown): Record<string, unknown> {
18
+ if (meta === null || meta === undefined) {
19
+ return {};
20
+ }
21
+
22
+ let parsed: unknown;
23
+ if (typeof meta === 'string') {
24
+ try {
25
+ parsed = JSON.parse(meta);
26
+ } catch {
27
+ return {};
28
+ }
29
+ } else {
30
+ parsed = meta;
31
+ }
32
+
33
+ const result = MetaSchema(parsed);
34
+ if (result instanceof type.errors) {
35
+ return {};
36
+ }
37
+
38
+ return result as Record<string, unknown>;
39
+ }
40
+
41
+ const ContractMarkerRowSchema = type({
42
+ core_hash: 'string',
43
+ profile_hash: 'string',
44
+ 'contract_json?': 'unknown | null',
45
+ 'canonical_version?': 'number | null',
46
+ 'updated_at?': 'Date | string',
47
+ 'app_tag?': 'string | null',
48
+ 'meta?': 'unknown | null',
49
+ invariants: type('string').array(),
50
+ });
51
+
52
+ export function parseContractMarkerRow(row: unknown): ContractMarkerRecord {
53
+ const result = ContractMarkerRowSchema(row);
54
+ if (result instanceof type.errors) {
55
+ const messages = result.map((p: { message: string }) => p.message).join('; ');
56
+ throw new Error(`Invalid contract marker row: ${messages}`);
57
+ }
58
+
59
+ const updatedAt = result.updated_at
60
+ ? result.updated_at instanceof Date
61
+ ? result.updated_at
62
+ : new Date(result.updated_at)
63
+ : new Date();
64
+
65
+ return {
66
+ storageHash: result.core_hash,
67
+ profileHash: result.profile_hash,
68
+ contractJson: result.contract_json ?? null,
69
+ canonicalVersion: result.canonical_version ?? null,
70
+ updatedAt,
71
+ appTag: result.app_tag ?? null,
72
+ meta: parseMeta(result.meta),
73
+ invariants: result.invariants,
74
+ };
75
+ }
@@ -24,5 +24,6 @@ export async function runBeforeCompileChain(
24
24
  });
25
25
  current = result;
26
26
  }
27
+
27
28
  return current;
28
29
  }