@prisma-next/sql-runtime 0.5.0-dev.7 → 0.5.0-dev.71
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-CXtbKm5q.mjs +1516 -0
- package/dist/exports-CXtbKm5q.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-C4Dz0JKE.d.mts} +116 -45
- package/dist/index-C4Dz0JKE.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 -56
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +17 -18
- package/src/codecs/alias-resolver.ts +34 -0
- package/src/codecs/decoding.ts +263 -176
- package/src/codecs/encoding.ts +151 -38
- 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 -0
- package/src/middleware/budgets.ts +36 -120
- 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-context.ts +315 -105
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +89 -51
- package/src/sql-runtime.ts +305 -144
- package/dist/exports-BQZSVXXt.mjs +0 -981
- package/dist/exports-BQZSVXXt.mjs.map +0 -1
- package/dist/index-yb51L_1h.d.mts.map +0 -1
- package/src/codecs/json-schema-validation.ts +0 -61
- 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-runtime.ts
CHANGED
|
@@ -1,39 +1,48 @@
|
|
|
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 type {
|
|
27
|
+
import { validateParamRefRefs } from '@prisma-next/sql-relational-core/ast';
|
|
28
|
+
import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
29
|
+
import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
|
|
30
|
+
import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
|
|
30
31
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
31
32
|
import { decodeRow } from './codecs/decoding';
|
|
32
33
|
import { encodeParams } from './codecs/encoding';
|
|
33
34
|
import { validateCodecRegistryCompleteness } from './codecs/validation';
|
|
35
|
+
import { computeSqlContentHash } from './content-hash';
|
|
36
|
+
import { computeSqlFingerprint } from './fingerprint';
|
|
34
37
|
import { lowerSqlPlan } from './lower-sql-plan';
|
|
35
38
|
import { runBeforeCompileChain } from './middleware/before-compile-chain';
|
|
36
|
-
import type { SqlMiddleware } from './middleware/sql-middleware';
|
|
39
|
+
import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
|
|
40
|
+
import type {
|
|
41
|
+
RuntimeFamilyAdapter,
|
|
42
|
+
RuntimeTelemetryEvent,
|
|
43
|
+
RuntimeVerifyOptions,
|
|
44
|
+
TelemetryOutcome,
|
|
45
|
+
} from './runtime-spi';
|
|
37
46
|
import type {
|
|
38
47
|
ExecutionContext,
|
|
39
48
|
SqlRuntimeAdapterInstance,
|
|
@@ -41,6 +50,8 @@ import type {
|
|
|
41
50
|
} from './sql-context';
|
|
42
51
|
import { SqlFamilyAdapter } from './sql-family-adapter';
|
|
43
52
|
|
|
53
|
+
export type Log = RuntimeLog;
|
|
54
|
+
|
|
44
55
|
export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contract<SqlStorage>> {
|
|
45
56
|
readonly context: ExecutionContext<TContract>;
|
|
46
57
|
readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
@@ -79,25 +90,15 @@ export interface Runtime extends RuntimeQueryable {
|
|
|
79
90
|
export interface RuntimeConnection extends RuntimeQueryable {
|
|
80
91
|
transaction(): Promise<RuntimeTransaction>;
|
|
81
92
|
/**
|
|
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.
|
|
93
|
+
* 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
94
|
*/
|
|
87
95
|
release(): Promise<void>;
|
|
88
96
|
/**
|
|
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).
|
|
97
|
+
* 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
98
|
*
|
|
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.
|
|
99
|
+
* 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
100
|
*
|
|
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.
|
|
101
|
+
* `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
102
|
*/
|
|
102
103
|
destroy(reason?: unknown): Promise<void>;
|
|
103
104
|
}
|
|
@@ -107,39 +108,41 @@ export interface RuntimeTransaction extends RuntimeQueryable {
|
|
|
107
108
|
rollback(): Promise<void>;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
export interface RuntimeQueryable {
|
|
111
|
-
execute<Row = Record<string, unknown>>(
|
|
112
|
-
plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
113
|
-
): AsyncIterableResult<Row>;
|
|
114
|
-
}
|
|
111
|
+
export interface RuntimeQueryable extends RuntimeScope {}
|
|
115
112
|
|
|
116
113
|
export interface TransactionContext extends RuntimeQueryable {
|
|
117
114
|
readonly invalidated: boolean;
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
|
|
118
|
+
|
|
119
|
+
function isExecutionPlan(plan: SqlExecutionPlan | SqlQueryPlan): plan is SqlExecutionPlan {
|
|
120
|
+
return 'sql' in plan;
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
|
|
123
|
+
// v8 ignore next 2
|
|
124
|
+
const noopLogSink = (): void => {};
|
|
125
|
+
const noopLog: Log = { info: noopLogSink, warn: noopLogSink, error: noopLogSink };
|
|
125
126
|
|
|
126
127
|
class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
|
|
128
|
+
extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>
|
|
127
129
|
implements Runtime
|
|
128
130
|
{
|
|
129
|
-
private readonly core: RuntimeCore<TContract, SqlDriver<unknown>, SqlMiddleware>;
|
|
130
131
|
private readonly contract: TContract;
|
|
131
132
|
private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
132
|
-
private readonly
|
|
133
|
-
private readonly
|
|
133
|
+
private readonly driver: SqlDriver<unknown>;
|
|
134
|
+
private readonly familyAdapter: RuntimeFamilyAdapter<Contract<SqlStorage>>;
|
|
135
|
+
private readonly contractCodecs: ContractCodecRegistry;
|
|
136
|
+
private readonly codecDescriptors: CodecDescriptorRegistry;
|
|
137
|
+
private readonly sqlCtx: SqlMiddlewareContext;
|
|
138
|
+
private readonly verify: RuntimeVerifyOptions;
|
|
134
139
|
private codecRegistryValidated: boolean;
|
|
140
|
+
private verified: boolean;
|
|
141
|
+
private startupVerified: boolean;
|
|
142
|
+
private _telemetry: RuntimeTelemetryEvent | null;
|
|
135
143
|
|
|
136
144
|
constructor(options: RuntimeOptions<TContract>) {
|
|
137
145
|
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
146
|
|
|
144
147
|
if (middleware) {
|
|
145
148
|
for (const mw of middleware) {
|
|
@@ -147,126 +150,295 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
...ifDefined('mode', mode),
|
|
158
|
-
...ifDefined('log', log),
|
|
153
|
+
const sqlCtx: SqlMiddlewareContext = {
|
|
154
|
+
contract: context.contract,
|
|
155
|
+
mode: mode ?? 'strict',
|
|
156
|
+
now: () => Date.now(),
|
|
157
|
+
log: log ?? noopLog,
|
|
158
|
+
// ctx is only invoked by runWithMiddleware with execs this runtime lowered; the framework parameter type is the cross-family base.
|
|
159
|
+
contentHash: (exec) => computeSqlContentHash(exec as SqlExecutionPlan),
|
|
159
160
|
};
|
|
160
161
|
|
|
161
|
-
|
|
162
|
+
super({ middleware: middleware ?? [], ctx: sqlCtx });
|
|
163
|
+
|
|
164
|
+
this.contract = context.contract;
|
|
165
|
+
this.adapter = adapter;
|
|
166
|
+
this.driver = driver;
|
|
167
|
+
this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
168
|
+
this.contractCodecs = context.contractCodecs;
|
|
169
|
+
this.codecDescriptors = context.codecDescriptors;
|
|
170
|
+
this.sqlCtx = sqlCtx;
|
|
171
|
+
this.verify = verify;
|
|
172
|
+
this.codecRegistryValidated = false;
|
|
173
|
+
this.verified = verify.mode === 'startup' ? false : verify.mode === 'always';
|
|
174
|
+
this.startupVerified = false;
|
|
175
|
+
this._telemetry = null;
|
|
162
176
|
|
|
163
177
|
if (verify.mode === 'startup') {
|
|
164
|
-
validateCodecRegistryCompleteness(this.
|
|
178
|
+
validateCodecRegistryCompleteness(this.codecDescriptors, context.contract);
|
|
165
179
|
this.codecRegistryValidated = true;
|
|
166
180
|
}
|
|
167
181
|
}
|
|
168
182
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
183
|
+
/**
|
|
184
|
+
* 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.
|
|
185
|
+
*
|
|
186
|
+
* `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.
|
|
187
|
+
*/
|
|
188
|
+
protected override async lower(
|
|
189
|
+
plan: SqlQueryPlan,
|
|
190
|
+
ctx: SqlCodecCallContext,
|
|
191
|
+
): Promise<SqlExecutionPlan> {
|
|
192
|
+
validateParamRefRefs(plan.ast, this.codecDescriptors);
|
|
193
|
+
const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
|
|
194
|
+
return Object.freeze({
|
|
195
|
+
...lowered,
|
|
196
|
+
params: await encodeParams(lowered, ctx, this.contractCodecs),
|
|
197
|
+
});
|
|
174
198
|
}
|
|
175
199
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
200
|
+
/**
|
|
201
|
+
* 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.
|
|
202
|
+
*/
|
|
203
|
+
// v8 ignore next 6
|
|
204
|
+
protected override runDriver(exec: SqlExecutionPlan): AsyncIterable<Record<string, unknown>> {
|
|
205
|
+
return this.driver.execute<Record<string, unknown>>({
|
|
206
|
+
sql: exec.sql,
|
|
207
|
+
params: exec.params,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
186
210
|
|
|
211
|
+
/**
|
|
212
|
+
* 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
|
|
213
|
+
* AST directly.
|
|
214
|
+
*/
|
|
215
|
+
protected override async runBeforeCompile(plan: SqlQueryPlan): Promise<SqlQueryPlan> {
|
|
187
216
|
const rewrittenDraft = await runBeforeCompileChain(
|
|
188
|
-
this.
|
|
217
|
+
this.middleware,
|
|
189
218
|
{ ast: plan.ast, meta: plan.meta },
|
|
190
|
-
this.
|
|
219
|
+
this.sqlCtx,
|
|
191
220
|
);
|
|
221
|
+
return rewrittenDraft.ast === plan.ast
|
|
222
|
+
? plan
|
|
223
|
+
: { ...plan, ast: rewrittenDraft.ast, meta: rewrittenDraft.meta };
|
|
224
|
+
}
|
|
192
225
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
226
|
+
override execute<Row>(
|
|
227
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
228
|
+
options?: RuntimeExecuteOptions,
|
|
229
|
+
): AsyncIterableResult<Row> {
|
|
230
|
+
return this.executeAgainstQueryable<Row>(plan, this.driver, options);
|
|
197
231
|
}
|
|
198
232
|
|
|
199
|
-
private executeAgainstQueryable<Row
|
|
200
|
-
plan:
|
|
201
|
-
queryable:
|
|
233
|
+
private executeAgainstQueryable<Row>(
|
|
234
|
+
plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
|
|
235
|
+
queryable: SqlQueryable,
|
|
236
|
+
options?: RuntimeExecuteOptions,
|
|
202
237
|
): AsyncIterableResult<Row> {
|
|
203
|
-
this.ensureCodecRegistryValidated(
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
238
|
+
this.ensureCodecRegistryValidated();
|
|
239
|
+
|
|
240
|
+
const self = this;
|
|
241
|
+
const signal = options?.signal;
|
|
242
|
+
// 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).
|
|
243
|
+
const codecCtx: SqlCodecCallContext = signal === undefined ? {} : { signal };
|
|
244
|
+
|
|
245
|
+
const generator = async function* (): AsyncGenerator<Row, void, unknown> {
|
|
246
|
+
checkAborted(codecCtx, 'stream');
|
|
247
|
+
|
|
248
|
+
let exec: SqlExecutionPlan;
|
|
249
|
+
if (isExecutionPlan(plan)) {
|
|
250
|
+
if (plan.ast) {
|
|
251
|
+
validateParamRefRefs(plan.ast, self.codecDescriptors);
|
|
252
|
+
}
|
|
253
|
+
exec = Object.freeze({
|
|
254
|
+
...plan,
|
|
255
|
+
params: await encodeParams(plan, codecCtx, self.contractCodecs),
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
exec = await self.lower(await self.runBeforeCompile(plan), codecCtx);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
self.familyAdapter.validatePlan(exec, self.contract);
|
|
262
|
+
self._telemetry = null;
|
|
263
|
+
|
|
264
|
+
if (!self.startupVerified && self.verify.mode === 'startup') {
|
|
265
|
+
await self.verifyMarker();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!self.verified && self.verify.mode === 'onFirstUse') {
|
|
269
|
+
await self.verifyMarker();
|
|
270
|
+
}
|
|
214
271
|
|
|
215
|
-
const
|
|
272
|
+
const startedAt = Date.now();
|
|
273
|
+
let outcome: TelemetryOutcome | null = null;
|
|
216
274
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
275
|
+
try {
|
|
276
|
+
if (self.verify.mode === 'always') {
|
|
277
|
+
await self.verifyMarker();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const stream = runWithMiddleware<SqlExecutionPlan, Record<string, unknown>>(
|
|
281
|
+
exec,
|
|
282
|
+
self.middleware,
|
|
283
|
+
self.ctx,
|
|
284
|
+
() =>
|
|
285
|
+
queryable.execute<Record<string, unknown>>({
|
|
286
|
+
sql: exec.sql,
|
|
287
|
+
params: exec.params,
|
|
288
|
+
}),
|
|
223
289
|
);
|
|
224
|
-
|
|
290
|
+
|
|
291
|
+
// 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.
|
|
292
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
293
|
+
try {
|
|
294
|
+
while (true) {
|
|
295
|
+
checkAborted(codecCtx, 'stream');
|
|
296
|
+
const next = await iterator.next();
|
|
297
|
+
if (next.done) {
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
const decodedRow = await decodeRow(next.value, exec, codecCtx, self.contractCodecs);
|
|
301
|
+
yield decodedRow as Row;
|
|
302
|
+
}
|
|
303
|
+
} finally {
|
|
304
|
+
// Best-effort iterator cleanup so the driver can release its resources whether the stream finished normally, threw, or was abandoned by the consumer.
|
|
305
|
+
await iterator.return?.();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
outcome = 'success';
|
|
309
|
+
} catch (error) {
|
|
310
|
+
outcome = 'runtime-error';
|
|
311
|
+
throw error;
|
|
312
|
+
} finally {
|
|
313
|
+
if (outcome !== null) {
|
|
314
|
+
self.recordTelemetry(exec, outcome, Date.now() - startedAt);
|
|
315
|
+
}
|
|
225
316
|
}
|
|
226
317
|
};
|
|
227
318
|
|
|
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);
|
|
319
|
+
return new AsyncIterableResult(generator());
|
|
235
320
|
}
|
|
236
321
|
|
|
237
322
|
async connection(): Promise<RuntimeConnection> {
|
|
238
|
-
const
|
|
323
|
+
const driverConn = await this.driver.acquireConnection();
|
|
239
324
|
const self = this;
|
|
325
|
+
|
|
240
326
|
const wrappedConnection: RuntimeConnection = {
|
|
241
327
|
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
|
-
};
|
|
328
|
+
const driverTx = await driverConn.beginTransaction();
|
|
329
|
+
return self.wrapTransaction(driverTx);
|
|
330
|
+
},
|
|
331
|
+
async release(): Promise<void> {
|
|
332
|
+
await driverConn.release();
|
|
252
333
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
334
|
+
async destroy(reason?: unknown): Promise<void> {
|
|
335
|
+
await driverConn.destroy(reason);
|
|
336
|
+
},
|
|
337
|
+
execute<Row>(
|
|
338
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
339
|
+
options?: RuntimeExecuteOptions,
|
|
257
340
|
): AsyncIterableResult<Row> {
|
|
258
|
-
return self.executeAgainstQueryable(plan,
|
|
341
|
+
return self.executeAgainstQueryable<Row>(plan, driverConn, options);
|
|
259
342
|
},
|
|
260
343
|
};
|
|
344
|
+
|
|
261
345
|
return wrappedConnection;
|
|
262
346
|
}
|
|
263
347
|
|
|
348
|
+
private wrapTransaction(driverTx: SqlTransaction): RuntimeTransaction {
|
|
349
|
+
const self = this;
|
|
350
|
+
return {
|
|
351
|
+
async commit(): Promise<void> {
|
|
352
|
+
await driverTx.commit();
|
|
353
|
+
},
|
|
354
|
+
async rollback(): Promise<void> {
|
|
355
|
+
await driverTx.rollback();
|
|
356
|
+
},
|
|
357
|
+
execute<Row>(
|
|
358
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
359
|
+
options?: RuntimeExecuteOptions,
|
|
360
|
+
): AsyncIterableResult<Row> {
|
|
361
|
+
return self.executeAgainstQueryable<Row>(plan, driverTx, options);
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
264
366
|
telemetry(): RuntimeTelemetryEvent | null {
|
|
265
|
-
return this.
|
|
367
|
+
return this._telemetry;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async close(): Promise<void> {
|
|
371
|
+
await this.driver.close();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private ensureCodecRegistryValidated(): void {
|
|
375
|
+
if (!this.codecRegistryValidated) {
|
|
376
|
+
validateCodecRegistryCompleteness(this.codecDescriptors, this.contract);
|
|
377
|
+
this.codecRegistryValidated = true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private async verifyMarker(): Promise<void> {
|
|
382
|
+
const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
|
|
383
|
+
const result = await this.driver.query(readStatement.sql, readStatement.params);
|
|
384
|
+
|
|
385
|
+
if (result.rows.length === 0) {
|
|
386
|
+
if (this.verify.requireMarker) {
|
|
387
|
+
throw runtimeError('CONTRACT.MARKER_MISSING', 'Contract marker not found in database');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.verified = true;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const marker = this.familyAdapter.markerReader.parseMarkerRow(result.rows[0]);
|
|
395
|
+
|
|
396
|
+
const contract = this.contract as {
|
|
397
|
+
storage: { storageHash: string };
|
|
398
|
+
execution?: { executionHash?: string | null };
|
|
399
|
+
profileHash?: string | null;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
if (marker.storageHash !== contract.storage.storageHash) {
|
|
403
|
+
throw runtimeError(
|
|
404
|
+
'CONTRACT.MARKER_MISMATCH',
|
|
405
|
+
'Database storage hash does not match contract',
|
|
406
|
+
{
|
|
407
|
+
expected: contract.storage.storageHash,
|
|
408
|
+
actual: marker.storageHash,
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const expectedProfile = contract.profileHash ?? null;
|
|
414
|
+
if (expectedProfile !== null && marker.profileHash !== expectedProfile) {
|
|
415
|
+
throw runtimeError(
|
|
416
|
+
'CONTRACT.MARKER_MISMATCH',
|
|
417
|
+
'Database profile hash does not match contract',
|
|
418
|
+
{
|
|
419
|
+
expectedProfile,
|
|
420
|
+
actualProfile: marker.profileHash,
|
|
421
|
+
},
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.verified = true;
|
|
426
|
+
this.startupVerified = true;
|
|
266
427
|
}
|
|
267
428
|
|
|
268
|
-
|
|
269
|
-
|
|
429
|
+
private recordTelemetry(
|
|
430
|
+
plan: SqlExecutionPlan,
|
|
431
|
+
outcome: TelemetryOutcome,
|
|
432
|
+
durationMs?: number,
|
|
433
|
+
): void {
|
|
434
|
+
const contract = this.contract as { target: string };
|
|
435
|
+
this._telemetry = Object.freeze({
|
|
436
|
+
lane: plan.meta.lane,
|
|
437
|
+
target: contract.target,
|
|
438
|
+
fingerprint: computeSqlFingerprint(plan.sql),
|
|
439
|
+
outcome,
|
|
440
|
+
...(durationMs !== undefined ? { durationMs } : {}),
|
|
441
|
+
});
|
|
270
442
|
}
|
|
271
443
|
}
|
|
272
444
|
|
|
@@ -290,13 +462,14 @@ export async function withTransaction<R>(
|
|
|
290
462
|
get invalidated() {
|
|
291
463
|
return invalidated;
|
|
292
464
|
},
|
|
293
|
-
execute<Row
|
|
294
|
-
plan:
|
|
465
|
+
execute<Row>(
|
|
466
|
+
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
467
|
+
options?: RuntimeExecuteOptions,
|
|
295
468
|
): AsyncIterableResult<Row> {
|
|
296
469
|
if (invalidated) {
|
|
297
470
|
throw transactionClosedError();
|
|
298
471
|
}
|
|
299
|
-
const inner = transaction.execute(plan);
|
|
472
|
+
const inner = transaction.execute(plan, options);
|
|
300
473
|
const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
|
|
301
474
|
for await (const row of inner) {
|
|
302
475
|
if (invalidated) {
|
|
@@ -313,11 +486,7 @@ export async function withTransaction<R>(
|
|
|
313
486
|
const destroyConnection = async (reason: unknown): Promise<void> => {
|
|
314
487
|
if (connectionDisposed) return;
|
|
315
488
|
connectionDisposed = true;
|
|
316
|
-
// SqlConnection.destroy() propagates teardown errors so callers can
|
|
317
|
-
// decide what to do with them. Here, we're already about to throw a
|
|
318
|
-
// more informative error describing why we're evicting the connection
|
|
319
|
-
// (rollback/commit failure), so swallowing the teardown error is the
|
|
320
|
-
// right call — surfacing it would mask the original cause.
|
|
489
|
+
// 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.
|
|
321
490
|
await connection.destroy(reason).catch(() => undefined);
|
|
322
491
|
};
|
|
323
492
|
|
|
@@ -346,17 +515,9 @@ export async function withTransaction<R>(
|
|
|
346
515
|
try {
|
|
347
516
|
await transaction.commit();
|
|
348
517
|
} catch (commitError) {
|
|
349
|
-
// After a failed COMMIT the server-side transaction may be: (a) already
|
|
350
|
-
// committed (error on response path), (b) already rolled back (deferred
|
|
351
|
-
// constraint / serialization failure), or (c) still open (COMMIT never
|
|
352
|
-
// reached the server). Attempt a best-effort rollback to cover (c) and
|
|
353
|
-
// confirm the protocol is healthy.
|
|
518
|
+
// 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.
|
|
354
519
|
//
|
|
355
|
-
// If rollback succeeds, the server is definitely no longer in a
|
|
356
|
-
// transaction (no-op in (a)/(b), real cleanup in (c)) and we've just
|
|
357
|
-
// proved the connection round-trips correctly — it's safe to return
|
|
358
|
-
// to the pool. If rollback fails, the connection state is ambiguous
|
|
359
|
-
// (broken socket, protocol desync, etc.) and we must destroy it.
|
|
520
|
+
// 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.
|
|
360
521
|
try {
|
|
361
522
|
await transaction.rollback();
|
|
362
523
|
} catch {
|