@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.
- package/README.md +29 -21
- package/dist/{exports-BQZSVXXt.mjs → exports-Cq_9ZrU4.mjs} +649 -275
- package/dist/exports-Cq_9ZrU4.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-Df2GsLSH.d.mts} +65 -16
- package/dist/index-Df2GsLSH.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 +10 -12
- package/src/codecs/decoding.ts +256 -173
- package/src/codecs/encoding.ts +123 -39
- 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-family-adapter.ts +3 -2
- package/src/sql-marker.ts +62 -47
- package/src/sql-runtime.ts +321 -111
- 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
package/src/codecs/encoding.ts
CHANGED
|
@@ -1,66 +1,150 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
checkAborted,
|
|
3
|
+
raceAgainstAbort,
|
|
4
|
+
runtimeError,
|
|
5
|
+
} from '@prisma-next/framework-components/runtime';
|
|
6
|
+
import {
|
|
7
|
+
type Codec,
|
|
8
|
+
type CodecRegistry,
|
|
9
|
+
collectOrderedParamRefs,
|
|
10
|
+
type SqlCodecCallContext,
|
|
11
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
12
|
+
import type { SqlExecutionPlan } from '@prisma-next/sql-relational-core/plan';
|
|
3
13
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
interface ParamMetadata {
|
|
15
|
+
readonly codecId: string | undefined;
|
|
16
|
+
readonly name: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const NO_METADATA: ParamMetadata = Object.freeze({ codecId: undefined, name: undefined });
|
|
20
|
+
|
|
21
|
+
function paramLabel(metadata: ParamMetadata, paramIndex: number): string {
|
|
22
|
+
return metadata.name ?? `param[${paramIndex}]`;
|
|
23
|
+
}
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
function wrapEncodeFailure(
|
|
26
|
+
error: unknown,
|
|
27
|
+
metadata: ParamMetadata,
|
|
28
|
+
paramIndex: number,
|
|
29
|
+
codecId: string,
|
|
30
|
+
): never {
|
|
31
|
+
const label = paramLabel(metadata, paramIndex);
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
const wrapped = runtimeError(
|
|
34
|
+
'RUNTIME.ENCODE_FAILED',
|
|
35
|
+
`Failed to encode parameter ${label} with codec '${codecId}': ${message}`,
|
|
36
|
+
{ label, codec: codecId, paramIndex },
|
|
37
|
+
);
|
|
38
|
+
wrapped.cause = error;
|
|
39
|
+
throw wrapped;
|
|
16
40
|
}
|
|
17
41
|
|
|
18
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Encodes a single parameter through its codec. Always awaits codec.encode so
|
|
44
|
+
* a Promise can never leak into the driver, even if a sync-authored codec is
|
|
45
|
+
* lifted to async by the codec() factory. Failures are wrapped in
|
|
46
|
+
* `RUNTIME.ENCODE_FAILED` with `{ label, codec, paramIndex }` and the original
|
|
47
|
+
* error attached on `cause`.
|
|
48
|
+
*
|
|
49
|
+
* `ctx` is forwarded verbatim to `codec.encode` so codec authors who opt
|
|
50
|
+
* into the `(value, ctx)` arity see the same `SqlCodecCallContext` the
|
|
51
|
+
* runtime built for the surrounding `runtime.execute()` call. The ctx is
|
|
52
|
+
* always present; its `signal` field may be `undefined`. Encode call
|
|
53
|
+
* sites do not populate `ctx.column` — encode-time column context is the
|
|
54
|
+
* middleware's domain.
|
|
55
|
+
*/
|
|
56
|
+
export async function encodeParam(
|
|
19
57
|
value: unknown,
|
|
20
|
-
|
|
58
|
+
paramRef: { readonly codecId?: string; readonly name?: string },
|
|
21
59
|
paramIndex: number,
|
|
22
60
|
registry: CodecRegistry,
|
|
23
|
-
|
|
61
|
+
ctx: SqlCodecCallContext,
|
|
62
|
+
): Promise<unknown> {
|
|
63
|
+
return encodeParamValue(
|
|
64
|
+
value,
|
|
65
|
+
{ codecId: paramRef.codecId, name: paramRef.name },
|
|
66
|
+
paramIndex,
|
|
67
|
+
registry,
|
|
68
|
+
ctx,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function encodeParamValue(
|
|
73
|
+
value: unknown,
|
|
74
|
+
metadata: ParamMetadata,
|
|
75
|
+
paramIndex: number,
|
|
76
|
+
registry: CodecRegistry,
|
|
77
|
+
ctx: SqlCodecCallContext,
|
|
78
|
+
): Promise<unknown> {
|
|
24
79
|
if (value === null || value === undefined) {
|
|
25
80
|
return null;
|
|
26
81
|
}
|
|
27
82
|
|
|
28
|
-
|
|
29
|
-
if (!codec) {
|
|
83
|
+
if (!metadata.codecId) {
|
|
30
84
|
return value;
|
|
31
85
|
}
|
|
32
86
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
}
|
|
87
|
+
const codec: Codec | undefined = registry.get(metadata.codecId);
|
|
88
|
+
if (!codec) {
|
|
89
|
+
return value;
|
|
42
90
|
}
|
|
43
91
|
|
|
44
|
-
|
|
92
|
+
try {
|
|
93
|
+
return await codec.encode(value, ctx);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
wrapEncodeFailure(error, metadata, paramIndex, codec.id);
|
|
96
|
+
}
|
|
45
97
|
}
|
|
46
98
|
|
|
47
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Encodes all parameters concurrently via `Promise.all`. Per parameter, sync-
|
|
101
|
+
* and async-authored codecs share the same path: `codec.encode → await →
|
|
102
|
+
* return`. Param-level failures are wrapped in `RUNTIME.ENCODE_FAILED`.
|
|
103
|
+
*
|
|
104
|
+
* When `ctx.signal` is provided:
|
|
105
|
+
*
|
|
106
|
+
* - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED`
|
|
107
|
+
* (`{ phase: 'encode' }`) before any `codec.encode` call is made — codecs
|
|
108
|
+
* can pin this with a per-call counter that stays at zero.
|
|
109
|
+
* - **Mid-flight abort** races the per-param `Promise.all` against
|
|
110
|
+
* `abortable(ctx.signal)`. The runtime returns `RUNTIME.ABORTED` promptly
|
|
111
|
+
* even if codec bodies ignore the signal; the in-flight bodies are
|
|
112
|
+
* abandoned and run to completion in the background (cooperative
|
|
113
|
+
* cancellation, see ADR 204).
|
|
114
|
+
* - Existing `RUNTIME.ENCODE_FAILED` envelopes that surface from a codec
|
|
115
|
+
* body before the runtime observes the abort pass through unchanged
|
|
116
|
+
* (no double wrap).
|
|
117
|
+
*/
|
|
118
|
+
export async function encodeParams(
|
|
119
|
+
plan: SqlExecutionPlan,
|
|
120
|
+
registry: CodecRegistry,
|
|
121
|
+
ctx: SqlCodecCallContext,
|
|
122
|
+
): Promise<readonly unknown[]> {
|
|
123
|
+
checkAborted(ctx, 'encode');
|
|
124
|
+
const signal = ctx.signal;
|
|
125
|
+
|
|
48
126
|
if (plan.params.length === 0) {
|
|
49
127
|
return plan.params;
|
|
50
128
|
}
|
|
51
129
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
for (let i = 0; i < plan.params.length; i++) {
|
|
55
|
-
const paramValue = plan.params[i];
|
|
56
|
-
const paramDescriptor = plan.meta.paramDescriptors[i];
|
|
130
|
+
const paramCount = plan.params.length;
|
|
131
|
+
const metadata: ParamMetadata[] = new Array(paramCount).fill(NO_METADATA);
|
|
57
132
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
133
|
+
if (plan.ast) {
|
|
134
|
+
const refs = collectOrderedParamRefs(plan.ast);
|
|
135
|
+
for (let i = 0; i < paramCount && i < refs.length; i++) {
|
|
136
|
+
const ref = refs[i];
|
|
137
|
+
if (ref) {
|
|
138
|
+
metadata[i] = { codecId: ref.codecId, name: ref.name };
|
|
139
|
+
}
|
|
62
140
|
}
|
|
63
141
|
}
|
|
64
142
|
|
|
65
|
-
|
|
143
|
+
const tasks: Promise<unknown>[] = new Array(paramCount);
|
|
144
|
+
for (let i = 0; i < paramCount; i++) {
|
|
145
|
+
tasks[i] = encodeParamValue(plan.params[i], metadata[i] ?? NO_METADATA, i, registry, ctx);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const settled = await raceAgainstAbort(Promise.all(tasks), signal, 'encode');
|
|
149
|
+
return Object.freeze(settled);
|
|
66
150
|
}
|
package/src/exports/index.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
export type {
|
|
2
2
|
AfterExecuteResult,
|
|
3
|
-
Log,
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|
package/src/lower-sql-plan.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Contract
|
|
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
|
-
):
|
|
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
|
+
}
|