@prisma-next/sql-runtime 0.4.0-dev.9 → 0.4.2

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 +1 @@
1
- {"version":3,"file":"utils.mjs","names":["collectAsync"],"sources":["../../test/utils.ts"],"sourcesContent":["import type { Contract, ExecutionPlan, ResultType } from '@prisma-next/contract/types';\nimport { coreHash, profileHash } from '@prisma-next/contract/types';\nimport {\n instantiateExecutionStack,\n type RuntimeDriverDescriptor,\n} from '@prisma-next/framework-components/execution';\nimport { builtinGeneratorIds } from '@prisma-next/ids';\nimport { generateId } from '@prisma-next/ids/runtime';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\nimport type { Adapter, LoweredStatement, SelectAst } from '@prisma-next/sql-relational-core/ast';\nimport { codec, createCodecRegistry } from '@prisma-next/sql-relational-core/ast';\nimport type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';\nimport { collectAsync, drainAsyncIterable } from '@prisma-next/test-utils';\nimport type { Client } from 'pg';\nimport type { SqlStatement } from '../src/exports';\nimport {\n createExecutionContext,\n type createRuntime,\n createSqlExecutionStack,\n ensureSchemaStatement,\n ensureTableStatement,\n writeContractMarker,\n} from '../src/exports';\nimport type {\n ExecutionContext,\n SqlRuntimeAdapterDescriptor,\n SqlRuntimeAdapterInstance,\n SqlRuntimeDriverInstance,\n SqlRuntimeExtensionDescriptor,\n SqlRuntimeTargetDescriptor,\n} from '../src/sql-context';\n\nfunction createTestMutationDefaultGenerators() {\n return builtinGeneratorIds.map((id) => ({\n id,\n generate: (params?: Record<string, unknown>) => generateId(params ? { id, params } : { id }),\n }));\n}\n\n/**\n * Executes a plan and collects all results into an array.\n * This helper DRYs up the common pattern of executing plans in tests.\n * The return type is inferred from the plan's type parameter.\n */\nexport async function executePlanAndCollect<\n P extends ExecutionPlan<ResultType<P>> | SqlQueryPlan<ResultType<P>>,\n>(runtime: ReturnType<typeof createRuntime>, plan: P): Promise<ResultType<P>[]> {\n type Row = ResultType<P>;\n return collectAsync<Row>(runtime.execute<Row>(plan));\n}\n\n/**\n * Drains a plan execution, consuming all results without collecting them.\n * Useful for testing side effects without memory overhead.\n */\nexport async function drainPlanExecution(\n runtime: ReturnType<typeof createRuntime>,\n plan: ExecutionPlan | SqlQueryPlan<unknown>,\n): Promise<void> {\n return drainAsyncIterable(runtime.execute(plan));\n}\n\n/**\n * Executes a SQL statement on a database client.\n */\nexport async function executeStatement(client: Client, statement: SqlStatement): Promise<void> {\n if (statement.params.length > 0) {\n await client.query(statement.sql, [...statement.params]);\n return;\n }\n\n await client.query(statement.sql);\n}\n\n/**\n * Sets up database schema and data, then writes the contract marker.\n * This helper DRYs up the common pattern of database setup in tests.\n */\nexport async function setupTestDatabase(\n client: Client,\n contract: Contract<SqlStorage>,\n setupFn: (client: Client) => Promise<void>,\n): Promise<void> {\n await client.query('drop schema if exists prisma_contract cascade');\n await client.query('create schema if not exists public');\n\n await setupFn(client);\n\n await executeStatement(client, ensureSchemaStatement);\n await executeStatement(client, ensureTableStatement);\n const write = writeContractMarker({\n storageHash: contract.storage.storageHash,\n profileHash: contract.profileHash,\n contractJson: contract,\n canonicalVersion: 1,\n });\n await executeStatement(client, write.insert);\n}\n\n/**\n * Writes a contract marker to the database.\n * This helper DRYs up the common pattern of writing contract markers in tests.\n */\nexport async function writeTestContractMarker(\n client: Client,\n contract: Contract<SqlStorage>,\n): Promise<void> {\n const write = writeContractMarker({\n storageHash: contract.storage.storageHash,\n profileHash: contract.profileHash,\n contractJson: contract,\n canonicalVersion: 1,\n });\n await executeStatement(client, write.insert);\n}\n\n/**\n * Creates a test adapter descriptor from a raw adapter.\n * Wraps the adapter in an SqlRuntimeAdapterDescriptor with static contributions\n * derived from the adapter's codec registry.\n */\nexport function createTestAdapterDescriptor(\n adapter: Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement>,\n): SqlRuntimeAdapterDescriptor<'postgres'> {\n const codecRegistry = adapter.profile.codecs();\n return {\n kind: 'adapter' as const,\n id: 'test-adapter',\n version: '0.0.1',\n familyId: 'sql' as const,\n targetId: 'postgres' as const,\n codecs: () => codecRegistry,\n parameterizedCodecs: () => [],\n mutationDefaultGenerators: createTestMutationDefaultGenerators,\n create(): SqlRuntimeAdapterInstance<'postgres'> {\n return Object.assign({ familyId: 'sql' as const, targetId: 'postgres' as const }, adapter);\n },\n };\n}\n\n/**\n * Creates a test target descriptor with empty static contributions.\n */\nexport function createTestTargetDescriptor(): SqlRuntimeTargetDescriptor<'postgres'> {\n return {\n kind: 'target' as const,\n id: 'postgres',\n version: '0.0.1',\n familyId: 'sql' as const,\n targetId: 'postgres' as const,\n codecs: () => createCodecRegistry(),\n parameterizedCodecs: () => [],\n create() {\n return { familyId: 'sql' as const, targetId: 'postgres' as const };\n },\n };\n}\n\n/**\n * Creates an ExecutionContext for testing.\n * This helper DRYs up the common pattern of context creation in tests.\n *\n * Accepts a raw adapter and optional extension descriptors, wrapping the\n * adapter in a descriptor internally for descriptor-first context creation.\n */\nexport function createTestContext<TContract extends Contract<SqlStorage>>(\n contract: TContract,\n adapter: Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement>,\n options?: {\n extensionPacks?: ReadonlyArray<SqlRuntimeExtensionDescriptor<'postgres'>>;\n },\n): ExecutionContext<TContract> {\n return createExecutionContext({\n contract,\n stack: {\n target: createTestTargetDescriptor(),\n adapter: createTestAdapterDescriptor(adapter),\n extensionPacks: options?.extensionPacks ?? [],\n },\n });\n}\n\nexport function createTestStackInstance(options?: {\n extensionPacks?: ReadonlyArray<SqlRuntimeExtensionDescriptor<'postgres'>>;\n driver?: RuntimeDriverDescriptor<\n 'sql',\n 'postgres',\n unknown,\n SqlRuntimeDriverInstance<'postgres'>\n >;\n}) {\n const stack = createSqlExecutionStack({\n target: createTestTargetDescriptor(),\n adapter: createTestAdapterDescriptor(createStubAdapter()),\n driver: options?.driver,\n extensionPacks: options?.extensionPacks ?? [],\n });\n\n return instantiateExecutionStack(stack);\n}\n\n/**\n * Creates a stub adapter for testing.\n * This helper DRYs up the common pattern of adapter creation in tests.\n *\n * The stub adapter includes simple codecs for common test types (pg/int4@1, pg/text@1, pg/timestamptz@1)\n * to enable type inference in tests without requiring the postgres adapter package.\n */\nexport function createStubAdapter(): Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement> {\n const codecRegistry = createCodecRegistry();\n\n // Register stub codecs for common test types\n // These match the codec IDs used in test contracts (pg/int4@1, pg/text@1, pg/timestamptz@1)\n // but don't require importing from the postgres adapter package\n codecRegistry.register(\n codec({\n typeId: 'pg/int4@1',\n targetTypes: ['int4'],\n encode: (value: number) => value,\n decode: (wire: number) => wire,\n }),\n );\n\n codecRegistry.register(\n codec({\n typeId: 'pg/text@1',\n targetTypes: ['text'],\n encode: (value: string) => value,\n decode: (wire: string) => wire,\n }),\n );\n\n codecRegistry.register(\n codec({\n typeId: 'pg/timestamptz@1',\n targetTypes: ['timestamptz'],\n encode: (value: string | Date) => (value instanceof Date ? value.toISOString() : value),\n decode: (wire: string | Date) => (wire instanceof Date ? wire : new Date(wire)),\n }),\n );\n\n return {\n profile: {\n id: 'stub-profile',\n target: 'postgres',\n capabilities: {},\n codecs() {\n return codecRegistry;\n },\n readMarkerStatement() {\n return {\n sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta from prisma_contract.marker where id = $1',\n params: [1],\n };\n },\n },\n lower(ast: SelectAst, ctx: { contract: Contract<SqlStorage>; params?: readonly unknown[] }) {\n const sqlText = JSON.stringify(ast);\n return {\n profileId: this.profile.id,\n body: Object.freeze({ sql: sqlText, params: ctx.params ? [...ctx.params] : [] }),\n };\n },\n };\n}\n\nexport function createTestContract(\n contract: Partial<Omit<Contract<SqlStorage>, 'profileHash' | 'storage'>> & {\n storageHash?: string;\n profileHash?: string;\n storage?: Omit<SqlStorage, 'storageHash'>;\n },\n): Contract<SqlStorage> {\n const { execution, ...rest } = contract;\n const storageHashValue = coreHash(rest['storageHash'] ?? 'sha256:testcore');\n\n return {\n target: rest['target'] ?? 'postgres',\n targetFamily: rest['targetFamily'] ?? 'sql',\n storage: rest['storage']\n ? { ...rest['storage'], storageHash: storageHashValue }\n : { storageHash: storageHashValue, tables: {} },\n models: rest['models'] ?? {},\n roots: rest['roots'] ?? {},\n capabilities: rest['capabilities'] ?? {},\n extensionPacks: rest['extensionPacks'] ?? {},\n meta: rest['meta'] ?? {},\n ...(execution ? { execution } : {}),\n profileHash: profileHash(rest['profileHash'] ?? 'sha256:testprofile'),\n };\n}\n\n// Re-export generic utilities from test-utils\nexport {\n collectAsync,\n createDevDatabase,\n type DevDatabase,\n teardownTestDatabase,\n withClient,\n} from '@prisma-next/test-utils';\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils.mjs","names":["collectAsync"],"sources":["../../test/utils.ts"],"sourcesContent":["import type { Contract, ExecutionPlan, ResultType } from '@prisma-next/contract/types';\nimport { coreHash, profileHash } from '@prisma-next/contract/types';\nimport {\n instantiateExecutionStack,\n type RuntimeDriverDescriptor,\n} from '@prisma-next/framework-components/execution';\nimport { builtinGeneratorIds } from '@prisma-next/ids';\nimport { generateId } from '@prisma-next/ids/runtime';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\nimport type { Adapter, LoweredStatement, SelectAst } from '@prisma-next/sql-relational-core/ast';\nimport { codec, createCodecRegistry } from '@prisma-next/sql-relational-core/ast';\nimport type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';\nimport { collectAsync, drainAsyncIterable } from '@prisma-next/test-utils';\nimport type { Client } from 'pg';\nimport type { SqlStatement } from '../src/exports';\nimport {\n createExecutionContext,\n type createRuntime,\n createSqlExecutionStack,\n ensureSchemaStatement,\n ensureTableStatement,\n writeContractMarker,\n} from '../src/exports';\nimport type {\n ExecutionContext,\n SqlRuntimeAdapterDescriptor,\n SqlRuntimeAdapterInstance,\n SqlRuntimeDriverInstance,\n SqlRuntimeExtensionDescriptor,\n SqlRuntimeTargetDescriptor,\n} from '../src/sql-context';\n\nfunction createTestMutationDefaultGenerators() {\n return builtinGeneratorIds.map((id) => ({\n id,\n generate: (params?: Record<string, unknown>) => generateId(params ? { id, params } : { id }),\n }));\n}\n\n/**\n * Executes a plan and collects all results into an array.\n * This helper DRYs up the common pattern of executing plans in tests.\n * The return type is inferred from the plan's type parameter.\n */\nexport async function executePlanAndCollect<\n P extends ExecutionPlan<ResultType<P>> | SqlQueryPlan<ResultType<P>>,\n>(runtime: ReturnType<typeof createRuntime>, plan: P): Promise<ResultType<P>[]> {\n type Row = ResultType<P>;\n return collectAsync<Row>(runtime.execute<Row>(plan));\n}\n\n/**\n * Drains a plan execution, consuming all results without collecting them.\n * Useful for testing side effects without memory overhead.\n */\nexport async function drainPlanExecution(\n runtime: ReturnType<typeof createRuntime>,\n plan: ExecutionPlan | SqlQueryPlan<unknown>,\n): Promise<void> {\n return drainAsyncIterable(runtime.execute(plan));\n}\n\n/**\n * Executes a SQL statement on a database client.\n */\nexport async function executeStatement(client: Client, statement: SqlStatement): Promise<void> {\n if (statement.params.length > 0) {\n await client.query(statement.sql, [...statement.params]);\n return;\n }\n\n await client.query(statement.sql);\n}\n\n/**\n * Sets up database schema and data, then writes the contract marker.\n * This helper DRYs up the common pattern of database setup in tests.\n */\nexport async function setupTestDatabase(\n client: Client,\n contract: Contract<SqlStorage>,\n setupFn: (client: Client) => Promise<void>,\n): Promise<void> {\n await client.query('drop schema if exists prisma_contract cascade');\n await client.query('create schema if not exists public');\n\n await setupFn(client);\n\n await executeStatement(client, ensureSchemaStatement);\n await executeStatement(client, ensureTableStatement);\n const write = writeContractMarker({\n storageHash: contract.storage.storageHash,\n profileHash: contract.profileHash,\n contractJson: contract,\n canonicalVersion: 1,\n });\n await executeStatement(client, write.insert);\n}\n\n/**\n * Writes a contract marker to the database.\n * This helper DRYs up the common pattern of writing contract markers in tests.\n */\nexport async function writeTestContractMarker(\n client: Client,\n contract: Contract<SqlStorage>,\n): Promise<void> {\n const write = writeContractMarker({\n storageHash: contract.storage.storageHash,\n profileHash: contract.profileHash,\n contractJson: contract,\n canonicalVersion: 1,\n });\n await executeStatement(client, write.insert);\n}\n\n/**\n * Creates a test adapter descriptor from a raw adapter.\n * Wraps the adapter in an SqlRuntimeAdapterDescriptor with static contributions\n * derived from the adapter's codec registry.\n */\nexport function createTestAdapterDescriptor(\n adapter: Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement>,\n): SqlRuntimeAdapterDescriptor<'postgres'> {\n const codecRegistry = adapter.profile.codecs();\n return {\n kind: 'adapter' as const,\n id: 'test-adapter',\n version: '0.0.1',\n familyId: 'sql' as const,\n targetId: 'postgres' as const,\n codecs: () => codecRegistry,\n parameterizedCodecs: () => [],\n mutationDefaultGenerators: createTestMutationDefaultGenerators,\n create(_stack): SqlRuntimeAdapterInstance<'postgres'> {\n return Object.assign({ familyId: 'sql' as const, targetId: 'postgres' as const }, adapter);\n },\n };\n}\n\n/**\n * Creates a test target descriptor with empty static contributions.\n */\nexport function createTestTargetDescriptor(): SqlRuntimeTargetDescriptor<'postgres'> {\n return {\n kind: 'target' as const,\n id: 'postgres',\n version: '0.0.1',\n familyId: 'sql' as const,\n targetId: 'postgres' as const,\n codecs: () => createCodecRegistry(),\n parameterizedCodecs: () => [],\n create() {\n return { familyId: 'sql' as const, targetId: 'postgres' as const };\n },\n };\n}\n\n/**\n * Creates an ExecutionContext for testing.\n * This helper DRYs up the common pattern of context creation in tests.\n *\n * Accepts a raw adapter and optional extension descriptors, wrapping the\n * adapter in a descriptor internally for descriptor-first context creation.\n */\nexport function createTestContext<TContract extends Contract<SqlStorage>>(\n contract: TContract,\n adapter: Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement>,\n options?: {\n extensionPacks?: ReadonlyArray<SqlRuntimeExtensionDescriptor<'postgres'>>;\n },\n): ExecutionContext<TContract> {\n return createExecutionContext({\n contract,\n stack: {\n target: createTestTargetDescriptor(),\n adapter: createTestAdapterDescriptor(adapter),\n extensionPacks: options?.extensionPacks ?? [],\n },\n });\n}\n\nexport function createTestStackInstance(options?: {\n extensionPacks?: ReadonlyArray<SqlRuntimeExtensionDescriptor<'postgres'>>;\n driver?: RuntimeDriverDescriptor<\n 'sql',\n 'postgres',\n unknown,\n SqlRuntimeDriverInstance<'postgres'>\n >;\n}) {\n const stack = createSqlExecutionStack({\n target: createTestTargetDescriptor(),\n adapter: createTestAdapterDescriptor(createStubAdapter()),\n driver: options?.driver,\n extensionPacks: options?.extensionPacks ?? [],\n });\n\n return instantiateExecutionStack(stack);\n}\n\n/**\n * Creates a stub adapter for testing.\n * This helper DRYs up the common pattern of adapter creation in tests.\n *\n * The stub adapter includes simple codecs for common test types (pg/int4@1, pg/text@1, pg/timestamptz@1)\n * to enable type inference in tests without requiring the postgres adapter package.\n */\nexport function createStubAdapter(): Adapter<SelectAst, Contract<SqlStorage>, LoweredStatement> {\n const codecRegistry = createCodecRegistry();\n\n // Register stub codecs for common test types\n // These match the codec IDs used in test contracts (pg/int4@1, pg/text@1, pg/timestamptz@1)\n // but don't require importing from the postgres adapter package\n codecRegistry.register(\n codec({\n typeId: 'pg/int4@1',\n targetTypes: ['int4'],\n encode: (value: number) => value,\n decode: (wire: number) => wire,\n }),\n );\n\n codecRegistry.register(\n codec({\n typeId: 'pg/text@1',\n targetTypes: ['text'],\n encode: (value: string) => value,\n decode: (wire: string) => wire,\n }),\n );\n\n codecRegistry.register(\n codec({\n typeId: 'pg/timestamptz@1',\n targetTypes: ['timestamptz'],\n encode: (value: string | Date) => (value instanceof Date ? value.toISOString() : value),\n decode: (wire: string | Date) => (wire instanceof Date ? wire : new Date(wire)),\n }),\n );\n\n return {\n profile: {\n id: 'stub-profile',\n target: 'postgres',\n capabilities: {},\n codecs() {\n return codecRegistry;\n },\n readMarkerStatement() {\n return {\n sql: 'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta from prisma_contract.marker where id = $1',\n params: [1],\n };\n },\n },\n lower(ast: SelectAst, ctx: { contract: Contract<SqlStorage>; params?: readonly unknown[] }) {\n const sqlText = JSON.stringify(ast);\n return Object.freeze({ sql: sqlText, params: ctx.params ? [...ctx.params] : [] });\n },\n };\n}\n\nexport function createTestContract(\n contract: Partial<Omit<Contract<SqlStorage>, 'profileHash' | 'storage'>> & {\n storageHash?: string;\n profileHash?: string;\n storage?: Omit<SqlStorage, 'storageHash'>;\n },\n): Contract<SqlStorage> {\n const { execution, ...rest } = contract;\n const storageHashValue = coreHash(rest['storageHash'] ?? 'sha256:testcore');\n\n return {\n target: rest['target'] ?? 'postgres',\n targetFamily: rest['targetFamily'] ?? 'sql',\n storage: rest['storage']\n ? { ...rest['storage'], storageHash: storageHashValue }\n : { storageHash: storageHashValue, tables: {} },\n models: rest['models'] ?? {},\n roots: rest['roots'] ?? {},\n capabilities: rest['capabilities'] ?? {},\n extensionPacks: rest['extensionPacks'] ?? {},\n meta: rest['meta'] ?? {},\n ...(execution ? { execution } : {}),\n profileHash: profileHash(rest['profileHash'] ?? 'sha256:testprofile'),\n };\n}\n\n// Re-export generic utilities from test-utils\nexport {\n collectAsync,\n createDevDatabase,\n type DevDatabase,\n teardownTestDatabase,\n withClient,\n} from '@prisma-next/test-utils';\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@prisma-next/sql-runtime",
3
- "version": "0.4.0-dev.9",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "SQL runtime implementation for Prisma Next",
7
7
  "dependencies": {
8
8
  "arktype": "^2.1.26",
9
- "@prisma-next/contract": "0.4.0-dev.9",
10
- "@prisma-next/utils": "0.4.0-dev.9",
11
- "@prisma-next/framework-components": "0.4.0-dev.9",
12
- "@prisma-next/ids": "0.4.0-dev.9",
13
- "@prisma-next/operations": "0.4.0-dev.9",
14
- "@prisma-next/sql-operations": "0.4.0-dev.9",
15
- "@prisma-next/runtime-executor": "0.4.0-dev.9",
16
- "@prisma-next/sql-relational-core": "0.4.0-dev.9",
17
- "@prisma-next/sql-contract": "0.4.0-dev.9"
9
+ "@prisma-next/contract": "0.4.2",
10
+ "@prisma-next/utils": "0.4.2",
11
+ "@prisma-next/ids": "0.4.2",
12
+ "@prisma-next/runtime-executor": "0.4.2",
13
+ "@prisma-next/operations": "0.4.2",
14
+ "@prisma-next/framework-components": "0.4.2",
15
+ "@prisma-next/sql-operations": "0.4.2",
16
+ "@prisma-next/sql-relational-core": "0.4.2",
17
+ "@prisma-next/sql-contract": "0.4.2"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/pg": "8.16.0",
@@ -22,8 +22,8 @@
22
22
  "tsdown": "0.18.4",
23
23
  "typescript": "5.9.3",
24
24
  "vitest": "4.0.17",
25
- "@prisma-next/tsconfig": "0.0.0",
26
25
  "@prisma-next/test-utils": "0.0.1",
26
+ "@prisma-next/tsconfig": "0.0.0",
27
27
  "@prisma-next/tsdown": "0.0.0"
28
28
  },
29
29
  "files": [
@@ -44,8 +44,12 @@ export {
44
44
  export type {
45
45
  CreateRuntimeOptions,
46
46
  Runtime,
47
+ RuntimeConnection,
48
+ RuntimeQueryable,
47
49
  RuntimeTelemetryEvent,
50
+ RuntimeTransaction,
48
51
  RuntimeVerifyOptions,
49
52
  TelemetryOutcome,
53
+ TransactionContext,
50
54
  } from '../sql-runtime';
51
- export { createRuntime } from '../sql-runtime';
55
+ export { createRuntime, withTransaction } from '../sql-runtime';
@@ -21,11 +21,9 @@ export function lowerSqlPlan<Row>(
21
21
  params: queryPlan.params,
22
22
  });
23
23
 
24
- const body = lowered.body;
25
-
26
24
  return Object.freeze({
27
- sql: body.sql,
28
- params: body.params ?? queryPlan.params,
25
+ sql: lowered.sql,
26
+ params: lowered.params ?? queryPlan.params,
29
27
  ast: queryPlan.ast,
30
28
  meta: queryPlan.meta,
31
29
  });
@@ -0,0 +1,28 @@
1
+ import type { DraftPlan, SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
2
+
3
+ export async function runBeforeCompileChain(
4
+ middleware: readonly SqlMiddleware[],
5
+ initial: DraftPlan,
6
+ ctx: SqlMiddlewareContext,
7
+ ): Promise<DraftPlan> {
8
+ let current = initial;
9
+ for (const mw of middleware) {
10
+ if (!mw.beforeCompile) {
11
+ continue;
12
+ }
13
+ const result = await mw.beforeCompile(current, ctx);
14
+ if (result === undefined) {
15
+ continue;
16
+ }
17
+ if (result.ast === current.ast) {
18
+ continue;
19
+ }
20
+ ctx.log.debug?.({
21
+ event: 'middleware.rewrite',
22
+ middleware: mw.name,
23
+ lane: current.meta.lane,
24
+ });
25
+ current = result;
26
+ }
27
+ return current;
28
+ }
@@ -1,11 +1,8 @@
1
1
  import type { ExecutionPlan } from '@prisma-next/contract/types';
2
2
  import { type RuntimeErrorEnvelope, runtimeError } from '@prisma-next/framework-components/runtime';
3
- import type {
4
- AfterExecuteResult,
5
- Middleware,
6
- MiddlewareContext,
7
- } from '@prisma-next/runtime-executor';
3
+ import type { AfterExecuteResult } from '@prisma-next/runtime-executor';
8
4
  import { isQueryAst, type SelectAst } from '@prisma-next/sql-relational-core/ast';
5
+ import type { SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
9
6
 
10
7
  export interface BudgetsOptions {
11
8
  readonly maxRows?: number;
@@ -77,7 +74,7 @@ function hasDetectableLimitFromHeuristics(plan: ExecutionPlan): boolean {
77
74
  function emitBudgetViolation(
78
75
  error: RuntimeErrorEnvelope,
79
76
  shouldBlock: boolean,
80
- ctx: MiddlewareContext<unknown>,
77
+ ctx: SqlMiddlewareContext,
81
78
  ): void {
82
79
  if (shouldBlock) {
83
80
  throw error;
@@ -89,7 +86,7 @@ function emitBudgetViolation(
89
86
  });
90
87
  }
91
88
 
92
- export function budgets<TContract = unknown>(options?: BudgetsOptions): Middleware<TContract> {
89
+ export function budgets(options?: BudgetsOptions): SqlMiddleware {
93
90
  const maxRows = options?.maxRows ?? 10_000;
94
91
  const defaultTableRows = options?.defaultTableRows ?? 10_000;
95
92
  const tableRows = options?.tableRows ?? {};
@@ -102,7 +99,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
102
99
  name: 'budgets',
103
100
  familyId: 'sql' as const,
104
101
 
105
- async beforeExecute(plan: ExecutionPlan, ctx: MiddlewareContext<TContract>) {
102
+ async beforeExecute(plan: ExecutionPlan, ctx: SqlMiddlewareContext) {
106
103
  observedRowsByPlan.set(plan, { count: 0 });
107
104
 
108
105
  if (isQueryAst(plan.ast)) {
@@ -115,11 +112,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
115
112
  return evaluateWithHeuristics(plan, ctx);
116
113
  },
117
114
 
118
- async onRow(
119
- _row: Record<string, unknown>,
120
- plan: ExecutionPlan,
121
- _ctx: MiddlewareContext<TContract>,
122
- ) {
115
+ async onRow(_row: Record<string, unknown>, plan: ExecutionPlan, _ctx: SqlMiddlewareContext) {
123
116
  const state = observedRowsByPlan.get(plan);
124
117
  if (!state) return;
125
118
  state.count += 1;
@@ -135,7 +128,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
135
128
  async afterExecute(
136
129
  _plan: ExecutionPlan,
137
130
  result: AfterExecuteResult,
138
- ctx: MiddlewareContext<TContract>,
131
+ ctx: SqlMiddlewareContext,
139
132
  ) {
140
133
  const latencyMs = result.latencyMs;
141
134
  if (latencyMs > maxLatencyMs) {
@@ -146,17 +139,13 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
146
139
  maxLatencyMs,
147
140
  }),
148
141
  shouldBlock,
149
- ctx as MiddlewareContext<unknown>,
142
+ ctx,
150
143
  );
151
144
  }
152
145
  },
153
146
  });
154
147
 
155
- function evaluateSelectAst(
156
- plan: ExecutionPlan,
157
- ast: SelectAst,
158
- ctx: MiddlewareContext<TContract>,
159
- ) {
148
+ function evaluateSelectAst(plan: ExecutionPlan, ast: SelectAst, ctx: SqlMiddlewareContext) {
160
149
  const hasAggNoGroup = hasAggregateWithoutGroupBy(ast);
161
150
  const estimated = estimateRowsFromAst(
162
151
  ast,
@@ -177,7 +166,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
177
166
  maxRows,
178
167
  }),
179
168
  shouldBlock,
180
- ctx as MiddlewareContext<unknown>,
169
+ ctx,
181
170
  );
182
171
  return;
183
172
  }
@@ -188,7 +177,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
188
177
  maxRows,
189
178
  }),
190
179
  shouldBlock,
191
- ctx as MiddlewareContext<unknown>,
180
+ ctx,
192
181
  );
193
182
  return;
194
183
  }
@@ -201,12 +190,12 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
201
190
  maxRows,
202
191
  }),
203
192
  shouldBlock,
204
- ctx as MiddlewareContext<unknown>,
193
+ ctx,
205
194
  );
206
195
  }
