@prisma-next/sql-runtime 0.5.0-dev.8 → 0.5.0-dev.80
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 +31 -22
- package/dist/exports-C0exnDCV.mjs +1514 -0
- package/dist/exports-C0exnDCV.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-CFbuVnYJ.d.mts} +130 -45
- package/dist/index-CFbuVnYJ.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -3
- package/dist/test/utils.d.mts +38 -33
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +107 -61
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +17 -17
- package/src/codecs/alias-resolver.ts +37 -0
- package/src/codecs/decoding.ts +168 -130
- package/src/codecs/encoding.ts +123 -47
- package/src/codecs/validation.ts +4 -4
- package/src/content-hash.ts +44 -0
- package/src/exports/index.ts +13 -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 -31
- package/src/middleware/budgets.ts +36 -120
- package/src/middleware/lints.ts +20 -26
- package/src/middleware/sql-middleware.ts +25 -5
- package/src/runtime-spi.ts +44 -0
- package/src/sql-context.ts +315 -105
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +89 -51
- package/src/sql-runtime.ts +325 -146
- package/dist/exports-Cssiepsb.mjs +0 -1068
- package/dist/exports-Cssiepsb.mjs.map +0 -1
- package/dist/index-yb51L_1h.d.mts.map +0 -1
- package/src/codecs/json-schema-validation.ts +0 -61
package/src/sql-runtime.ts
CHANGED
|
@@ -1,39 +1,53 @@
|
|
|
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
|
+
checkAborted,
|
|
9
|
+
checkMiddlewareCompatibility,
|
|
10
|
+
RuntimeCore,
|
|
11
|
+
type RuntimeExecuteOptions,
|
|
12
|
+
type RuntimeLog,
|
|
18
13
|
runtimeError,
|
|
19
|
-
|
|
14
|
+
runWithMiddleware,
|
|
15
|
+
} from '@prisma-next/framework-components/runtime';
|
|
20
16
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
21
17
|
import type {
|
|
22
18
|
Adapter,
|
|
23
19
|
AnyQueryAst,
|
|
24
|
-
|
|
20
|
+
ContractCodecRegistry,
|
|
25
21
|
LoweredStatement,
|
|
22
|
+
SqlCodecCallContext,
|
|
26
23
|
SqlDriver,
|
|
24
|
+
SqlQueryable,
|
|
25
|
+
SqlTransaction,
|
|
27
26
|
} from '@prisma-next/sql-relational-core/ast';
|
|
28
|
-
import
|
|
29
|
-
import
|
|
27
|
+
import { validateParamRefRefs } from '@prisma-next/sql-relational-core/ast';
|
|
28
|
+
import {
|
|
29
|
+
createSqlParamRefMutator,
|
|
30
|
+
type SqlParamRefMutator,
|
|
31
|
+
type SqlParamRefMutatorInternal,
|
|
32
|
+
} from '@prisma-next/sql-relational-core/middleware';
|
|
33
|
+
import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
34
|
+
import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
|
|
35
|
+
import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
|
|
30
36
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
31
37
|
import { decodeRow } from './codecs/decoding';
|
|
32
38
|
import { encodeParams } from './codecs/encoding';
|
|
33
39
|
import { validateCodecRegistryCompleteness } from './codecs/validation';
|
|
40
|
+
import { computeSqlContentHash } from './content-hash';
|
|
41
|
+
import { computeSqlFingerprint } from './fingerprint';
|
|
34
42
|
import { lowerSqlPlan } from './lower-sql-plan';
|
|
35
43
|
import { runBeforeCompileChain } from './middleware/before-compile-chain';
|
|
36
|
-
import type { SqlMiddleware } from './middleware/sql-middleware';
|
|
44
|
+
import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
|
|
45
|
+
import type {
|
|
46
|
+
RuntimeFamilyAdapter,
|
|
47
|
+
RuntimeTelemetryEvent,
|
|
48
|
+
RuntimeVerifyOptions,
|
|
49
|
+
TelemetryOutcome,
|
|
50
|
+
} from './runtime-spi';
|
|
37
51
|
import type {
|
|
38
52
|
ExecutionContext,
|
|
39
53
|
SqlRuntimeAdapterInstance,
|
|
@@ -41,6 +55,8 @@ import type {
|
|
|
41
55
|
} from './sql-context';
|
|
42
56
|
import { SqlFamilyAdapter } from './sql-family-adapter';
|
|
43
57
|
|
|
58
|
+
export type Log = RuntimeLog;
|
|
59
|
+
|
|
44
60
|
export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contract<SqlStorage>> {
|
|
45
61
|
readonly context: ExecutionContext<TContract>;
|
|
46
62
|
readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
@@ -79,25 +95,15 @@ export interface Runtime extends RuntimeQueryable {
|
|
|
79
95
|
export interface RuntimeConnection extends RuntimeQueryable {
|
|
80
96
|
transaction(): Promise<RuntimeTransaction>;
|
|
81
97
|
/**
|
|
82
|
-
* Returns the connection to the pool for reuse. Only call this when the
|
|
83
|
-
* connection is known to be in a clean state. If a transaction
|
|
84
|
-
* commit/rollback failed or the connection is otherwise suspect, call
|
|
85
|
-
* `destroy(reason)` instead.
|
|
98
|
+
* Returns the connection to the pool for reuse. Only call this when the connection is known to be in a clean state. If a transaction commit/rollback failed or the connection is otherwise suspect, call `destroy(reason)` instead.
|
|
86
99
|
*/
|
|
87
100
|
release(): Promise<void>;
|
|
88
101
|
/**
|
|
89
|
-
* Evicts the connection so it is never reused. Call this when the
|
|
90
|
-
* connection may be in an indeterminate state (e.g. a failed rollback
|
|
91
|
-
* leaving an open transaction, or a broken socket).
|
|
102
|
+
* Evicts the connection so it is never reused. Call this when the connection may be in an indeterminate state (e.g. a failed rollback leaving an open transaction, or a broken socket).
|
|
92
103
|
*
|
|
93
|
-
* If teardown fails the error is propagated and the connection remains
|
|
94
|
-
* retryable, so the caller can decide whether to swallow the failure or
|
|
95
|
-
* retry cleanup. Calling destroy() or release() more than once after a
|
|
96
|
-
* successful teardown is caller error.
|
|
104
|
+
* If teardown fails the error is propagated and the connection remains retryable, so the caller can decide whether to swallow the failure or retry cleanup. Calling destroy() or release() more than once after a successful teardown is caller error.
|
|
97
105
|
*
|
|
98
|
-
* `reason` is advisory context only. It may be surfaced to driver-level
|
|
99
|
-
* observability hooks (e.g. pg-pool's `'release'` event) but does not
|
|
100
|
-
* influence eviction behavior and is not rethrown.
|
|
106
|
+
* `reason` is advisory context only. It may be surfaced to driver-level observability hooks (e.g. pg-pool's `'release'` event) but does not influence eviction behavior and is not rethrown.
|
|
101
107
|
*/
|
|
102
108
|
destroy(reason?: unknown): Promise<void>;
|
|
103
109
|
}
|
|
@@ -107,39 +113,41 @@ 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
|
-
|
|
121
|
-
|
|
122
|
+
export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
|
|
123
|
+
|
|
124
|
+
function isExecutionPlan(plan: SqlExecutionPlan | SqlQueryPlan): plan is SqlExecutionPlan {
|
|
125
|
+
return 'sql' in plan;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
// v8 ignore next 2
|
|
129
|
+
const noopLogSink = (): void => {};
|
|
130
|
+
const noopLog: Log = { info: noopLogSink, warn: noopLogSink, error: noopLogSink };
|
|
125
131
|
|
|
126
132
|
class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
|
|
133
|
+
extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>
|
|
127
134
|
implements Runtime
|
|
128
135
|
{
|
|
129
|
-
private readonly core: RuntimeCore<TContract, SqlDriver<unknown>, SqlMiddleware>;
|
|
130
136
|
private readonly contract: TContract;
|
|
131
137
|
private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
132
|
-
private readonly
|
|
133
|
-
private readonly
|
|
138
|
+
private readonly driver: SqlDriver<unknown>;
|
|
139
|
+
private readonly familyAdapter: RuntimeFamilyAdapter<Contract<SqlStorage>>;
|
|
140
|
+
private readonly contractCodecs: ContractCodecRegistry;
|
|
141
|
+
private readonly codecDescriptors: CodecDescriptorRegistry;
|
|
142
|
+
private readonly sqlCtx: SqlMiddlewareContext;
|
|
143
|
+
private readonly verify: RuntimeVerifyOptions;
|
|
134
144
|
private codecRegistryValidated: boolean;
|
|
145
|
+
private verified: boolean;
|
|
146
|
+
private startupVerified: boolean;
|
|
147
|
+
private _telemetry: RuntimeTelemetryEvent | null;
|
|
135
148
|
|
|
136
149
|
constructor(options: RuntimeOptions<TContract>) {
|
|
137
150
|
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
151
|
|
|
144
152
|
if (middleware) {
|
|
145
153
|
for (const mw of middleware) {
|
|
@@ -147,128 +155,310 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
157
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
...ifDefined('mode', mode),
|
|
158
|
-
...ifDefined('log', log),
|
|
158
|
+
const sqlCtx: SqlMiddlewareContext = {
|
|
159
|
+
contract: context.contract,
|
|
160
|
+
mode: mode ?? 'strict',
|
|
161
|
+
now: () => Date.now(),
|
|
162
|
+
log: log ?? noopLog,
|
|
163
|
+
// ctx is only invoked by runWithMiddleware with execs this runtime lowered; the framework parameter type is the cross-family base.
|
|
164
|
+
contentHash: (exec) => computeSqlContentHash(exec as SqlExecutionPlan),
|
|
159
165
|
};
|
|
160
166
|
|
|
161
|
-
|
|
167
|
+
super({ middleware: middleware ?? [], ctx: sqlCtx });
|
|
168
|
+
|
|
169
|
+
this.contract = context.contract;
|
|
170
|
+
this.adapter = adapter;
|
|
171
|
+
this.driver = driver;
|
|
172
|
+
this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
173
|
+
this.contractCodecs = context.contractCodecs;
|
|
174
|
+
this.codecDescriptors = context.codecDescriptors;
|
|
175
|
+
this.sqlCtx = sqlCtx;
|
|
176
|
+
this.verify = verify;
|
|
177
|
+
this.codecRegistryValidated = false;
|
|
178
|
+
this.verified = verify.mode === 'startup' ? false : verify.mode === 'always';
|
|
179
|
+
this.startupVerified = false;
|
|
180
|
+
this._telemetry = null;
|
|
162
181
|
|
|
163
182
|
if (verify.mode === 'startup') {
|
|
164
|
-
validateCodecRegistryCompleteness(this.
|
|
183
|
+
validateCodecRegistryCompleteness(this.codecDescriptors, context.contract);
|
|
165
184
|
this.codecRegistryValidated = true;
|
|
166
185
|
}
|
|
167
186
|
}
|
|
168
187
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with encoded parameters ready for the driver. This is the single point at which params transition from app-layer values to driver wire-format.
|
|
190
|
+
*
|
|
191
|
+
* `ctx: SqlCodecCallContext` is forwarded to `encodeParams` so per-query cancellation reaches every codec body during parameter encoding. The framework abstract typed this as `CodecCallContext`; the SQL family narrows it to the SQL-specific extension. SQL params do not populate `ctx.column` — encode-side column metadata is the middleware's domain.
|
|
192
|
+
*/
|
|
193
|
+
protected override async lower(
|
|
194
|
+
plan: SqlQueryPlan,
|
|
195
|
+
ctx: SqlCodecCallContext,
|
|
196
|
+
): Promise<SqlExecutionPlan> {
|
|
197
|
+
validateParamRefRefs(plan.ast, this.codecDescriptors);
|
|
198
|
+
const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
|
|
199
|
+
return Object.freeze({
|
|
200
|
+
...lowered,
|
|
201
|
+
params: await encodeParams(lowered, ctx, this.contractCodecs),
|
|
202
|
+
});
|
|
174
203
|
}
|
|
175
204
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Default driver invocation required by the abstract `RuntimeCore` contract. Every production path overrides `execute()` and routes through `executeAgainstQueryable`, so this hook is defensive only — subclasses that delegate back to `super.execute()` would land here.
|
|
207
|
+
*/
|
|
208
|
+
// v8 ignore next 6
|
|
209
|
+
protected override runDriver(exec: SqlExecutionPlan): AsyncIterable<Record<string, unknown>> {
|
|
210
|
+
return this.driver.execute<Record<string, unknown>>({
|
|
211
|
+
sql: exec.sql,
|
|
212
|
+
params: exec.params,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
186
215
|
|
|
216
|
+
/**
|
|
217
|
+
* SQL pre-compile hook. Runs the registered middleware `beforeCompile` chain over the plan's draft (AST + meta). Returns the original plan unchanged when no middleware rewrote the AST; otherwise returns a new plan carrying the rewritten AST and meta. The AST is the authoritative source of execution metadata, so a rewrite needs no sidecar reconciliation here — the lowering adapter and the encoder both walk the rewritten
|
|
218
|
+
* AST directly.
|
|
219
|
+
*/
|
|
220
|
+
protected override async runBeforeCompile(plan: SqlQueryPlan): Promise<SqlQueryPlan> {
|
|
187
221
|
const rewrittenDraft = await runBeforeCompileChain(
|
|
188
|
-
this.
|
|
222
|
+
this.middleware,
|
|
189
223
|
{ ast: plan.ast, meta: plan.meta },
|
|
190
|
-
this.
|
|
224
|
+
this.sqlCtx,
|
|
191
225
|
);
|
|
226
|
+
return rewrittenDraft.ast === plan.ast
|
|
227
|
+
? plan
|
|
228
|
+
: { ...plan, ast: rewrittenDraft.ast, meta: rewrittenDraft.meta };
|
|
229
|
+
}
|
|
192
230
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return lowerSqlPlan(this.adapter, this.contract, planToLower);
|
|
231
|
+
override execute<Row>(
|
|
232
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
233
|
+
options?: RuntimeExecuteOptions,
|
|
234
|
+
): AsyncIterableResult<Row> {
|
|
235
|
+
return this.executeAgainstQueryable<Row>(plan, this.driver, options);
|
|
199
236
|
}
|
|
200
237
|
|
|
201
|
-
private executeAgainstQueryable<Row
|
|
202
|
-
plan:
|
|
203
|
-
queryable:
|
|
238
|
+
private executeAgainstQueryable<Row>(
|
|
239
|
+
plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
|
|
240
|
+
queryable: SqlQueryable,
|
|
241
|
+
options?: RuntimeExecuteOptions,
|
|
204
242
|
): AsyncIterableResult<Row> {
|
|
205
|
-
this.ensureCodecRegistryValidated(
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
243
|
+
this.ensureCodecRegistryValidated();
|
|
244
|
+
|
|
245
|
+
const self = this;
|
|
246
|
+
const signal = options?.signal;
|
|
247
|
+
// One ctx per execute() call — the same reference is shared by encodeParams (lower), decodeRow (per-row), and the stream loop's between-row checks. Per-cell ctx allocations inside decodeField add `column` for resolvable cells without re-wrapping the signal. The ctx object is always allocated; the `signal` field is only included when a signal was supplied (exactOptionalPropertyTypes).
|
|
248
|
+
const codecCtx: SqlCodecCallContext = signal === undefined ? {} : { signal };
|
|
249
|
+
// Per-execute view of the middleware ctx that carries the per-query
|
|
250
|
+
// signal. `self.ctx` is allocated once at construction (no signal); we
|
|
251
|
+
// shallow-clone it here so middleware sees the same `AbortSignal`
|
|
252
|
+
// reference threaded into `codecCtx.signal` (ADR 207 identity).
|
|
253
|
+
const execMiddlewareCtx = signal === undefined ? self.ctx : { ...self.ctx, signal };
|
|
254
|
+
|
|
255
|
+
const generator = async function* (): AsyncGenerator<Row, void, unknown> {
|
|
256
|
+
checkAborted(codecCtx, 'stream');
|
|
257
|
+
|
|
258
|
+
let exec: SqlExecutionPlan;
|
|
259
|
+
if (isExecutionPlan(plan)) {
|
|
260
|
+
if (plan.ast) {
|
|
261
|
+
validateParamRefRefs(plan.ast, self.codecDescriptors);
|
|
262
|
+
}
|
|
263
|
+
exec = Object.freeze({
|
|
264
|
+
...plan,
|
|
265
|
+
params: await encodeParams(plan, codecCtx, self.contractCodecs),
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
exec = await self.lower(await self.runBeforeCompile(plan), codecCtx);
|
|
269
|
+
}
|
|
216
270
|
|
|
217
|
-
|
|
271
|
+
self.familyAdapter.validatePlan(exec, self.contract);
|
|
272
|
+
self._telemetry = null;
|
|
218
273
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
274
|
+
if (!self.startupVerified && self.verify.mode === 'startup') {
|
|
275
|
+
await self.verifyMarker();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!self.verified && self.verify.mode === 'onFirstUse') {
|
|
279
|
+
await self.verifyMarker();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const startedAt = Date.now();
|
|
283
|
+
let outcome: TelemetryOutcome | null = null;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
if (self.verify.mode === 'always') {
|
|
287
|
+
await self.verifyMarker();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const paramsMutator: SqlParamRefMutatorInternal = createSqlParamRefMutator(exec);
|
|
291
|
+
const stream = runWithMiddleware<
|
|
292
|
+
SqlExecutionPlan,
|
|
293
|
+
Record<string, unknown>,
|
|
294
|
+
SqlParamRefMutator
|
|
295
|
+
>(
|
|
296
|
+
exec,
|
|
297
|
+
self.middleware,
|
|
298
|
+
execMiddlewareCtx,
|
|
299
|
+
() =>
|
|
300
|
+
queryable.execute<Record<string, unknown>>({
|
|
301
|
+
sql: exec.sql,
|
|
302
|
+
// Read params after the `beforeExecute` middleware chain has
|
|
303
|
+
// run so that any `mutator.replaceValue(...)` calls land in
|
|
304
|
+
// the driver-bound params array. When no middleware mutated,
|
|
305
|
+
// `currentParams()` returns `exec.params` by reference identity.
|
|
306
|
+
params: paramsMutator.currentParams(),
|
|
307
|
+
}),
|
|
308
|
+
paramsMutator,
|
|
225
309
|
);
|
|
226
|
-
|
|
310
|
+
|
|
311
|
+
// Manually drive the driver's async iterator so the between-row abort check fires *before* requesting the next row. With a `for await...of` loop the runtime would await `iterator.next()` first, leaving a window where one extra row is pulled through the driver after the signal aborted.
|
|
312
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
313
|
+
try {
|
|
314
|
+
while (true) {
|
|
315
|
+
checkAborted(codecCtx, 'stream');
|
|
316
|
+
const next = await iterator.next();
|
|
317
|
+
if (next.done) {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
const decodedRow = await decodeRow(next.value, exec, codecCtx, self.contractCodecs);
|
|
321
|
+
yield decodedRow as Row;
|
|
322
|
+
}
|
|
323
|
+
} finally {
|
|
324
|
+
// Best-effort iterator cleanup so the driver can release its resources whether the stream finished normally, threw, or was abandoned by the consumer.
|
|
325
|
+
await iterator.return?.();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
outcome = 'success';
|
|
329
|
+
} catch (error) {
|
|
330
|
+
outcome = 'runtime-error';
|
|
331
|
+
throw error;
|
|
332
|
+
} finally {
|
|
333
|
+
if (outcome !== null) {
|
|
334
|
+
self.recordTelemetry(exec, outcome, Date.now() - startedAt);
|
|
335
|
+
}
|
|
227
336
|
}
|
|
228
337
|
};
|
|
229
338
|
|
|
230
|
-
return new AsyncIterableResult(
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
execute<Row = Record<string, unknown>>(
|
|
234
|
-
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
235
|
-
): AsyncIterableResult<Row> {
|
|
236
|
-
return this.executeAgainstQueryable(plan, this.core);
|
|
339
|
+
return new AsyncIterableResult(generator());
|
|
237
340
|
}
|
|
238
341
|
|
|
239
342
|
async connection(): Promise<RuntimeConnection> {
|
|
240
|
-
const
|
|
343
|
+
const driverConn = await this.driver.acquireConnection();
|
|
241
344
|
const self = this;
|
|
345
|
+
|
|
242
346
|
const wrappedConnection: RuntimeConnection = {
|
|
243
347
|
async transaction(): Promise<RuntimeTransaction> {
|
|
244
|
-
const
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
250
|
-
): AsyncIterableResult<Row> {
|
|
251
|
-
return self.executeAgainstQueryable(plan, coreTx);
|
|
252
|
-
},
|
|
253
|
-
};
|
|
348
|
+
const driverTx = await driverConn.beginTransaction();
|
|
349
|
+
return self.wrapTransaction(driverTx);
|
|
350
|
+
},
|
|
351
|
+
async release(): Promise<void> {
|
|
352
|
+
await driverConn.release();
|
|
254
353
|
},
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
354
|
+
async destroy(reason?: unknown): Promise<void> {
|
|
355
|
+
await driverConn.destroy(reason);
|
|
356
|
+
},
|
|
357
|
+
execute<Row>(
|
|
358
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
359
|
+
options?: RuntimeExecuteOptions,
|
|
259
360
|
): AsyncIterableResult<Row> {
|
|
260
|
-
return self.executeAgainstQueryable(plan,
|
|
361
|
+
return self.executeAgainstQueryable<Row>(plan, driverConn, options);
|
|
261
362
|
},
|
|
262
363
|
};
|
|
364
|
+
|
|
263
365
|
return wrappedConnection;
|
|
264
366
|
}
|
|
265
367
|
|
|
368
|
+
private wrapTransaction(driverTx: SqlTransaction): RuntimeTransaction {
|
|
369
|
+
const self = this;
|
|
370
|
+
return {
|
|
371
|
+
async commit(): Promise<void> {
|
|
372
|
+
await driverTx.commit();
|
|
373
|
+
},
|
|
374
|
+
async rollback(): Promise<void> {
|
|
375
|
+
await driverTx.rollback();
|
|
376
|
+
},
|
|
377
|
+
execute<Row>(
|
|
378
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
379
|
+
options?: RuntimeExecuteOptions,
|
|
380
|
+
): AsyncIterableResult<Row> {
|
|
381
|
+
return self.executeAgainstQueryable<Row>(plan, driverTx, options);
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
266
386
|
telemetry(): RuntimeTelemetryEvent | null {
|
|
267
|
-
return this.
|
|
387
|
+
return this._telemetry;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async close(): Promise<void> {
|
|
391
|
+
await this.driver.close();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private ensureCodecRegistryValidated(): void {
|
|
395
|
+
if (!this.codecRegistryValidated) {
|
|
396
|
+
validateCodecRegistryCompleteness(this.codecDescriptors, this.contract);
|
|
397
|
+
this.codecRegistryValidated = true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async verifyMarker(): Promise<void> {
|
|
402
|
+
const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
|
|
403
|
+
const result = await this.driver.query(readStatement.sql, readStatement.params);
|
|
404
|
+
|
|
405
|
+
if (result.rows.length === 0) {
|
|
406
|
+
if (this.verify.requireMarker) {
|
|
407
|
+
throw runtimeError('CONTRACT.MARKER_MISSING', 'Contract marker not found in database');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.verified = true;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const marker = this.familyAdapter.markerReader.parseMarkerRow(result.rows[0]);
|
|
415
|
+
|
|
416
|
+
const contract = this.contract as {
|
|
417
|
+
storage: { storageHash: string };
|
|
418
|
+
execution?: { executionHash?: string | null };
|
|
419
|
+
profileHash?: string | null;
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
if (marker.storageHash !== contract.storage.storageHash) {
|
|
423
|
+
throw runtimeError(
|
|
424
|
+
'CONTRACT.MARKER_MISMATCH',
|
|
425
|
+
'Database storage hash does not match contract',
|
|
426
|
+
{
|
|
427
|
+
expected: contract.storage.storageHash,
|
|
428
|
+
actual: marker.storageHash,
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const expectedProfile = contract.profileHash ?? null;
|
|
434
|
+
if (expectedProfile !== null && marker.profileHash !== expectedProfile) {
|
|
435
|
+
throw runtimeError(
|
|
436
|
+
'CONTRACT.MARKER_MISMATCH',
|
|
437
|
+
'Database profile hash does not match contract',
|
|
438
|
+
{
|
|
439
|
+
expectedProfile,
|
|
440
|
+
actualProfile: marker.profileHash,
|
|
441
|
+
},
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
this.verified = true;
|
|
446
|
+
this.startupVerified = true;
|
|
268
447
|
}
|
|
269
448
|
|
|
270
|
-
|
|
271
|
-
|
|
449
|
+
private recordTelemetry(
|
|
450
|
+
plan: SqlExecutionPlan,
|
|
451
|
+
outcome: TelemetryOutcome,
|
|
452
|
+
durationMs?: number,
|
|
453
|
+
): void {
|
|
454
|
+
const contract = this.contract as { target: string };
|
|
455
|
+
this._telemetry = Object.freeze({
|
|
456
|
+
lane: plan.meta.lane,
|
|
457
|
+
target: contract.target,
|
|
458
|
+
fingerprint: computeSqlFingerprint(plan.sql),
|
|
459
|
+
outcome,
|
|
460
|
+
...(durationMs !== undefined ? { durationMs } : {}),
|
|
461
|
+
});
|
|
272
462
|
}
|
|
273
463
|
}
|
|
274
464
|
|
|
@@ -292,13 +482,14 @@ export async function withTransaction<R>(
|
|
|
292
482
|
get invalidated() {
|
|
293
483
|
return invalidated;
|
|
294
484
|
},
|
|
295
|
-
execute<Row
|
|
296
|
-
plan:
|
|
485
|
+
execute<Row>(
|
|
486
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
487
|
+
options?: RuntimeExecuteOptions,
|
|
297
488
|
): AsyncIterableResult<Row> {
|
|
298
489
|
if (invalidated) {
|
|
299
490
|
throw transactionClosedError();
|
|
300
491
|
}
|
|
301
|
-
const inner = transaction.execute(plan);
|
|
492
|
+
const inner = transaction.execute(plan, options);
|
|
302
493
|
const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
|
|
303
494
|
for await (const row of inner) {
|
|
304
495
|
if (invalidated) {
|
|
@@ -315,11 +506,7 @@ export async function withTransaction<R>(
|
|
|
315
506
|
const destroyConnection = async (reason: unknown): Promise<void> => {
|
|
316
507
|
if (connectionDisposed) return;
|
|
317
508
|
connectionDisposed = true;
|
|
318
|
-
// SqlConnection.destroy() propagates teardown errors so callers can
|
|
319
|
-
// decide what to do with them. Here, we're already about to throw a
|
|
320
|
-
// more informative error describing why we're evicting the connection
|
|
321
|
-
// (rollback/commit failure), so swallowing the teardown error is the
|
|
322
|
-
// right call — surfacing it would mask the original cause.
|
|
509
|
+
// SqlConnection.destroy() propagates teardown errors so callers can decide what to do with them. Here, we're already about to throw a more informative error describing why we're evicting the connection (rollback/commit failure), so swallowing the teardown error is the right call — surfacing it would mask the original cause.
|
|
323
510
|
await connection.destroy(reason).catch(() => undefined);
|
|
324
511
|
};
|
|
325
512
|
|
|
@@ -348,17 +535,9 @@ export async function withTransaction<R>(
|
|
|
348
535
|
try {
|
|
349
536
|
await transaction.commit();
|
|
350
537
|
} catch (commitError) {
|
|
351
|
-
// After a failed COMMIT the server-side transaction may be: (a) already
|
|
352
|
-
// committed (error on response path), (b) already rolled back (deferred
|
|
353
|
-
// constraint / serialization failure), or (c) still open (COMMIT never
|
|
354
|
-
// reached the server). Attempt a best-effort rollback to cover (c) and
|
|
355
|
-
// confirm the protocol is healthy.
|
|
538
|
+
// After a failed COMMIT the server-side transaction may be: (a) already committed (error on response path), (b) already rolled back (deferred constraint / serialization failure), or (c) still open (COMMIT never reached the server). Attempt a best-effort rollback to cover (c) and confirm the protocol is healthy.
|
|
356
539
|
//
|
|
357
|
-
// If rollback succeeds, the server is definitely no longer in a
|
|
358
|
-
// transaction (no-op in (a)/(b), real cleanup in (c)) and we've just
|
|
359
|
-
// proved the connection round-trips correctly — it's safe to return
|
|
360
|
-
// to the pool. If rollback fails, the connection state is ambiguous
|
|
361
|
-
// (broken socket, protocol desync, etc.) and we must destroy it.
|
|
540
|
+
// If rollback succeeds, the server is definitely no longer in a transaction (no-op in (a)/(b), real cleanup in (c)) and we've just proved the connection round-trips correctly — it's safe to return to the pool. If rollback fails, the connection state is ambiguous (broken socket, protocol desync, etc.) and we must destroy it.
|
|
362
541
|
try {
|
|
363
542
|
await transaction.rollback();
|
|
364
543
|
} catch {
|