@prisma-next/sql-runtime 0.5.0-dev.9 → 0.6.0-dev.1
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 +2 -1
- package/dist/exports-BSTHn_rH.mjs +1516 -0
- package/dist/exports-BSTHn_rH.mjs.map +1 -0
- package/dist/{index-CZmC2kD3.d.mts → index-CTCvZOWI.d.mts} +87 -44
- package/dist/index-CTCvZOWI.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -3
- package/dist/test/utils.d.mts +33 -29
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +104 -64
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +15 -14
- package/src/codecs/alias-resolver.ts +37 -0
- package/src/codecs/decoding.ts +163 -129
- package/src/codecs/encoding.ts +121 -46
- package/src/codecs/validation.ts +4 -4
- package/src/content-hash.ts +44 -0
- package/src/exports/index.ts +4 -1
- package/src/guardrails/raw.ts +1 -50
- package/src/marker.ts +13 -20
- package/src/middleware/before-compile-chain.ts +1 -31
- package/src/middleware/budgets.ts +26 -113
- package/src/middleware/lints.ts +17 -23
- package/src/middleware/sql-middleware.ts +21 -2
- package/src/runtime-spi.ts +3 -8
- package/src/sql-context.ts +320 -109
- package/src/sql-marker.ts +88 -50
- package/src/sql-runtime.ts +108 -90
- package/dist/exports-BOHa3Emo.mjs +0 -1334
- package/dist/exports-BOHa3Emo.mjs.map +0 -1
- package/dist/index-CZmC2kD3.d.mts.map +0 -1
- package/src/codecs/json-schema-validation.ts +0 -61
package/src/sql-marker.ts
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
|
+
import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
|
|
1
2
|
import type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
|
|
2
3
|
|
|
4
|
+
export { APP_SPACE_ID };
|
|
5
|
+
|
|
3
6
|
export interface SqlStatement {
|
|
4
7
|
readonly sql: string;
|
|
5
8
|
readonly params: readonly unknown[];
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export interface WriteMarkerInput {
|
|
12
|
+
/**
|
|
13
|
+
* Logical space identifier for this marker row. Required at every
|
|
14
|
+
* call site so the type system surfaces every place that needs to
|
|
15
|
+
* thread the value (rather than letting an `?? APP_SPACE_ID`
|
|
16
|
+
* fall-through silently collapse multi-space markers onto the
|
|
17
|
+
* `'app'` row). App-plan callers pass {@link APP_SPACE_ID}
|
|
18
|
+
* (`'app'`); per-extension callers pass the extension's space id.
|
|
19
|
+
*/
|
|
20
|
+
readonly space: string;
|
|
9
21
|
readonly storageHash: string;
|
|
10
22
|
readonly profileHash: string;
|
|
11
23
|
readonly contractJson?: unknown;
|
|
12
24
|
readonly canonicalVersion?: number;
|
|
13
25
|
readonly appTag?: string;
|
|
14
26
|
readonly meta?: Record<string, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* Applied-invariants set on the marker.
|
|
29
|
+
*
|
|
30
|
+
* - `undefined` → existing column left untouched. Sign and
|
|
31
|
+
* verify-database paths use this; they don't accumulate invariants.
|
|
32
|
+
* - explicit value (including `[]`) → column overwritten with
|
|
33
|
+
* exactly that value.
|
|
34
|
+
*/
|
|
35
|
+
readonly invariants?: readonly string[];
|
|
15
36
|
}
|
|
16
37
|
|
|
17
38
|
export const ensureSchemaStatement: SqlStatement = {
|
|
@@ -19,21 +40,33 @@ export const ensureSchemaStatement: SqlStatement = {
|
|
|
19
40
|
params: [],
|
|
20
41
|
};
|
|
21
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Schema for `prisma_contract.marker`. The `space text` primary key
|
|
45
|
+
* supports one row per loaded contract space (`'app'`,
|
|
46
|
+
* `'<extension-id>'`, …); brand-new databases create this shape
|
|
47
|
+
* directly. Pre-1.0 single-row markers (no `space` column) are not
|
|
48
|
+
* auto-migrated — the target-specific migration runner detects the
|
|
49
|
+
* legacy shape at boot and surfaces a structured `LEGACY_MARKER_SHAPE`
|
|
50
|
+
* failure pointing the operator at re-running `dbInit`.
|
|
51
|
+
*
|
|
52
|
+
* @see specs/framework-mechanism.spec.md § 2.
|
|
53
|
+
*/
|
|
22
54
|
export const ensureTableStatement: SqlStatement = {
|
|
23
55
|
sql: `create table if not exists prisma_contract.marker (
|
|
24
|
-
|
|
56
|
+
space text not null primary key default '${APP_SPACE_ID}',
|
|
25
57
|
core_hash text not null,
|
|
26
58
|
profile_hash text not null,
|
|
27
59
|
contract_json jsonb,
|
|
28
60
|
canonical_version int,
|
|
29
61
|
updated_at timestamptz not null default now(),
|
|
30
62
|
app_tag text,
|
|
31
|
-
meta jsonb not null default '{}'
|
|
63
|
+
meta jsonb not null default '{}',
|
|
64
|
+
invariants text[] not null default '{}'
|
|
32
65
|
)`,
|
|
33
66
|
params: [],
|
|
34
67
|
};
|
|
35
68
|
|
|
36
|
-
export function readContractMarker(): MarkerStatement {
|
|
69
|
+
export function readContractMarker(space: string): MarkerStatement {
|
|
37
70
|
return {
|
|
38
71
|
sql: `select
|
|
39
72
|
core_hash,
|
|
@@ -42,10 +75,11 @@ export function readContractMarker(): MarkerStatement {
|
|
|
42
75
|
canonical_version,
|
|
43
76
|
updated_at,
|
|
44
77
|
app_tag,
|
|
45
|
-
meta
|
|
78
|
+
meta,
|
|
79
|
+
invariants
|
|
46
80
|
from prisma_contract.marker
|
|
47
|
-
where
|
|
48
|
-
params: [
|
|
81
|
+
where space = $1`,
|
|
82
|
+
params: [space],
|
|
49
83
|
};
|
|
50
84
|
}
|
|
51
85
|
|
|
@@ -54,52 +88,56 @@ export interface WriteContractMarkerStatements {
|
|
|
54
88
|
readonly update: SqlStatement;
|
|
55
89
|
}
|
|
56
90
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Variable columns that participate in INSERT/UPDATE alongside the
|
|
93
|
+
* always-on `space = $1` and `updated_at = now()`. Each column declares
|
|
94
|
+
* its name, optional cast type, and parameter value; the placeholder
|
|
95
|
+
* (`$N`) is computed positionally below — adding or reordering a
|
|
96
|
+
* column doesn't desync indices. `invariants` only appears when the
|
|
97
|
+
* caller supplies it — see `WriteMarkerInput.invariants`.
|
|
98
|
+
*/
|
|
99
|
+
function markerColumns(
|
|
100
|
+
input: WriteMarkerInput,
|
|
101
|
+
): ReadonlyArray<{ readonly name: string; readonly type?: string; readonly param: unknown }> {
|
|
102
|
+
return [
|
|
103
|
+
{ name: 'core_hash', param: input.storageHash },
|
|
104
|
+
{ name: 'profile_hash', param: input.profileHash },
|
|
105
|
+
{ name: 'contract_json', type: 'jsonb', param: input.contractJson ?? null },
|
|
106
|
+
{ name: 'canonical_version', param: input.canonicalVersion ?? null },
|
|
107
|
+
{ name: 'app_tag', param: input.appTag ?? null },
|
|
108
|
+
{ name: 'meta', type: 'jsonb', param: JSON.stringify(input.meta ?? {}) },
|
|
109
|
+
...(input.invariants !== undefined
|
|
110
|
+
? [{ name: 'invariants' as const, type: 'text[]' as const, param: input.invariants }]
|
|
111
|
+
: []),
|
|
66
112
|
];
|
|
113
|
+
}
|
|
67
114
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
meta
|
|
78
|
-
) values (
|
|
79
|
-
$1,
|
|
80
|
-
$2,
|
|
81
|
-
$3,
|
|
82
|
-
$4::jsonb,
|
|
83
|
-
$5,
|
|
84
|
-
now(),
|
|
85
|
-
$6,
|
|
86
|
-
$7::jsonb
|
|
87
|
-
)`,
|
|
88
|
-
params: baseParams,
|
|
89
|
-
};
|
|
115
|
+
export function writeContractMarker(input: WriteMarkerInput): WriteContractMarkerStatements {
|
|
116
|
+
const cols = markerColumns(input);
|
|
117
|
+
// $1 is reserved for `space`; subsequent positions follow the order of cols.
|
|
118
|
+
const placed = cols.map((c, i) => ({
|
|
119
|
+
name: c.name,
|
|
120
|
+
expr: c.type ? `$${i + 2}::${c.type}` : `$${i + 2}`,
|
|
121
|
+
param: c.param,
|
|
122
|
+
}));
|
|
123
|
+
const params: readonly unknown[] = [input.space, ...placed.map((c) => c.param)];
|
|
90
124
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
app_tag = $6,
|
|
99
|
-
meta = $7::jsonb
|
|
100
|
-
where id = $1`,
|
|
101
|
-
params: baseParams,
|
|
102
|
-
};
|
|
125
|
+
// `updated_at = now()` is a SQL literal with no parameter slot, so it
|
|
126
|
+
// sits outside `placed` and is appended directly to each statement.
|
|
127
|
+
const insertColumns = ['space', ...placed.map((c) => c.name), 'updated_at'].join(', ');
|
|
128
|
+
const insertValues = ['$1', ...placed.map((c) => c.expr), 'now()'].join(', ');
|
|
129
|
+
const setClauses = [...placed.map((c) => `${c.name} = ${c.expr}`), 'updated_at = now()'].join(
|
|
130
|
+
', ',
|
|
131
|
+
);
|
|
103
132
|
|
|
104
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
insert: {
|
|
135
|
+
sql: `insert into prisma_contract.marker (${insertColumns}) values (${insertValues})`,
|
|
136
|
+
params,
|
|
137
|
+
},
|
|
138
|
+
update: {
|
|
139
|
+
sql: `update prisma_contract.marker set ${setClauses} where space = $1`,
|
|
140
|
+
params,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
105
143
|
}
|
package/src/sql-runtime.ts
CHANGED
|
@@ -5,8 +5,10 @@ import type {
|
|
|
5
5
|
} from '@prisma-next/framework-components/execution';
|
|
6
6
|
import {
|
|
7
7
|
AsyncIterableResult,
|
|
8
|
+
checkAborted,
|
|
8
9
|
checkMiddlewareCompatibility,
|
|
9
10
|
RuntimeCore,
|
|
11
|
+
type RuntimeExecuteOptions,
|
|
10
12
|
type RuntimeLog,
|
|
11
13
|
runtimeError,
|
|
12
14
|
runWithMiddleware,
|
|
@@ -15,22 +17,29 @@ import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
|
15
17
|
import type {
|
|
16
18
|
Adapter,
|
|
17
19
|
AnyQueryAst,
|
|
18
|
-
|
|
20
|
+
ContractCodecRegistry,
|
|
19
21
|
LoweredStatement,
|
|
22
|
+
SqlCodecCallContext,
|
|
20
23
|
SqlDriver,
|
|
21
24
|
SqlQueryable,
|
|
22
25
|
SqlTransaction,
|
|
23
26
|
} from '@prisma-next/sql-relational-core/ast';
|
|
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';
|
|
24
33
|
import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
25
|
-
import type {
|
|
34
|
+
import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
|
|
26
35
|
import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
|
|
27
36
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
28
37
|
import { decodeRow } from './codecs/decoding';
|
|
29
38
|
import { encodeParams } from './codecs/encoding';
|
|
30
39
|
import { validateCodecRegistryCompleteness } from './codecs/validation';
|
|
40
|
+
import { computeSqlContentHash } from './content-hash';
|
|
31
41
|
import { computeSqlFingerprint } from './fingerprint';
|
|
32
42
|
import { lowerSqlPlan } from './lower-sql-plan';
|
|
33
|
-
import { parseContractMarkerRow } from './marker';
|
|
34
43
|
import { runBeforeCompileChain } from './middleware/before-compile-chain';
|
|
35
44
|
import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
|
|
36
45
|
import type {
|
|
@@ -86,25 +95,15 @@ export interface Runtime extends RuntimeQueryable {
|
|
|
86
95
|
export interface RuntimeConnection extends RuntimeQueryable {
|
|
87
96
|
transaction(): Promise<RuntimeTransaction>;
|
|
88
97
|
/**
|
|
89
|
-
* Returns the connection to the pool for reuse. Only call this when the
|
|
90
|
-
* connection is known to be in a clean state. If a transaction
|
|
91
|
-
* commit/rollback failed or the connection is otherwise suspect, call
|
|
92
|
-
* `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.
|
|
93
99
|
*/
|
|
94
100
|
release(): Promise<void>;
|
|
95
101
|
/**
|
|
96
|
-
* Evicts the connection so it is never reused. Call this when the
|
|
97
|
-
* connection may be in an indeterminate state (e.g. a failed rollback
|
|
98
|
-
* 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).
|
|
99
103
|
*
|
|
100
|
-
* If teardown fails the error is propagated and the connection remains
|
|
101
|
-
* retryable, so the caller can decide whether to swallow the failure or
|
|
102
|
-
* retry cleanup. Calling destroy() or release() more than once after a
|
|
103
|
-
* 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.
|
|
104
105
|
*
|
|
105
|
-
* `reason` is advisory context only. It may be surfaced to driver-level
|
|
106
|
-
* observability hooks (e.g. pg-pool's `'release'` event) but does not
|
|
107
|
-
* 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.
|
|
108
107
|
*/
|
|
109
108
|
destroy(reason?: unknown): Promise<void>;
|
|
110
109
|
}
|
|
@@ -126,6 +125,10 @@ function isExecutionPlan(plan: SqlExecutionPlan | SqlQueryPlan): plan is SqlExec
|
|
|
126
125
|
return 'sql' in plan;
|
|
127
126
|
}
|
|
128
127
|
|
|
128
|
+
// v8 ignore next 2
|
|
129
|
+
const noopLogSink = (): void => {};
|
|
130
|
+
const noopLog: Log = { info: noopLogSink, warn: noopLogSink, error: noopLogSink };
|
|
131
|
+
|
|
129
132
|
class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
|
|
130
133
|
extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>
|
|
131
134
|
implements Runtime
|
|
@@ -134,8 +137,8 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
134
137
|
private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
|
|
135
138
|
private readonly driver: SqlDriver<unknown>;
|
|
136
139
|
private readonly familyAdapter: RuntimeFamilyAdapter<Contract<SqlStorage>>;
|
|
137
|
-
private readonly
|
|
138
|
-
private readonly
|
|
140
|
+
private readonly contractCodecs: ContractCodecRegistry;
|
|
141
|
+
private readonly codecDescriptors: CodecDescriptorRegistry;
|
|
139
142
|
private readonly sqlCtx: SqlMiddlewareContext;
|
|
140
143
|
private readonly verify: RuntimeVerifyOptions;
|
|
141
144
|
private codecRegistryValidated: boolean;
|
|
@@ -156,11 +159,9 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
156
159
|
contract: context.contract,
|
|
157
160
|
mode: mode ?? 'strict',
|
|
158
161
|
now: () => Date.now(),
|
|
159
|
-
log: log ??
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
error: () => {},
|
|
163
|
-
},
|
|
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),
|
|
164
165
|
};
|
|
165
166
|
|
|
166
167
|
super({ middleware: middleware ?? [], ctx: sqlCtx });
|
|
@@ -169,8 +170,8 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
169
170
|
this.adapter = adapter;
|
|
170
171
|
this.driver = driver;
|
|
171
172
|
this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
173
|
+
this.contractCodecs = context.contractCodecs;
|
|
174
|
+
this.codecDescriptors = context.codecDescriptors;
|
|
174
175
|
this.sqlCtx = sqlCtx;
|
|
175
176
|
this.verify = verify;
|
|
176
177
|
this.codecRegistryValidated = false;
|
|
@@ -179,30 +180,32 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
179
180
|
this._telemetry = null;
|
|
180
181
|
|
|
181
182
|
if (verify.mode === 'startup') {
|
|
182
|
-
validateCodecRegistryCompleteness(this.
|
|
183
|
+
validateCodecRegistryCompleteness(this.codecDescriptors, context.contract);
|
|
183
184
|
this.codecRegistryValidated = true;
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
/**
|
|
188
|
-
* Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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.
|
|
191
192
|
*/
|
|
192
|
-
protected override async lower(
|
|
193
|
+
protected override async lower(
|
|
194
|
+
plan: SqlQueryPlan,
|
|
195
|
+
ctx: SqlCodecCallContext,
|
|
196
|
+
): Promise<SqlExecutionPlan> {
|
|
197
|
+
validateParamRefRefs(plan.ast, this.codecDescriptors);
|
|
193
198
|
const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
|
|
194
199
|
return Object.freeze({
|
|
195
200
|
...lowered,
|
|
196
|
-
params: await encodeParams(lowered, this.
|
|
201
|
+
params: await encodeParams(lowered, ctx, this.contractCodecs),
|
|
197
202
|
});
|
|
198
203
|
}
|
|
199
204
|
|
|
200
205
|
/**
|
|
201
|
-
* Default driver invocation.
|
|
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.
|
|
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.
|
|
205
207
|
*/
|
|
208
|
+
// v8 ignore next 6
|
|
206
209
|
protected override runDriver(exec: SqlExecutionPlan): AsyncIterable<Record<string, unknown>> {
|
|
207
210
|
return this.driver.execute<Record<string, unknown>>({
|
|
208
211
|
sql: exec.sql,
|
|
@@ -211,11 +214,8 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
/**
|
|
214
|
-
* SQL pre-compile hook. Runs the registered middleware `beforeCompile`
|
|
215
|
-
*
|
|
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.
|
|
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
219
|
*/
|
|
220
220
|
protected override async runBeforeCompile(plan: SqlQueryPlan): Promise<SqlQueryPlan> {
|
|
221
221
|
const rewrittenDraft = await runBeforeCompileChain(
|
|
@@ -230,24 +230,43 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
230
230
|
|
|
231
231
|
override execute<Row>(
|
|
232
232
|
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
233
|
+
options?: RuntimeExecuteOptions,
|
|
233
234
|
): AsyncIterableResult<Row> {
|
|
234
|
-
return this.executeAgainstQueryable<Row>(plan, this.driver);
|
|
235
|
+
return this.executeAgainstQueryable<Row>(plan, this.driver, options);
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
private executeAgainstQueryable<Row>(
|
|
238
239
|
plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
|
|
239
240
|
queryable: SqlQueryable,
|
|
241
|
+
options?: RuntimeExecuteOptions,
|
|
240
242
|
): AsyncIterableResult<Row> {
|
|
241
243
|
this.ensureCodecRegistryValidated();
|
|
242
244
|
|
|
243
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
|
+
|
|
244
255
|
const generator = async function* (): AsyncGenerator<Row, void, unknown> {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
}
|
|
251
270
|
|
|
252
271
|
self.familyAdapter.validatePlan(exec, self.contract);
|
|
253
272
|
self._telemetry = null;
|
|
@@ -268,25 +287,42 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
268
287
|
await self.verifyMarker();
|
|
269
288
|
}
|
|
270
289
|
|
|
271
|
-
const
|
|
290
|
+
const paramsMutator: SqlParamRefMutatorInternal = createSqlParamRefMutator(exec);
|
|
291
|
+
const stream = runWithMiddleware<
|
|
292
|
+
SqlExecutionPlan,
|
|
293
|
+
Record<string, unknown>,
|
|
294
|
+
SqlParamRefMutator
|
|
295
|
+
>(
|
|
272
296
|
exec,
|
|
273
297
|
self.middleware,
|
|
274
|
-
|
|
298
|
+
execMiddlewareCtx,
|
|
275
299
|
() =>
|
|
276
300
|
queryable.execute<Record<string, unknown>>({
|
|
277
301
|
sql: exec.sql,
|
|
278
|
-
params
|
|
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(),
|
|
279
307
|
}),
|
|
308
|
+
paramsMutator,
|
|
280
309
|
);
|
|
281
310
|
|
|
282
|
-
for await
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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?.();
|
|
290
326
|
}
|
|
291
327
|
|
|
292
328
|
outcome = 'success';
|
|
@@ -320,8 +356,9 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
320
356
|
},
|
|
321
357
|
execute<Row>(
|
|
322
358
|
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
359
|
+
options?: RuntimeExecuteOptions,
|
|
323
360
|
): AsyncIterableResult<Row> {
|
|
324
|
-
return self.executeAgainstQueryable<Row>(plan, driverConn);
|
|
361
|
+
return self.executeAgainstQueryable<Row>(plan, driverConn, options);
|
|
325
362
|
},
|
|
326
363
|
};
|
|
327
364
|
|
|
@@ -339,8 +376,9 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
339
376
|
},
|
|
340
377
|
execute<Row>(
|
|
341
378
|
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
379
|
+
options?: RuntimeExecuteOptions,
|
|
342
380
|
): AsyncIterableResult<Row> {
|
|
343
|
-
return self.executeAgainstQueryable<Row>(plan, driverTx);
|
|
381
|
+
return self.executeAgainstQueryable<Row>(plan, driverTx, options);
|
|
344
382
|
},
|
|
345
383
|
};
|
|
346
384
|
}
|
|
@@ -355,24 +393,15 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
355
393
|
|
|
356
394
|
private ensureCodecRegistryValidated(): void {
|
|
357
395
|
if (!this.codecRegistryValidated) {
|
|
358
|
-
validateCodecRegistryCompleteness(this.
|
|
396
|
+
validateCodecRegistryCompleteness(this.codecDescriptors, this.contract);
|
|
359
397
|
this.codecRegistryValidated = true;
|
|
360
398
|
}
|
|
361
399
|
}
|
|
362
400
|
|
|
363
401
|
private async verifyMarker(): Promise<void> {
|
|
364
|
-
|
|
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);
|
|
402
|
+
const readResult = await this.familyAdapter.markerReader.readMarker(this.driver);
|
|
374
403
|
|
|
375
|
-
if (
|
|
404
|
+
if (readResult.kind !== 'present') {
|
|
376
405
|
if (this.verify.requireMarker) {
|
|
377
406
|
throw runtimeError('CONTRACT.MARKER_MISSING', 'Contract marker not found in database');
|
|
378
407
|
}
|
|
@@ -381,7 +410,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
381
410
|
return;
|
|
382
411
|
}
|
|
383
412
|
|
|
384
|
-
const marker =
|
|
413
|
+
const marker = readResult.record;
|
|
385
414
|
|
|
386
415
|
const contract = this.contract as {
|
|
387
416
|
storage: { storageHash: string };
|
|
@@ -454,11 +483,12 @@ export async function withTransaction<R>(
|
|
|
454
483
|
},
|
|
455
484
|
execute<Row>(
|
|
456
485
|
plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
|
|
486
|
+
options?: RuntimeExecuteOptions,
|
|
457
487
|
): AsyncIterableResult<Row> {
|
|
458
488
|
if (invalidated) {
|
|
459
489
|
throw transactionClosedError();
|
|
460
490
|
}
|
|
461
|
-
const inner = transaction.execute(plan);
|
|
491
|
+
const inner = transaction.execute(plan, options);
|
|
462
492
|
const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
|
|
463
493
|
for await (const row of inner) {
|
|
464
494
|
if (invalidated) {
|
|
@@ -475,11 +505,7 @@ export async function withTransaction<R>(
|
|
|
475
505
|
const destroyConnection = async (reason: unknown): Promise<void> => {
|
|
476
506
|
if (connectionDisposed) return;
|
|
477
507
|
connectionDisposed = true;
|
|
478
|
-
// SqlConnection.destroy() propagates teardown errors so callers can
|
|
479
|
-
// decide what to do with them. Here, we're already about to throw a
|
|
480
|
-
// more informative error describing why we're evicting the connection
|
|
481
|
-
// (rollback/commit failure), so swallowing the teardown error is the
|
|
482
|
-
// right call — surfacing it would mask the original cause.
|
|
508
|
+
// 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.
|
|
483
509
|
await connection.destroy(reason).catch(() => undefined);
|
|
484
510
|
};
|
|
485
511
|
|
|
@@ -508,17 +534,9 @@ export async function withTransaction<R>(
|
|
|
508
534
|
try {
|
|
509
535
|
await transaction.commit();
|
|
510
536
|
} catch (commitError) {
|
|
511
|
-
// After a failed COMMIT the server-side transaction may be: (a) already
|
|
512
|
-
// committed (error on response path), (b) already rolled back (deferred
|
|
513
|
-
// constraint / serialization failure), or (c) still open (COMMIT never
|
|
514
|
-
// reached the server). Attempt a best-effort rollback to cover (c) and
|
|
515
|
-
// confirm the protocol is healthy.
|
|
537
|
+
// 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.
|
|
516
538
|
//
|
|
517
|
-
// If rollback succeeds, the server is definitely no longer in a
|
|
518
|
-
// transaction (no-op in (a)/(b), real cleanup in (c)) and we've just
|
|
519
|
-
// proved the connection round-trips correctly — it's safe to return
|
|
520
|
-
// to the pool. If rollback fails, the connection state is ambiguous
|
|
521
|
-
// (broken socket, protocol desync, etc.) and we must destroy it.
|
|
539
|
+
// 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.
|
|
522
540
|
try {
|
|
523
541
|
await transaction.rollback();
|
|
524
542
|
} catch {
|