207
196
  }
208
197
 
209
- async function evaluateWithHeuristics(plan: ExecutionPlan, ctx: MiddlewareContext<TContract>) {
198
+ async function evaluateWithHeuristics(plan: ExecutionPlan, ctx: SqlMiddlewareContext) {
210
199
  const estimated = estimateRowsFromHeuristics(plan, tableRows, defaultTableRows);
211
200
  const isUnbounded = !hasDetectableLimitFromHeuristics(plan);
212
201
  const sqlUpper = plan.sql.trimStart().toUpperCase();
@@ -222,7 +211,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
222
211
  maxRows,
223
212
  }),
224
213
  shouldBlock,
225
- ctx as MiddlewareContext<unknown>,
214
+ ctx,
226
215
  );
227
216
  return;
228
217
  }
@@ -233,7 +222,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
233
222
  maxRows,
234
223
  }),
235
224
  shouldBlock,
236
- ctx as MiddlewareContext<unknown>,
225
+ ctx,
237
226
  );
238
227
  return;
239
228
  }
@@ -247,7 +236,7 @@ export function budgets<TContract = unknown>(options?: BudgetsOptions): Middlewa
247
236
  maxRows,
248
237
  }),
249
238
  shouldBlock,
250
- ctx as MiddlewareContext<unknown>,
239
+ ctx,
251
240
  );
