@prisma-next/sql-runtime 0.5.0-dev.8 → 0.5.0-dev.9

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