@prisma-next/sql-runtime 0.5.0-dev.2 → 0.5.0-dev.21
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-BET5HxxT.mjs} +552 -171
- package/dist/exports-BET5HxxT.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 +10 -4
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +11 -13
- package/src/codecs/decoding.ts +172 -116
- package/src/codecs/encoding.ts +59 -21
- package/src/exports/index.ts +11 -7
- package/src/fingerprint.ts +22 -0
- package/src/guardrails/raw.ts +214 -0
- package/src/lower-sql-plan.ts +3 -3
- package/src/marker.ts +75 -0
- package/src/middleware/before-compile-chain.ts +32 -1
- package/src/middleware/budgets.ts +14 -11
- 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 +271 -110
- 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/sql-marker.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MarkerStatement } from '@prisma-next/
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 {
|
|
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
|
}
|
package/src/sql-runtime.ts
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import type { Contract
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
2
|
import type {
|
|
3
3
|
ExecutionStackInstance,
|
|
4
4
|
RuntimeDriverInstance,
|
|
5
5
|
} from '@prisma-next/framework-components/execution';
|
|
6
|
-
import { checkMiddlewareCompatibility } from '@prisma-next/framework-components/runtime';
|
|
7
|
-
import type {
|
|
8
|
-
Log,
|
|
9
|
-
RuntimeCore,
|
|
10
|
-
RuntimeCoreOptions,
|
|
11
|
-
RuntimeTelemetryEvent,
|
|
12
|
-
RuntimeVerifyOptions,
|
|
13
|
-
TelemetryOutcome,
|
|
14
|
-
} from '@prisma-next/runtime-executor';
|
|
15
6
|
import {
|
|
16
7
|
AsyncIterableResult,
|
|
17
|
-
|
|
8
|
+
checkMiddlewareCompatibility,
|
|
9
|
+
RuntimeCore,
|
|
10
|
+
type RuntimeLog,
|
|
18
11
|
runtimeError,
|
|
19
|
-
|
|
12
|
+
runWithMiddleware,
|
|
13
|
+
} from '@prisma-next/framework-components/runtime';
|
|
20
14
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
21
15
|
import type {
|
|
22
16
|
Adapter,
|
|
@@ -24,16 +18,26 @@ import type {
|
|
|
24
18
|
CodecRegistry,
|
|
25
19
|
LoweredStatement,
|
|
26
20
|
SqlDriver,
|
|
21
|
+
SqlQueryable,
|
|
22
|
+
SqlTransaction,
|
|
27
23
|
} from '@prisma-next/sql-relational-core/ast';
|
|
28
|
-
import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
24
|
+
import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
29
25
|
import type { JsonSchemaValidatorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
|
|
26
|
+
import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
|
|
30
27
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
31
28
|
import { decodeRow } from './codecs/decoding';
|
|
32
29
|
import { encodeParams } from './codecs/encoding';
|
|
33
30
|
import { validateCodecRegistryCompleteness } from './codecs/validation';
|
|
31
|
+
import { computeSqlFingerprint } from './fingerprint';
|
|
34
32
|
import { lowerSqlPlan } from './lower-sql-plan';
|
|
35
33
|
import { runBeforeCompileChain } from './middleware/before-compile-chain';
|
|
36
|
-
import type { SqlMiddleware } from './middleware/sql-middleware';
|
|
34
|
+
import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
|
|
35
|
+
import type {
|
|
36
|
+
RuntimeFamilyAdapter,
|
|
37
|
+
RuntimeTelemetryEvent,
|
|
38
|
+
RuntimeVerifyOptions,
|
|
39
|
+
TelemetryOutcome,
|
|
40
|
+
} from './runtime-spi';
|
|
37
41
|
import type {
|
|
38
42
|
ExecutionContext,
|
|
39
43
|
SqlRuntimeAdapterInstance,
|
|
@@ -41,6 +45,8 @@ import type {
|
|
|
41
45
|
} from './sql-context';
|
|
42
46
|
import { SqlFamilyAdapter } from './sql-family-adapter';
|
|
43
47
|
|
|
48
|
+
export type Log = RuntimeLog;
|
|
49
|
+
|
|
44
50
|
export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contract<SqlStorage>> {
|
|
45
51
|
readonly context: ExecutionContext<TContract>;
|
|
46
52
|
readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
@@ -107,39 +113,37 @@ export interface RuntimeTransaction extends RuntimeQueryable {
|
|
|
107
113
|
rollback(): Promise<void>;
|
|
108
114
|
}
|
|
109
115
|
|
|
110
|
-
export interface RuntimeQueryable {
|
|
111
|
-
execute<Row = Record<string, unknown>>(
|
|
112
|
-
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
113
|
-
): AsyncIterableResult<Row>;
|
|
114
|
-
}
|
|
116
|
+
export interface RuntimeQueryable extends RuntimeScope {}
|
|
115
117
|
|
|
116
118
|
export interface TransactionContext extends RuntimeQueryable {
|
|
117
119
|
readonly invalidated: boolean;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
interface CoreQueryable {
|
|
121
|
-
execute<Row = Record<string, unknown>>(plan: ExecutionPlan<Row>): AsyncIterableResult<Row>;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
122
|
export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
|
|
125
123
|
|
|
124
|
+
function isExecutionPlan(plan: SqlExecutionPlan | SqlQueryPlan): plan is SqlExecutionPlan {
|
|
125
|
+
return 'sql' in plan;
|
|
126
|
+
}
|
|
127
|
+
|
|
126
128
|
class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
|
|
129
|
+
extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>
|
|
127
130
|
implements Runtime
|
|
128
131
|
{
|
|
129
|
-
private readonly core: RuntimeCore<TContract, SqlDriver<unknown>, SqlMiddleware>;
|
|
130
132
|
private readonly contract: TContract;
|
|
131
133
|
private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
134
|
+
private readonly driver: SqlDriver<unknown>;
|
|
135
|
+
private readonly familyAdapter: RuntimeFamilyAdapter<Contract<SqlStorage>>;
|
|
132
136
|
private readonly codecRegistry: CodecRegistry;
|
|
133
137
|
private readonly jsonSchemaValidators: JsonSchemaValidatorRegistry | undefined;
|
|
138
|
+
private readonly sqlCtx: SqlMiddlewareContext;
|
|
139
|
+
private readonly verify: RuntimeVerifyOptions;
|
|
134
140
|
private codecRegistryValidated: boolean;
|
|
141
|
+
private verified: boolean;
|
|
142
|
+
private startupVerified: boolean;
|
|
143
|
+
private _telemetry: RuntimeTelemetryEvent | null;
|
|
135
144
|
|
|
136
145
|
constructor(options: RuntimeOptions<TContract>) {
|
|
137
146
|
const { context, adapter, driver, verify, middleware, mode, log } = options;
|
|
138
|
-
this.contract = context.contract;
|
|
139
|
-
this.adapter = adapter;
|
|
140
|
-
this.codecRegistry = context.codecs;
|
|
141
|
-
this.jsonSchemaValidators = context.jsonSchemaValidators;
|
|
142
|
-
this.codecRegistryValidated = false;
|
|
143
147
|
|
|
144
148
|
if (middleware) {
|
|
145
149
|
for (const mw of middleware) {
|
|
@@ -147,18 +151,31 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
const sqlCtx: SqlMiddlewareContext = {
|
|
155
|
+
contract: context.contract,
|
|
156
|
+
mode: mode ?? 'strict',
|
|
157
|
+
now: () => Date.now(),
|
|
158
|
+
log: log ?? {
|
|
159
|
+
info: () => {},
|
|
160
|
+
warn: () => {},
|
|
161
|
+
error: () => {},
|
|
162
|
+
},
|
|
159
163
|
};
|
|
160
164
|
|
|
161
|
-
|
|
165
|
+
super({ middleware: middleware ?? [], ctx: sqlCtx });
|
|
166
|
+
|
|
167
|
+
this.contract = context.contract;
|
|
168
|
+
this.adapter = adapter;
|
|
169
|
+
this.driver = driver;
|
|
170
|
+
this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
171
|
+
this.codecRegistry = context.codecs;
|
|
172
|
+
this.jsonSchemaValidators = context.jsonSchemaValidators;
|
|
173
|
+
this.sqlCtx = sqlCtx;
|
|
174
|
+
this.verify = verify;
|
|
175
|
+
this.codecRegistryValidated = false;
|
|
176
|
+
this.verified = verify.mode === 'startup' ? false : verify.mode === 'always';
|
|
177
|
+
this.startupVerified = false;
|
|
178
|
+
this._telemetry = null;
|
|
162
179
|
|
|
163
180
|
if (verify.mode === 'startup') {
|
|
164
181
|
validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
|
|
@@ -166,107 +183,251 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
166
183
|
}
|
|
167
184
|
}
|
|
168
185
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with
|
|
188
|
+
* encoded parameters ready for the driver. This is the single point at
|
|
189
|
+
* which params transition from app-layer values to driver wire-format.
|
|
190
|
+
*/
|
|
191
|
+
protected override async lower(plan: SqlQueryPlan): Promise<SqlExecutionPlan> {
|
|
192
|
+
const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
|
|
193
|
+
return Object.freeze({
|
|
194
|
+
...lowered,
|
|
195
|
+
params: await encodeParams(lowered, this.codecRegistry),
|
|
196
|
+
});
|
|
174
197
|
}
|
|
175
198
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Default driver invocation. Production execution paths override the
|
|
201
|
+
* queryable target (e.g. transaction or connection) by going through
|
|
202
|
+
* `executeAgainstQueryable`; this implementation supports any caller of
|
|
203
|
+
* `super.execute(plan)` and the abstract-base contract.
|
|
204
|
+
*/
|
|
205
|
+
protected override runDriver(exec: SqlExecutionPlan): AsyncIterable<Record<string, unknown>> {
|
|
206
|
+
return this.driver.execute<Record<string, unknown>>({
|
|
207
|
+
sql: exec.sql,
|
|
208
|
+
params: exec.params,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
186
211
|
|
|
212
|
+
/**
|
|
213
|
+
* SQL pre-compile hook. Runs the registered middleware `beforeCompile`
|
|
214
|
+
* chain over the plan's draft (AST + meta) and returns a `SqlQueryPlan`
|
|
215
|
+
* with the rewritten AST and meta when the chain mutates them. The chain
|
|
216
|
+
* re-derives `meta.paramDescriptors` from the rewritten AST so descriptors
|
|
217
|
+
* stay in lockstep with the params the adapter will emit during lowering.
|
|
218
|
+
*/
|
|
219
|
+
protected override async runBeforeCompile(plan: SqlQueryPlan): Promise<SqlQueryPlan> {
|
|
187
220
|
const rewrittenDraft = await runBeforeCompileChain(
|
|
188
|
-
this.
|
|
221
|
+
this.middleware,
|
|
189
222
|
{ ast: plan.ast, meta: plan.meta },
|
|
190
|
-
this.
|
|
223
|
+
this.sqlCtx,
|
|
191
224
|
);
|
|
225
|
+
return rewrittenDraft.ast === plan.ast
|
|
226
|
+
? plan
|
|
227
|
+
: { ...plan, ast: rewrittenDraft.ast, meta: rewrittenDraft.meta };
|
|
228
|
+
}
|
|
192
229
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return
|
|
230
|
+
override execute<Row>(
|
|
231
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
232
|
+
): AsyncIterableResult<Row> {
|
|
233
|
+
return this.executeAgainstQueryable<Row>(plan, this.driver);
|
|
197
234
|
}
|
|
198
235
|
|
|
199
|
-
private executeAgainstQueryable<Row
|
|
200
|
-
plan:
|
|
201
|
-
queryable:
|
|
236
|
+
private executeAgainstQueryable<Row>(
|
|
237
|
+
plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
|
|
238
|
+
queryable: SqlQueryable,
|
|
202
239
|
): AsyncIterableResult<Row> {
|
|
203
|
-
this.ensureCodecRegistryValidated(
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
240
|
+
this.ensureCodecRegistryValidated();
|
|
241
|
+
|
|
242
|
+
const self = this;
|
|
243
|
+
const generator = async function* (): AsyncGenerator<Row, void, unknown> {
|
|
244
|
+
const exec: SqlExecutionPlan = isExecutionPlan(plan)
|
|
245
|
+
? Object.freeze({
|
|
246
|
+
...plan,
|
|
247
|
+
params: await encodeParams(plan, self.codecRegistry),
|
|
248
|
+
})
|
|
249
|
+
: await self.lower(await self.runBeforeCompile(plan));
|
|
250
|
+
|
|
251
|
+
self.familyAdapter.validatePlan(exec, self.contract);
|
|
252
|
+
self._telemetry = null;
|
|
253
|
+
|
|
254
|
+
if (!self.startupVerified && self.verify.mode === 'startup') {
|
|
255
|
+
await self.verifyMarker();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!self.verified && self.verify.mode === 'onFirstUse') {
|
|
259
|
+
await self.verifyMarker();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const startedAt = Date.now();
|
|
263
|
+
let outcome: TelemetryOutcome | null = null;
|
|
214
264
|
|
|
215
|
-
|
|
265
|
+
try {
|
|
266
|
+
if (self.verify.mode === 'always') {
|
|
267
|
+
await self.verifyMarker();
|
|
268
|
+
}
|
|
216
269
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
270
|
+
const stream = runWithMiddleware<SqlExecutionPlan, Record<string, unknown>>(
|
|
271
|
+
exec,
|
|
272
|
+
self.middleware,
|
|
273
|
+
self.ctx,
|
|
274
|
+
() =>
|
|
275
|
+
queryable.execute<Record<string, unknown>>({
|
|
276
|
+
sql: exec.sql,
|
|
277
|
+
params: exec.params,
|
|
278
|
+
}),
|
|
223
279
|
);
|
|
224
|
-
|
|
280
|
+
|
|
281
|
+
for await (const rawRow of stream) {
|
|
282
|
+
const decodedRow = await decodeRow(
|
|
283
|
+
rawRow,
|
|
284
|
+
exec,
|
|
285
|
+
self.codecRegistry,
|
|
286
|
+
self.jsonSchemaValidators,
|
|
287
|
+
);
|
|
288
|
+
yield decodedRow as Row;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
outcome = 'success';
|
|
292
|
+
} catch (error) {
|
|
293
|
+
outcome = 'runtime-error';
|
|
294
|
+
throw error;
|
|
295
|
+
} finally {
|
|
296
|
+
if (outcome !== null) {
|
|
297
|
+
self.recordTelemetry(exec, outcome, Date.now() - startedAt);
|
|
298
|
+
}
|
|
225
299
|
}
|
|
226
300
|
};
|
|
227
301
|
|
|
228
|
-
return new AsyncIterableResult(
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
execute<Row = Record<string, unknown>>(
|
|
232
|
-
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
233
|
-
): AsyncIterableResult<Row> {
|
|
234
|
-
return this.executeAgainstQueryable(plan, this.core);
|
|
302
|
+
return new AsyncIterableResult(generator());
|
|
235
303
|
}
|
|
236
304
|
|
|
237
305
|
async connection(): Promise<RuntimeConnection> {
|
|
238
|
-
const
|
|
306
|
+
const driverConn = await this.driver.acquireConnection();
|
|
239
307
|
const self = this;
|
|
308
|
+
|
|
240
309
|
const wrappedConnection: RuntimeConnection = {
|
|
241
310
|
async transaction(): Promise<RuntimeTransaction> {
|
|
242
|
-
const
|
|
243
|
-
return
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
248
|
-
): AsyncIterableResult<Row> {
|
|
249
|
-
return self.executeAgainstQueryable(plan, coreTx);
|
|
250
|
-
},
|
|
251
|
-
};
|
|
311
|
+
const driverTx = await driverConn.beginTransaction();
|
|
312
|
+
return self.wrapTransaction(driverTx);
|
|
313
|
+
},
|
|
314
|
+
async release(): Promise<void> {
|
|
315
|
+
await driverConn.release();
|
|
252
316
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
317
|
+
async destroy(reason?: unknown): Promise<void> {
|
|
318
|
+
await driverConn.destroy(reason);
|
|
319
|
+
},
|
|
320
|
+
execute<Row>(
|
|
321
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
257
322
|
): AsyncIterableResult<Row> {
|
|
258
|
-
return self.executeAgainstQueryable(plan,
|
|
323
|
+
return self.executeAgainstQueryable<Row>(plan, driverConn);
|
|
259
324
|
},
|
|
260
325
|
};
|
|
326
|
+
|
|
261
327
|
return wrappedConnection;
|
|
262
328
|
}
|
|
263
329
|
|
|
330
|
+
private wrapTransaction(driverTx: SqlTransaction): RuntimeTransaction {
|
|
331
|
+
const self = this;
|
|
332
|
+
return {
|
|
333
|
+
async commit(): Promise<void> {
|
|
334
|
+
await driverTx.commit();
|
|
335
|
+
},
|
|
336
|
+
async rollback(): Promise<void> {
|
|
337
|
+
await driverTx.rollback();
|
|
338
|
+
},
|
|
339
|
+
execute<Row>(
|
|
340
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
341
|
+
): AsyncIterableResult<Row> {
|
|
342
|
+
return self.executeAgainstQueryable<Row>(plan, driverTx);
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
264
347
|
telemetry(): RuntimeTelemetryEvent | null {
|
|
265
|
-
return this.
|
|
348
|
+
return this._telemetry;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async close(): Promise<void> {
|
|
352
|
+
await this.driver.close();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private ensureCodecRegistryValidated(): void {
|
|
356
|
+
if (!this.codecRegistryValidated) {
|
|
357
|
+
validateCodecRegistryCompleteness(this.codecRegistry, this.contract);
|
|
358
|
+
this.codecRegistryValidated = true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private async verifyMarker(): Promise<void> {
|
|
363
|
+
if (this.verify.mode === 'always') {
|
|
364
|
+
this.verified = false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (this.verified) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
|
|
372
|
+
const result = await this.driver.query(readStatement.sql, readStatement.params);
|
|
373
|
+
|
|
374
|
+
if (result.rows.length === 0) {
|
|
375
|
+
if (this.verify.requireMarker) {
|
|
376
|
+
throw runtimeError('CONTRACT.MARKER_MISSING', 'Contract marker not found in database');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.verified = true;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const marker = this.familyAdapter.markerReader.parseMarkerRow(result.rows[0]);
|
|
384
|
+
|
|
385
|
+
const contract = this.contract as {
|
|
386
|
+
storage: { storageHash: string };
|
|
387
|
+
execution?: { executionHash?: string | null };
|
|
388
|
+
profileHash?: string | null;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (marker.storageHash !== contract.storage.storageHash) {
|
|
392
|
+
throw runtimeError(
|
|
393
|
+
'CONTRACT.MARKER_MISMATCH',
|
|
394
|
+
'Database storage hash does not match contract',
|
|
395
|
+
{
|
|
396
|
+
expected: contract.storage.storageHash,
|
|
397
|
+
actual: marker.storageHash,
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const expectedProfile = contract.profileHash ?? null;
|
|
403
|
+
if (expectedProfile !== null && marker.profileHash !== expectedProfile) {
|
|
404
|
+
throw runtimeError(
|
|
405
|
+
'CONTRACT.MARKER_MISMATCH',
|
|
406
|
+
'Database profile hash does not match contract',
|
|
407
|
+
{
|
|
408
|
+
expectedProfile,
|
|
409
|
+
actualProfile: marker.profileHash,
|
|
410
|
+
},
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.verified = true;
|
|
415
|
+
this.startupVerified = true;
|
|
266
416
|
}
|
|
267
417
|
|
|
268
|
-
|
|
269
|
-
|
|
418
|
+
private recordTelemetry(
|
|
419
|
+
plan: SqlExecutionPlan,
|
|
420
|
+
outcome: TelemetryOutcome,
|
|
421
|
+
durationMs?: number,
|
|
422
|
+
): void {
|
|
423
|
+
const contract = this.contract as { target: string };
|
|
424
|
+
this._telemetry = Object.freeze({
|
|
425
|
+
lane: plan.meta.lane,
|
|
426
|
+
target: contract.target,
|
|
427
|
+
fingerprint: computeSqlFingerprint(plan.sql),
|
|
428
|
+
outcome,
|
|
429
|
+
...(durationMs !== undefined ? { durationMs } : {}),
|
|
430
|
+
});
|
|
270
431
|
}
|
|
271
432
|
}
|
|
272
433
|
|
|
@@ -290,8 +451,8 @@ export async function withTransaction<R>(
|
|
|
290
451
|
get invalidated() {
|
|
291
452
|
return invalidated;
|
|
292
453
|
},
|
|
293
|
-
execute<Row
|
|
294
|
-
plan:
|
|
454
|
+
execute<Row>(
|
|
455
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
295
456
|
): AsyncIterableResult<Row> {
|
|
296
457
|
if (invalidated) {
|
|
297
458
|
throw transactionClosedError();
|