252
241
  }
253
242
  return;
@@ -1,6 +1,5 @@
1
1
  import type { ExecutionPlan } from '@prisma-next/contract/types';
2
2
  import { runtimeError } from '@prisma-next/framework-components/runtime';
3
- import type { Middleware, MiddlewareContext } from '@prisma-next/runtime-executor';
4
3
  import { evaluateRawGuardrails } from '@prisma-next/runtime-executor';
5
4
  import {
6
5
  type AnyFromSource,
@@ -8,6 +7,7 @@ import {
8
7
  isQueryAst,
9
8
  } from '@prisma-next/sql-relational-core/ast';
10
9
  import { ifDefined } from '@prisma-next/utils/defined';
10
+ import type { SqlMiddleware, SqlMiddlewareContext } from './sql-middleware';
11
11
 
12
12
  export interface LintsOptions {
13
13
  readonly severities?: {
@@ -138,14 +138,14 @@ function getConfiguredSeverity(code: string, options?: LintsOptions): 'warn' | '
138
138
  * Fallback: When ast is missing, `fallbackWhenAstMissing: 'raw'` uses heuristic
139
139
  * SQL parsing; `'skip'` skips all lints. Default is `'raw'`.
140
140
  */
141
- export function lints<TContract = unknown>(options?: LintsOptions): Middleware<TContract> {
141
+ export function lints(options?: LintsOptions): SqlMiddleware {
142
142
  const fallback = options?.fallbackWhenAstMissing ?? 'raw';
143
143
 
144
144
  return Object.freeze({
145
145
  name: 'lints',
146
146
  familyId: 'sql' as const,
147
147
 
148
- async beforeExecute(plan: ExecutionPlan, ctx: MiddlewareContext<TContract>) {
148
+ async beforeExecute(plan: ExecutionPlan, ctx: SqlMiddlewareContext) {
149
149
  if (isQueryAst(plan.ast)) {
150
150
  const findings = evaluateAstLints(plan.ast);
151
151
 
@@ -1,17 +1,46 @@
1
- import type { Contract, ExecutionPlan } from '@prisma-next/contract/types';
1
+ import type { Contract, ExecutionPlan, PlanMeta } from '@prisma-next/contract/types';
2
2
  import type {
3
3
  AfterExecuteResult,
4
4
  RuntimeMiddleware,
5
5
  RuntimeMiddlewareContext,
6
6
  } from '@prisma-next/framework-components/runtime';
7
7
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
8
+ import type { AnyQueryAst } from '@prisma-next/sql-relational-core/ast';
8
9
 
9
10
  export interface SqlMiddlewareContext extends RuntimeMiddlewareContext {
10
11
  readonly contract: Contract<SqlStorage>;
11
12
  }
12
13
 
14
+ /**
15
+ * Pre-lowering query view passed to `beforeCompile`. Carries the typed SQL
16
+ * AST and plan metadata; `sql`/`params` are produced later by the adapter.
17
+ */
18
+ export interface DraftPlan {
19
+ readonly ast: AnyQueryAst;
20
+ readonly meta: PlanMeta;
21
+ }
22
+
13
23
  export interface SqlMiddleware extends RuntimeMiddleware {
14
- readonly familyId: 'sql';
24
+ readonly familyId?: 'sql';
25
+ /**
26
+ * Rewrite the query AST before it is lowered to SQL. Middlewares run in
27
+ * registration order; each sees the predecessor's output, so rewrites
28
+ * compose (e.g. soft-delete + tenant isolation).
29
+ *
30
+ * Return `undefined` (or a draft whose `ast` reference equals the input's)
31
+ * to pass through. Return a draft with a new `ast` reference to replace it;
32
+ * the runtime emits a `middleware.rewrite` debug log event and continues
33
+ * with the new draft. `adapter.lower()` runs once after the chain.
34
+ *
35
+ * Use `AstRewriter` / `SelectAst.withWhere` / `AndExpr.of` etc. to build
36
+ * the rewritten AST. Predicates and literals go through parameterized
37
+ * constructors by default — no SQL-injection surface is added. **Warning:**
38
+ * constructing `LiteralExpr.of(userInput)` from untrusted input bypasses
39
+ * that guarantee; use `ParamRef.of(userInput, ...)` instead.
40
+ *
41
+ * See `docs/architecture docs/subsystems/4. Runtime & Middleware Framework.md`.
42
+ */
43
+ beforeCompile?(draft: DraftPlan, ctx: SqlMiddlewareContext): Promise<DraftPlan | undefined>;
15
44
  beforeExecute?(plan: ExecutionPlan, ctx: SqlMiddlewareContext): Promise<void>;
16
45
  onRow?(
17
46
  row: Record<string, unknown>,
@@ -6,14 +6,17 @@ import type {
6
6
  import { checkMiddlewareCompatibility } from '@prisma-next/framework-components/runtime';
7
7
  import type {
8
8
  Log,
9
- Middleware,
10
9
  RuntimeCore,
11
10
  RuntimeCoreOptions,
12
11
  RuntimeTelemetryEvent,
13
12
  RuntimeVerifyOptions,
14
13
  TelemetryOutcome,
15
14
  } from '@prisma-next/runtime-executor';
16
- import { AsyncIterableResult, createRuntimeCore } from '@prisma-next/runtime-executor';
15
+ import {
16
+ AsyncIterableResult,
17
+ createRuntimeCore,
18
+ runtimeError,
19
+ } from '@prisma-next/runtime-executor';
17
20
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
18
21
  import type {
19
22
  Adapter,
@@ -29,6 +32,8 @@ import { decodeRow } from './codecs/decoding';
29
32
  import { encodeParams } from './codecs/encoding';
30
33
  import { validateCodecRegistryCompleteness } from './codecs/validation';
31
34
  import { lowerSqlPlan } from './lower-sql-plan';
35
+ import { runBeforeCompileChain } from './middleware/before-compile-chain';
36
+ import type { SqlMiddleware } from './middleware/sql-middleware';
32
37
  import type {
33
38
  ExecutionContext,
34
39
  SqlRuntimeAdapterInstance,
@@ -41,7 +46,7 @@ export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contrac
41
46
  readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
42
47
  readonly driver: SqlDriver<unknown>;
43
48
  readonly verify: RuntimeVerifyOptions;
44
- readonly middleware?: readonly Middleware<TContract>[];
49
+ readonly middleware?: readonly SqlMiddleware[];
45
50
  readonly mode?: 'strict' | 'permissive';
46
51
  readonly log?: Log;
47
52
  }
@@ -60,7 +65,7 @@ export interface CreateRuntimeOptions<
60
65
  readonly context: ExecutionContext<TContract>;
61
66
  readonly driver: SqlDriver<unknown>;
62
67
  readonly verify: RuntimeVerifyOptions;
63
- readonly middleware?: readonly Middleware<TContract>[];
68
+ readonly middleware?: readonly SqlMiddleware[];
64
69
  readonly mode?: 'strict' | 'permissive';
65
70
  readonly log?: Log;
66
71
  }
@@ -73,7 +78,28 @@ export interface Runtime extends RuntimeQueryable {
73
78
 
74
79
  export interface RuntimeConnection extends RuntimeQueryable {
75
80
  transaction(): Promise<RuntimeTransaction>;
81
+ /**
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.
86
+ */
76
87
  release(): Promise<void>;
88
+ /**
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).
92
+ *
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.
97
+ *
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
+ */
102
+ destroy(reason?: unknown): Promise<void>;
77
103
  }
78
104
 
79
105
  export interface RuntimeTransaction extends RuntimeQueryable {
@@ -87,6 +113,10 @@ export interface RuntimeQueryable {
87
113
  ): AsyncIterableResult<Row>;
88
114
  }
89
115
 
116
+ export interface TransactionContext extends RuntimeQueryable {
117
+ readonly invalidated: boolean;
118
+ }
119
+
90
120
  interface CoreQueryable {
91
121
  execute<Row = Record<string, unknown>>(plan: ExecutionPlan<Row>): AsyncIterableResult<Row>;
92
122
  }
@@ -96,7 +126,7 @@ export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
96
126
  class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
97
127
  implements Runtime
98
128
  {
99
- private readonly core: RuntimeCore<TContract, SqlDriver<unknown>>;
129
+ private readonly core: RuntimeCore<TContract, SqlDriver<unknown>, SqlMiddleware>;
100
130
  private readonly contract: TContract;
101
131
  private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
102
132
  private readonly codecRegistry: CodecRegistry;
@@ -119,7 +149,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
119
149
 
120
150
  const familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
121
151
 
122
- const coreOptions: RuntimeCoreOptions<TContract, SqlDriver<unknown>> = {
152
+ const coreOptions: RuntimeCoreOptions<TContract, SqlDriver<unknown>, SqlMiddleware> = {
123
153
  familyAdapter,
124
154
  driver,
125
155
  verify,
@@ -143,12 +173,27 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
143
173
  }
144
174
  }
145
175
 
146
- private toExecutionPlan<Row>(plan: ExecutionPlan<Row> | SqlQueryPlan<Row>): ExecutionPlan<Row> {
176
+ private async toExecutionPlan<Row>(
177
+ plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
178
+ ): Promise<ExecutionPlan<Row>> {
147
179
  const isSqlQueryPlan = (p: ExecutionPlan<Row> | SqlQueryPlan<Row>): p is SqlQueryPlan<Row> => {
148
180
  return 'ast' in p && !('sql' in p);
149
181
  };
150
182
 
151
- return isSqlQueryPlan(plan) ? lowerSqlPlan(this.adapter, this.contract, plan) : plan;
183
+ if (!isSqlQueryPlan(plan)) {
184
+ return plan;
185
+ }
186
+
187
+ const rewrittenDraft = await runBeforeCompileChain(
188
+ this.core.middleware,
189
+ { ast: plan.ast, meta: plan.meta },
190
+ this.core.middlewareContext,
191
+ );
192
+
193
+ const planToLower: SqlQueryPlan<Row> =
194
+ rewrittenDraft.ast === plan.ast ? plan : { ...plan, ast: rewrittenDraft.ast };
195
+
196
+ return lowerSqlPlan(this.adapter, this.contract, planToLower);
152
197
  }
153
198
 
154
199
  private executeAgainstQueryable<Row = Record<string, unknown>>(
@@ -156,11 +201,11 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
156
201
  queryable: CoreQueryable,
157
202
  ): AsyncIterableResult<Row> {
158
203
  this.ensureCodecRegistryValidated(this.contract);
159
- const executablePlan = this.toExecutionPlan(plan);
160
204
 
161
205
  const iterator = async function* (
162
206
  self: SqlRuntimeImpl<TContract>,
163
207
  ): AsyncGenerator<Row, void, unknown> {
208
+ const executablePlan = await self.toExecutionPlan(plan);
164
209
  const encodedParams = encodeParams(executablePlan, self.codecRegistry);
165
210
  const planWithEncodedParams: ExecutionPlan<Row> = {
166
211
  ...executablePlan,
@@ -206,6 +251,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
206
251
  };
207
252
  },
208
253
  release: coreConn.release.bind(coreConn),
254
+ destroy: coreConn.destroy.bind(coreConn),
209
255
  execute<Row = Record<string, unknown>>(
210
256
  plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
211
257
  ): AsyncIterableResult<Row> {
@@ -224,6 +270,114 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
224
270
  }
225
271
  }
226
272
 
273
+ function transactionClosedError(): Error {
274
+ return runtimeError(
275
+ 'RUNTIME.TRANSACTION_CLOSED',
276
+ 'Cannot read from a query result after the transaction has ended. Await the result or call .toArray() inside the transaction callback.',
277
+ {},
278
+ );
279
+ }
280
+
281
+ export async function withTransaction<R>(
282
+ runtime: Runtime,
283
+ fn: (tx: TransactionContext) => PromiseLike<R>,
284
+ ): Promise<R> {
285
+ const connection = await runtime.connection();
286
+ const transaction = await connection.transaction();
287
+
288
+ let invalidated = false;
289
+ const txContext: TransactionContext = {
290
+ get invalidated() {
291
+ return invalidated;
292
+ },
293
+ execute<Row = Record<string, unknown>>(
294
+ plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
295
+ ): AsyncIterableResult<Row> {
296
+ if (invalidated) {
297
+ throw transactionClosedError();
298
+ }
299
+ const inner = transaction.execute(plan);
300
+ const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
301
+ for await (const row of inner) {
302
+ if (invalidated) {
303
+ throw transactionClosedError();
304
+ }
305
+ yield row;
306
+ }
307
+ };
308
+ return new AsyncIterableResult(guarded());
309
+ },
310
+ };
311
+
312
+ let connectionDisposed = false;
313
+ const destroyConnection = async (reason: unknown): Promise<void> => {
314
+ if (connectionDisposed) return;
315
+ 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.
321
+ await connection.destroy(reason).catch(() => undefined);
322
+ };
323
+
324
+ try {
325
+ let result: R;
326
+ try {
327
+ result = await fn(txContext);
328
+ } catch (error) {
329
+ try {
330
+ await transaction.rollback();
331
+ } catch (rollbackError) {
332
+ await destroyConnection(rollbackError);
333
+ const wrapped = runtimeError(
334
+ 'RUNTIME.TRANSACTION_ROLLBACK_FAILED',
335
+ 'Transaction rollback failed after callback error',
336
+ { rollbackError },
337
+ );
338
+ wrapped.cause = error;
339
+ throw wrapped;
340
+ }
341
+ throw error;
342
+ } finally {
343
+ invalidated = true;
344
+ }
345
+
346
+ try {
347
+ await transaction.commit();
348
+ } 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.
354
+ //
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.
360
+ try {
361
+ await transaction.rollback();
362
+ } catch {
363
+ await destroyConnection(commitError);
364
+ }
365
+ const wrapped = runtimeError(
366
+ 'RUNTIME.TRANSACTION_COMMIT_FAILED',
367
+ 'Transaction commit failed',
368
+ { commitError },
369
+ );
370
+ wrapped.cause = commitError;
371
+ throw wrapped;
372
+ }
373
+ return result;
374
+ } finally {
375
+ if (!connectionDisposed) {
376
+ await connection.release();
377
+ }
378
+ }
379
+ }
380
+
227
381
  export function createRuntime<TContract extends Contract<SqlStorage>, TTargetId extends string>(
228
382
  options: CreateRuntimeOptions<TContract, TTargetId>,
229
383
  ): Runtime {