turbine-orm 0.79.1 → 0.80.0
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 +4 -4
- package/dist/checkout.d.ts +53 -0
- package/dist/checkout.js +78 -0
- package/dist/cjs/checkout.d.ts +53 -0
- package/dist/cjs/checkout.js +82 -0
- package/dist/cjs/cli/index.js +4 -0
- package/dist/cjs/cli/mcp.js +4 -0
- package/dist/cjs/cli/migrate.js +6 -0
- package/dist/cjs/cli/observe.js +8 -0
- package/dist/cjs/cli/studio.js +13 -1
- package/dist/cjs/client.d.ts +12 -2
- package/dist/cjs/client.js +61 -75
- package/dist/cjs/connection-guard.d.ts +120 -0
- package/dist/cjs/connection-guard.js +191 -0
- package/dist/cjs/errors.d.ts +26 -0
- package/dist/cjs/errors.js +85 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/nested-write.d.ts +12 -2
- package/dist/cjs/nested-write.js +4 -10
- package/dist/cjs/pipeline.js +12 -7
- package/dist/cjs/plan-flip-probe.js +4 -0
- package/dist/cjs/powdb-shared.d.ts +22 -2
- package/dist/cjs/powdb-shared.js +27 -2
- package/dist/cjs/powdb.js +36 -37
- package/dist/cjs/powql.d.ts +51 -6
- package/dist/cjs/powql.js +199 -45
- package/dist/cjs/prisma-compat.js +28 -4
- package/dist/cjs/query/builder.d.ts +44 -24
- package/dist/cjs/query/builder.js +125 -66
- package/dist/cjs/query/deferred.d.ts +9 -0
- package/dist/cjs/query/option-surface.js +12 -0
- package/dist/cjs/query/types.d.ts +68 -4
- package/dist/cjs/query/writes.d.ts +39 -9
- package/dist/cjs/query/writes.js +72 -34
- package/dist/cjs/realtime.d.ts +46 -2
- package/dist/cjs/realtime.js +125 -20
- package/dist/cjs/schema-sql.js +6 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/mcp.js +4 -0
- package/dist/cli/migrate.js +6 -0
- package/dist/cli/observe.js +8 -0
- package/dist/cli/studio.js +13 -1
- package/dist/client.d.ts +12 -2
- package/dist/client.js +62 -76
- package/dist/connection-guard.d.ts +120 -0
- package/dist/connection-guard.js +183 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +83 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/nested-write.d.ts +12 -2
- package/dist/nested-write.js +4 -10
- package/dist/pipeline.js +13 -8
- package/dist/plan-flip-probe.js +4 -0
- package/dist/powdb-shared.d.ts +22 -2
- package/dist/powdb-shared.js +25 -2
- package/dist/powdb.js +23 -24
- package/dist/powql.d.ts +51 -6
- package/dist/powql.js +200 -46
- package/dist/prisma-compat.js +28 -4
- package/dist/query/builder.d.ts +44 -24
- package/dist/query/builder.js +126 -67
- package/dist/query/deferred.d.ts +9 -0
- package/dist/query/option-surface.js +12 -0
- package/dist/query/types.d.ts +68 -4
- package/dist/query/writes.d.ts +39 -9
- package/dist/query/writes.js +71 -34
- package/dist/realtime.d.ts +46 -2
- package/dist/realtime.js +125 -20
- package/dist/schema-sql.js +6 -0
- package/package.json +5 -3
package/dist/query/builder.js
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* Schema-driven: all column names, types, and relations come from introspected
|
|
11
11
|
* metadata, nothing is hardcoded.
|
|
12
12
|
*/
|
|
13
|
+
import { acquireConnection, openCheckout } from '../checkout.js';
|
|
14
|
+
import { settleEventLoop } from '../connection-guard.js';
|
|
13
15
|
import { postgresDialect } from '../dialect.js';
|
|
14
|
-
import { NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError } from '../errors.js';
|
|
16
|
+
import { explainConnectionLoss, isStaleConnectionError, NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from '../errors.js';
|
|
15
17
|
import { missingIndexForRelation, schemaHasIndexInfo } from '../index-advisor.js';
|
|
16
18
|
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '../nested-write.js';
|
|
17
19
|
import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
@@ -1677,7 +1679,7 @@ export class QueryInterface {
|
|
|
1677
1679
|
const deferred = single
|
|
1678
1680
|
? this.buildFindUnique(baseArgs)
|
|
1679
1681
|
: this.buildFindMany(baseArgs);
|
|
1680
|
-
const result = await this.
|
|
1682
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
1681
1683
|
const rows = deferred.transform(result);
|
|
1682
1684
|
const entities = single ? (rows ? [rows] : []) : rows;
|
|
1683
1685
|
// Unconditionally, even for zero rows: with no parents the loader is a
|
|
@@ -1718,7 +1720,7 @@ export class QueryInterface {
|
|
|
1718
1720
|
// too: a batched load re-issues the SAME tenant-shaped predicate one
|
|
1719
1721
|
// level down, so leaving those named would keep exactly the plan-cache
|
|
1720
1722
|
// exposure the caller asked to be rid of.
|
|
1721
|
-
exec: (sql, params, preparedName) => this.
|
|
1723
|
+
exec: (sql, params, preparedName) => this.readWithTimeout(sql, params, timeout, this.preparedNameFor({ forceCustomPlan }, preparedName)),
|
|
1722
1724
|
quote: (name) => this.q(name),
|
|
1723
1725
|
buildInClause: (expr, paramRef, negated) => this.inClause(expr, paramRef, negated),
|
|
1724
1726
|
inClauseParam: (values) => this.inParam(values),
|
|
@@ -1831,7 +1833,7 @@ export class QueryInterface {
|
|
|
1831
1833
|
const { baseArgs, strip } = this.prepareBatchedBase(args, withClause);
|
|
1832
1834
|
// baseArgs.with is always undefined here; the cast just bridges the R generic.
|
|
1833
1835
|
const deferred = this.buildFindMany(baseArgs);
|
|
1834
|
-
const result = await this.
|
|
1836
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
1835
1837
|
const entities = deferred.transform(result);
|
|
1836
1838
|
// Unconditionally, even for zero rows: see the 'auto' path above, the
|
|
1837
1839
|
// loader doubles as the compile-time validation walk of the `with` tree.
|
|
@@ -1994,7 +1996,7 @@ export class QueryInterface {
|
|
|
1994
1996
|
resetUnlimitedWarnings() {
|
|
1995
1997
|
this.warnedTables.clear();
|
|
1996
1998
|
}
|
|
1997
|
-
emitQueryEvent(sql, params, duration, action, rows, error) {
|
|
1999
|
+
emitQueryEvent(sql, params, duration, action, rows, error, retried) {
|
|
1998
2000
|
const onQuery = this.options?._onQuery;
|
|
1999
2001
|
if (!onQuery)
|
|
2000
2002
|
return;
|
|
@@ -2009,6 +2011,7 @@ export class QueryInterface {
|
|
|
2009
2011
|
timestamp: new Date(),
|
|
2010
2012
|
error,
|
|
2011
2013
|
strategy: this.currentStrategyTag,
|
|
2014
|
+
...(retried ? { retried: true } : {}),
|
|
2012
2015
|
});
|
|
2013
2016
|
}
|
|
2014
2017
|
catch {
|
|
@@ -2078,13 +2081,39 @@ export class QueryInterface {
|
|
|
2078
2081
|
}
|
|
2079
2082
|
return undefined;
|
|
2080
2083
|
}
|
|
2084
|
+
/**
|
|
2085
|
+
* {@link queryWithTimeout} for a statement that only READS, which makes it
|
|
2086
|
+
* safe to send twice. Outside a transaction, a read that fails because the
|
|
2087
|
+
* connection it went out on had already been closed by the server
|
|
2088
|
+
* ({@link isStaleConnectionError}) is sent once more, after one event-loop
|
|
2089
|
+
* turn, on whatever connection the pool hands out next.
|
|
2090
|
+
*
|
|
2091
|
+
* Reads only, and the distinction is the whole design. For a write, "the
|
|
2092
|
+
* connection died" does not say whether the statement committed first: a
|
|
2093
|
+
* connection can drop after the server commits and before the reply
|
|
2094
|
+
* arrives, so resending an INSERT can insert it twice. A read has no such
|
|
2095
|
+
* outcome. Nor inside a transaction, where the connection that died WAS the
|
|
2096
|
+
* transaction and a retry on another one would run outside it.
|
|
2097
|
+
*
|
|
2098
|
+
* The loop turn is what makes a second attempt worth making: pg-pool evicts
|
|
2099
|
+
* an idle connection whose close it has read, so after one poll phase every
|
|
2100
|
+
* other connection that died alongside this one is gone from the idle list
|
|
2101
|
+
* rather than lent out to the retry.
|
|
2102
|
+
*
|
|
2103
|
+
* Every read call site uses this and every write uses `queryWithTimeout`
|
|
2104
|
+
* directly; `src/test/stale-connection-retry.test.ts` pins which is which.
|
|
2105
|
+
*/
|
|
2106
|
+
readWithTimeout(sql, params, timeout, preparedName) {
|
|
2107
|
+
return this.queryWithTimeout(sql, params, timeout, preparedName, true);
|
|
2108
|
+
}
|
|
2081
2109
|
/**
|
|
2082
2110
|
* Execute a pool.query with an optional timeout.
|
|
2083
2111
|
* If timeout is set, races the query against a timer and rejects on expiry.
|
|
2084
2112
|
* pg driver errors are translated to typed Turbine errors via wrapPgError.
|
|
2113
|
+
* `retryIfStale` is {@link readWithTimeout}'s; nothing else sets it.
|
|
2085
2114
|
*/
|
|
2086
|
-
async queryWithTimeout(sql, params, timeout, preparedName) {
|
|
2087
|
-
|
|
2115
|
+
async queryWithTimeout(sql, params, timeout, preparedName, retryIfStale = false) {
|
|
2116
|
+
let start = performance.now();
|
|
2088
2117
|
const action = this.currentAction;
|
|
2089
2118
|
// Build the query argument, use object form with `name` for prepared
|
|
2090
2119
|
// statements, or the plain (text, values) form otherwise.
|
|
@@ -2094,13 +2123,33 @@ export class QueryInterface {
|
|
|
2094
2123
|
// cast. Guarded at runtime by `preparedStatementsEnabled`, which only the
|
|
2095
2124
|
// drivers that accept it turn on.
|
|
2096
2125
|
const usePrepared = preparedName && this.preparedStatementsEnabled;
|
|
2097
|
-
const
|
|
2126
|
+
const send = () => usePrepared
|
|
2098
2127
|
? this.pool.query({
|
|
2099
2128
|
name: preparedName,
|
|
2100
2129
|
text: sql,
|
|
2101
2130
|
values: params,
|
|
2102
2131
|
})
|
|
2103
2132
|
: this.pool.query(sql, params);
|
|
2133
|
+
let exec = send();
|
|
2134
|
+
// Set when the caller's timeout has already answered, so a first attempt
|
|
2135
|
+
// that fails AFTER that does not send a retry nobody is waiting for.
|
|
2136
|
+
let abandoned = false;
|
|
2137
|
+
if (retryIfStale && !this.txScoped) {
|
|
2138
|
+
exec = exec.catch(async (err) => {
|
|
2139
|
+
if (abandoned || !isStaleConnectionError(err))
|
|
2140
|
+
throw err;
|
|
2141
|
+
// The failed attempt is reported as its own event, marked `retried`,
|
|
2142
|
+
// so a listener counting errors sees it and one alerting on failed
|
|
2143
|
+
// CALLS can skip it. The final event below times the retry alone.
|
|
2144
|
+
const wrapped = wrapPgError(err);
|
|
2145
|
+
this.emitQueryEvent(sql, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined, true);
|
|
2146
|
+
await settleEventLoop();
|
|
2147
|
+
if (abandoned)
|
|
2148
|
+
throw err;
|
|
2149
|
+
start = performance.now();
|
|
2150
|
+
return send();
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2104
2153
|
if (!timeout) {
|
|
2105
2154
|
try {
|
|
2106
2155
|
const result = await exec;
|
|
@@ -2115,7 +2164,10 @@ export class QueryInterface {
|
|
|
2115
2164
|
}
|
|
2116
2165
|
let timer;
|
|
2117
2166
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2118
|
-
timer = setTimeout(() =>
|
|
2167
|
+
timer = setTimeout(() => {
|
|
2168
|
+
abandoned = true;
|
|
2169
|
+
reject(new TimeoutError(timeout));
|
|
2170
|
+
}, timeout);
|
|
2119
2171
|
});
|
|
2120
2172
|
try {
|
|
2121
2173
|
const result = await Promise.race([exec, timeoutPromise]);
|
|
@@ -2157,19 +2209,37 @@ export class QueryInterface {
|
|
|
2157
2209
|
// Write compilation (extracted to writes.ts).
|
|
2158
2210
|
// ---------------------------------------------------------------------------
|
|
2159
2211
|
buildCreate(args) {
|
|
2160
|
-
return writesMod.buildCreate(this.ctx, args);
|
|
2212
|
+
return writesMod.buildCreate(this.ctx, args, this.resolveWriteProjection(args));
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* A single-row write's `select` / `omit`, resolved through the SAME
|
|
2216
|
+
* `resolveProjection` reads use (see WriteProjection in writes.ts), or
|
|
2217
|
+
* `undefined` for the default return shape. Resolved here rather than in
|
|
2218
|
+
* writes.ts because relations.ts imports writes.ts.
|
|
2219
|
+
*/
|
|
2220
|
+
resolveWriteProjection(args) {
|
|
2221
|
+
if (args.select === undefined && args.omit === undefined)
|
|
2222
|
+
return undefined;
|
|
2223
|
+
const columns = relationsMod.resolveProjection(this.ctx, this.table, this.tableMeta, args.select, args.omit, false);
|
|
2224
|
+
if (!columns || columns.length === 0) {
|
|
2225
|
+
// Only an `omit` naming every column gets here (an empty `select` is
|
|
2226
|
+
// refused inside resolveProjection): a write must return SOMETHING, and
|
|
2227
|
+
// an empty RETURNING list is a syntax error on every engine.
|
|
2228
|
+
throw new ValidationError(`\`omit\` on a write to "${this.table}" leaves no column to return. Name the fields to keep with \`select\` instead.`);
|
|
2229
|
+
}
|
|
2230
|
+
return { columns };
|
|
2161
2231
|
}
|
|
2162
2232
|
buildCreateMany(args) {
|
|
2163
2233
|
return writesMod.buildCreateMany(this.ctx, args);
|
|
2164
2234
|
}
|
|
2165
2235
|
buildUpdate(args) {
|
|
2166
|
-
return writesMod.buildUpdate(this.ctx, args);
|
|
2236
|
+
return writesMod.buildUpdate(this.ctx, args, this.resolveWriteProjection(args));
|
|
2167
2237
|
}
|
|
2168
2238
|
buildDelete(args) {
|
|
2169
|
-
return writesMod.buildDelete(this.ctx, args);
|
|
2239
|
+
return writesMod.buildDelete(this.ctx, args, this.resolveWriteProjection(args));
|
|
2170
2240
|
}
|
|
2171
2241
|
buildUpsert(args) {
|
|
2172
|
-
return writesMod.buildUpsert(this.ctx, args);
|
|
2242
|
+
return writesMod.buildUpsert(this.ctx, args, this.resolveWriteProjection(args));
|
|
2173
2243
|
}
|
|
2174
2244
|
buildUpdateMany(args) {
|
|
2175
2245
|
return writesMod.buildUpdateMany(this.ctx, args);
|
|
@@ -2250,7 +2320,7 @@ export class QueryInterface {
|
|
|
2250
2320
|
}
|
|
2251
2321
|
}
|
|
2252
2322
|
const deferred = this.buildFindUnique(args);
|
|
2253
|
-
const result = await this.
|
|
2323
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
2254
2324
|
return deferred.transform(result);
|
|
2255
2325
|
});
|
|
2256
2326
|
}
|
|
@@ -2274,7 +2344,7 @@ export class QueryInterface {
|
|
|
2274
2344
|
const proj = includeKeysForBatching(this.tableMeta, args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, resolveUnsafeFlag(args.includePii, 'includePii')));
|
|
2275
2345
|
const baseArgs = { ...args, with: undefined, select: proj.select, omit: proj.omit };
|
|
2276
2346
|
const deferred = this.buildFindUnique(baseArgs);
|
|
2277
|
-
const result = await this.
|
|
2347
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
2278
2348
|
const entity = deferred.transform(result);
|
|
2279
2349
|
// A miss still walks the `with` tree (compile-only child builds), so the
|
|
2280
2350
|
// same args throw or pass identically whether or not the row exists,
|
|
@@ -2508,7 +2578,7 @@ export class QueryInterface {
|
|
|
2508
2578
|
}
|
|
2509
2579
|
}
|
|
2510
2580
|
const deferred = this.buildFindMany(args);
|
|
2511
|
-
const result = await this.
|
|
2581
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
2512
2582
|
return deferred.transform(result);
|
|
2513
2583
|
});
|
|
2514
2584
|
}
|
|
@@ -3146,7 +3216,7 @@ export class QueryInterface {
|
|
|
3146
3216
|
...args,
|
|
3147
3217
|
limit: batchSize + 1,
|
|
3148
3218
|
});
|
|
3149
|
-
const speculativeResult = await this.
|
|
3219
|
+
const speculativeResult = await this.readWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout, streamPreparedName);
|
|
3150
3220
|
if (speculativeResult.rows.length <= batchSize) {
|
|
3151
3221
|
// Small drain, hand over the whole result and return, no cursor needed.
|
|
3152
3222
|
if (speculativeResult.rows.length > 0)
|
|
@@ -3166,7 +3236,8 @@ export class QueryInterface {
|
|
|
3166
3236
|
// connection, so `pool.query` already IS that connection. The stream rides
|
|
3167
3237
|
// on it, is never released here, and the dialect is told to emit no
|
|
3168
3238
|
// transaction control of its own (`ambientTransaction`).
|
|
3169
|
-
const
|
|
3239
|
+
const held = this.txScoped ? null : await this.acquireConnection();
|
|
3240
|
+
const client = held?.client ?? null;
|
|
3170
3241
|
const conn = client ?? {
|
|
3171
3242
|
query: async (text, values) => (await this.pool.query(text, values)),
|
|
3172
3243
|
};
|
|
@@ -3177,10 +3248,10 @@ export class QueryInterface {
|
|
|
3177
3248
|
}
|
|
3178
3249
|
catch (err) {
|
|
3179
3250
|
// Wrap pg constraint errors so streaming surfaces typed errors like the rest of the API
|
|
3180
|
-
throw wrapPgError(err);
|
|
3251
|
+
throw explainConnectionLoss(wrapPgError(err), held?.checkout.lostWith);
|
|
3181
3252
|
}
|
|
3182
3253
|
finally {
|
|
3183
|
-
|
|
3254
|
+
held?.checkout.release();
|
|
3184
3255
|
}
|
|
3185
3256
|
}
|
|
3186
3257
|
/**
|
|
@@ -3305,7 +3376,7 @@ export class QueryInterface {
|
|
|
3305
3376
|
}
|
|
3306
3377
|
}
|
|
3307
3378
|
const deferred = this.buildFindFirst(args);
|
|
3308
|
-
const result = await this.
|
|
3379
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
3309
3380
|
return deferred.transform(result);
|
|
3310
3381
|
});
|
|
3311
3382
|
}
|
|
@@ -3329,7 +3400,7 @@ export class QueryInterface {
|
|
|
3329
3400
|
async findFirstOrThrow(args) {
|
|
3330
3401
|
return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
|
|
3331
3402
|
const deferred = this.buildFindFirstOrThrow(args);
|
|
3332
|
-
const result = await this.
|
|
3403
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
3333
3404
|
return deferred.transform(result);
|
|
3334
3405
|
});
|
|
3335
3406
|
}
|
|
@@ -3358,7 +3429,7 @@ export class QueryInterface {
|
|
|
3358
3429
|
async findUniqueOrThrow(args) {
|
|
3359
3430
|
return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
|
|
3360
3431
|
const deferred = this.buildFindUniqueOrThrow(args);
|
|
3361
|
-
const result = await this.
|
|
3432
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
3362
3433
|
return deferred.transform(result);
|
|
3363
3434
|
});
|
|
3364
3435
|
}
|
|
@@ -3420,58 +3491,50 @@ export class QueryInterface {
|
|
|
3420
3491
|
// -------------------------------------------------------------------------
|
|
3421
3492
|
async nestedCreate(args) {
|
|
3422
3493
|
const data = args.data;
|
|
3494
|
+
// Validated up front, before any row is written, and then applied by the
|
|
3495
|
+
// nested engine's final read-back, which already speaks select/omit.
|
|
3496
|
+
this.resolveWriteProjection(args);
|
|
3497
|
+
const shape = { select: args.select, omit: args.omit };
|
|
3423
3498
|
if (this.txScoped) {
|
|
3424
3499
|
const ctx = this.buildNestedCtx();
|
|
3425
|
-
return executeNestedCreate(ctx, this.table, data);
|
|
3500
|
+
return executeNestedCreate(ctx, this.table, data, 0, [], shape);
|
|
3426
3501
|
}
|
|
3427
3502
|
return this.runInImplicitTx(async (ctx) => {
|
|
3428
|
-
const result = await executeNestedCreate(ctx, this.table, data);
|
|
3503
|
+
const result = await executeNestedCreate(ctx, this.table, data, 0, [], shape);
|
|
3429
3504
|
return result;
|
|
3430
3505
|
});
|
|
3431
3506
|
}
|
|
3432
3507
|
async nestedUpdate(args) {
|
|
3433
3508
|
const data = args.data;
|
|
3434
3509
|
const where = args.where;
|
|
3510
|
+
this.resolveWriteProjection(args);
|
|
3511
|
+
const shape = { select: args.select, omit: args.omit };
|
|
3435
3512
|
if (this.txScoped) {
|
|
3436
3513
|
const ctx = this.buildNestedCtx();
|
|
3437
|
-
return executeNestedUpdate(ctx, this.table, where, data);
|
|
3514
|
+
return executeNestedUpdate(ctx, this.table, where, data, 0, [], shape);
|
|
3438
3515
|
}
|
|
3439
3516
|
return this.runInImplicitTx(async (ctx) => {
|
|
3440
|
-
const result = await executeNestedUpdate(ctx, this.table, where, data);
|
|
3517
|
+
const result = await executeNestedUpdate(ctx, this.table, where, data, 0, [], shape);
|
|
3441
3518
|
return result;
|
|
3442
3519
|
});
|
|
3443
3520
|
}
|
|
3444
3521
|
/**
|
|
3445
|
-
* Check out a pooled connection,
|
|
3446
|
-
*
|
|
3447
|
-
*
|
|
3448
|
-
*
|
|
3449
|
-
* (SQLSTATE 28P01), no such database (3D000), nothing listening
|
|
3450
|
-
* (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
|
|
3451
|
-
* those surfaces from an ordinary `db.users.create({ data: { ...nested } })`
|
|
3452
|
-
* as a raw pg `DatabaseError` whose `.code` is a SQLSTATE, on the same
|
|
3453
|
-
* property Turbine puts `TURBINE_E0NN` in.
|
|
3454
|
-
*
|
|
3455
|
-
* client.ts has its own copy for `$transaction` / `connect()`; this one
|
|
3456
|
-
* exists because `query/` must not import client.ts (circular dependency).
|
|
3457
|
-
* The query paths need no equivalent: `pool.query()` opens the connection
|
|
3458
|
-
* itself and rejects with the connect error, which the query boundary
|
|
3459
|
-
* already wraps.
|
|
3522
|
+
* Check out a guarded pooled connection, as a typed error when that fails.
|
|
3523
|
+
* Shared with client.ts through checkout.ts (see there for why each part
|
|
3524
|
+
* matters); the cursor stream holds it, and a nested write opens its
|
|
3525
|
+
* implicit transaction on one through `openCheckout`.
|
|
3460
3526
|
*/
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
return await this.pool.connect();
|
|
3464
|
-
}
|
|
3465
|
-
catch (err) {
|
|
3466
|
-
throw wrapPgError(err);
|
|
3467
|
-
}
|
|
3527
|
+
acquireConnection() {
|
|
3528
|
+
return acquireConnection(this.pool);
|
|
3468
3529
|
}
|
|
3469
3530
|
async runInImplicitTx(fn) {
|
|
3470
|
-
|
|
3471
|
-
|
|
3531
|
+
// BEGIN runs inside openCheckout, which sends it once more on a fresh
|
|
3532
|
+
// connection when the first one turns out to be dead and releases the
|
|
3533
|
+
// connection itself when BEGIN fails for good. The catch below therefore
|
|
3534
|
+
// only sees a transaction that began, and never emits a stray ROLLBACK on
|
|
3535
|
+
// a connection that opened none.
|
|
3536
|
+
const { client, checkout } = await openCheckout(this.pool, (c) => c.query(this.dialect.beginStatement()));
|
|
3472
3537
|
try {
|
|
3473
|
-
await client.query(this.dialect.beginStatement());
|
|
3474
|
-
began = true;
|
|
3475
3538
|
const { TransactionClient } = await import('../client.js');
|
|
3476
3539
|
const tx = new TransactionClient(
|
|
3477
3540
|
// biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
|
|
@@ -3495,20 +3558,16 @@ export class QueryInterface {
|
|
|
3495
3558
|
return result;
|
|
3496
3559
|
}
|
|
3497
3560
|
catch (err) {
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
}
|
|
3504
|
-
catch {
|
|
3505
|
-
// Best-effort rollback: connection may have died.
|
|
3506
|
-
}
|
|
3561
|
+
try {
|
|
3562
|
+
await client.query(this.dialect.rollbackStatement());
|
|
3563
|
+
}
|
|
3564
|
+
catch {
|
|
3565
|
+
// Best-effort rollback: connection may have died.
|
|
3507
3566
|
}
|
|
3508
|
-
throw err;
|
|
3567
|
+
throw explainConnectionLoss(err, checkout.lostWith);
|
|
3509
3568
|
}
|
|
3510
3569
|
finally {
|
|
3511
|
-
|
|
3570
|
+
checkout.release();
|
|
3512
3571
|
}
|
|
3513
3572
|
}
|
|
3514
3573
|
buildNestedCtx() {
|
|
@@ -3571,7 +3630,7 @@ export class QueryInterface {
|
|
|
3571
3630
|
async count(args) {
|
|
3572
3631
|
return this.executeWithMiddleware('count', (args ?? {}), async () => {
|
|
3573
3632
|
const deferred = this.buildCount(args);
|
|
3574
|
-
const result = await this.
|
|
3633
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
3575
3634
|
return deferred.transform(result);
|
|
3576
3635
|
});
|
|
3577
3636
|
}
|
|
@@ -3615,7 +3674,7 @@ export class QueryInterface {
|
|
|
3615
3674
|
async groupBy(args) {
|
|
3616
3675
|
return this.executeWithMiddleware('groupBy', args, async () => {
|
|
3617
3676
|
const deferred = this.buildGroupBy(args);
|
|
3618
|
-
const result = await this.
|
|
3677
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
3619
3678
|
return deferred.transform(result);
|
|
3620
3679
|
});
|
|
3621
3680
|
}
|
|
@@ -3634,7 +3693,7 @@ export class QueryInterface {
|
|
|
3634
3693
|
async aggregate(args) {
|
|
3635
3694
|
return this.executeWithMiddleware('aggregate', args, async () => {
|
|
3636
3695
|
const deferred = this.buildAggregate(args);
|
|
3637
|
-
const result = await this.
|
|
3696
|
+
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
3638
3697
|
return deferred.transform(result);
|
|
3639
3698
|
});
|
|
3640
3699
|
}
|
package/dist/query/deferred.d.ts
CHANGED
|
@@ -108,6 +108,15 @@ export interface QueryEvent {
|
|
|
108
108
|
* batch statements are timed individually. Absent for everything else.
|
|
109
109
|
*/
|
|
110
110
|
batch?: 'pipeline' | 'transaction';
|
|
111
|
+
/**
|
|
112
|
+
* Set on a READ that failed because its connection had already been closed
|
|
113
|
+
* by the server, and that Turbine therefore sent once more on a fresh one.
|
|
114
|
+
* This event carries that first attempt's `error`; the retry reports its own
|
|
115
|
+
* event, so a failed read that was then answered shows as two events and a
|
|
116
|
+
* listener alerting on failed calls can skip the ones marked here. Writes
|
|
117
|
+
* and statements inside a transaction are never retried.
|
|
118
|
+
*/
|
|
119
|
+
retried?: true;
|
|
111
120
|
}
|
|
112
121
|
export type QueryEventListener = (event: QueryEvent) => void;
|
|
113
122
|
/** Options passed from TurbineClient to QueryInterface */
|
|
@@ -114,6 +114,9 @@ export const FIND_MANY_STREAM_OPTIONS = {
|
|
|
114
114
|
};
|
|
115
115
|
export const CREATE_OPTIONS = {
|
|
116
116
|
data: 'prisma',
|
|
117
|
+
// Field names, so hand-translated; prisma-compat narrows write results itself.
|
|
118
|
+
select: 'prisma',
|
|
119
|
+
omit: 'prisma',
|
|
117
120
|
timeout: 'native',
|
|
118
121
|
};
|
|
119
122
|
export const CREATE_MANY_OPTIONS = {
|
|
@@ -124,6 +127,9 @@ export const CREATE_MANY_OPTIONS = {
|
|
|
124
127
|
export const UPDATE_OPTIONS = {
|
|
125
128
|
where: 'prisma',
|
|
126
129
|
data: 'prisma',
|
|
130
|
+
// Field names, so hand-translated; prisma-compat narrows write results itself.
|
|
131
|
+
select: 'prisma',
|
|
132
|
+
omit: 'prisma',
|
|
127
133
|
// `{ field, expected }`, and `field` is a FIELD NAME, so it has to be renamed
|
|
128
134
|
// into turbine's naming space rather than copied. See THE ONE RULE above.
|
|
129
135
|
optimisticLock: 'prisma',
|
|
@@ -140,6 +146,9 @@ export const UPDATE_MANY_OPTIONS = {
|
|
|
140
146
|
};
|
|
141
147
|
export const DELETE_OPTIONS = {
|
|
142
148
|
where: 'prisma',
|
|
149
|
+
// Field names, so hand-translated; prisma-compat narrows write results itself.
|
|
150
|
+
select: 'prisma',
|
|
151
|
+
omit: 'prisma',
|
|
143
152
|
timeout: 'native',
|
|
144
153
|
allowFullTableScan: 'native',
|
|
145
154
|
skipGlobalFilters: 'native',
|
|
@@ -152,6 +161,9 @@ export const DELETE_MANY_OPTIONS = {
|
|
|
152
161
|
};
|
|
153
162
|
export const UPSERT_OPTIONS = {
|
|
154
163
|
where: 'prisma',
|
|
164
|
+
// Field names, so hand-translated; prisma-compat narrows write results itself.
|
|
165
|
+
select: 'prisma',
|
|
166
|
+
omit: 'prisma',
|
|
155
167
|
create: 'prisma',
|
|
156
168
|
update: 'prisma',
|
|
157
169
|
timeout: 'native',
|
package/dist/query/types.d.ts
CHANGED
|
@@ -870,12 +870,28 @@ export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWit
|
|
|
870
870
|
*/
|
|
871
871
|
batchSize?: number;
|
|
872
872
|
}
|
|
873
|
-
export interface CreateArgs<T, R extends object = {}> {
|
|
873
|
+
export interface CreateArgs<T, R extends object = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
|
|
874
874
|
/**
|
|
875
875
|
* Row data. On typed clients, relation names additionally accept nested
|
|
876
876
|
* write ops ({@link NestedCreateOp}): `create` / `connect` / `connectOrCreate`.
|
|
877
877
|
*/
|
|
878
878
|
data: CreateDataInput<T, R>;
|
|
879
|
+
/**
|
|
880
|
+
* Return only these fields of the written row, instead of the whole row.
|
|
881
|
+
* Keys are checked against `T` (see {@link FieldFlags}) and follow the read
|
|
882
|
+
* rules: an unknown name throws E003, a `select` must name at least one field,
|
|
883
|
+
* and it cannot be combined with `omit`. Narrows the `RETURNING` list itself,
|
|
884
|
+
* so the unselected columns never cross the wire, which is the point on a
|
|
885
|
+
* table with a large JSON or text column. Naming a PII-tagged column here is
|
|
886
|
+
* the explicit opt-in that returns it, exactly as on a read.
|
|
887
|
+
*/
|
|
888
|
+
select?: S & FieldFlags<T, S>;
|
|
889
|
+
/**
|
|
890
|
+
* Return the written row without these fields. Same rules as `select`;
|
|
891
|
+
* PII-tagged columns stay excluded, as they are from every default write
|
|
892
|
+
* return.
|
|
893
|
+
*/
|
|
894
|
+
omit?: O & FieldFlags<T, O>;
|
|
879
895
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
880
896
|
timeout?: number;
|
|
881
897
|
}
|
|
@@ -916,7 +932,7 @@ export type UpdateOperatorInput<V> = {
|
|
|
916
932
|
export type UpdateInput<T> = {
|
|
917
933
|
[K in keyof T]?: T[K] | UpdateOperatorInput<T[K]>;
|
|
918
934
|
};
|
|
919
|
-
export interface UpdateArgs<T, R extends object = {}> {
|
|
935
|
+
export interface UpdateArgs<T, R extends object = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
|
|
920
936
|
/** Row selector. Keys are checked against `T` and `R` (see {@link WhereClause}). */
|
|
921
937
|
where: WhereClause<T, R>;
|
|
922
938
|
/**
|
|
@@ -925,6 +941,22 @@ export interface UpdateArgs<T, R extends object = {}> {
|
|
|
925
941
|
* / `disconnect` / `set` / `delete` / `update` / `upsert`.
|
|
926
942
|
*/
|
|
927
943
|
data: UpdateDataInput<T, R>;
|
|
944
|
+
/**
|
|
945
|
+
* Return only these fields of the updated row, instead of the whole row.
|
|
946
|
+
* Keys are checked against `T` (see {@link FieldFlags}) and follow the read
|
|
947
|
+
* rules: an unknown name throws E003, a `select` must name at least one field,
|
|
948
|
+
* and it cannot be combined with `omit`. Narrows the `RETURNING` list itself,
|
|
949
|
+
* so the unselected columns never cross the wire, which is the point on a
|
|
950
|
+
* table with a large JSON or text column. Naming a PII-tagged column here is
|
|
951
|
+
* the explicit opt-in that returns it, exactly as on a read.
|
|
952
|
+
*/
|
|
953
|
+
select?: S & FieldFlags<T, S>;
|
|
954
|
+
/**
|
|
955
|
+
* Return the updated row without these fields. Same rules as `select`;
|
|
956
|
+
* PII-tagged columns stay excluded, as they are from every default write
|
|
957
|
+
* return.
|
|
958
|
+
*/
|
|
959
|
+
omit?: O & FieldFlags<T, O>;
|
|
928
960
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
929
961
|
timeout?: number;
|
|
930
962
|
/**
|
|
@@ -974,8 +1006,24 @@ export interface UpdateManyArgs<T, R extends object = {}> {
|
|
|
974
1006
|
/** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
|
|
975
1007
|
skipGlobalFilters?: SkipGlobalFilters;
|
|
976
1008
|
}
|
|
977
|
-
export interface DeleteArgs<T, R extends object = {}> {
|
|
1009
|
+
export interface DeleteArgs<T, R extends object = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
|
|
978
1010
|
where: WhereClause<T, R>;
|
|
1011
|
+
/**
|
|
1012
|
+
* Return only these fields of the deleted row, instead of the whole row.
|
|
1013
|
+
* Keys are checked against `T` (see {@link FieldFlags}) and follow the read
|
|
1014
|
+
* rules: an unknown name throws E003, a `select` must name at least one field,
|
|
1015
|
+
* and it cannot be combined with `omit`. Narrows the `RETURNING` list itself,
|
|
1016
|
+
* so the unselected columns never cross the wire, which is the point on a
|
|
1017
|
+
* table with a large JSON or text column. Naming a PII-tagged column here is
|
|
1018
|
+
* the explicit opt-in that returns it, exactly as on a read.
|
|
1019
|
+
*/
|
|
1020
|
+
select?: S & FieldFlags<T, S>;
|
|
1021
|
+
/**
|
|
1022
|
+
* Return the deleted row without these fields. Same rules as `select`;
|
|
1023
|
+
* PII-tagged columns stay excluded, as they are from every default write
|
|
1024
|
+
* return.
|
|
1025
|
+
*/
|
|
1026
|
+
omit?: O & FieldFlags<T, O>;
|
|
979
1027
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
980
1028
|
timeout?: number;
|
|
981
1029
|
/** See {@link UpdateArgs.allowFullTableScan}. */
|
|
@@ -992,8 +1040,24 @@ export interface DeleteManyArgs<T, R extends object = {}> {
|
|
|
992
1040
|
/** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
|
|
993
1041
|
skipGlobalFilters?: SkipGlobalFilters;
|
|
994
1042
|
}
|
|
995
|
-
export interface UpsertArgs<T, R extends object = {}> {
|
|
1043
|
+
export interface UpsertArgs<T, R extends object = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
|
|
996
1044
|
where: WhereClause<T, R>;
|
|
1045
|
+
/**
|
|
1046
|
+
* Return only these fields of the inserted or updated row, instead of the whole row.
|
|
1047
|
+
* Keys are checked against `T` (see {@link FieldFlags}) and follow the read
|
|
1048
|
+
* rules: an unknown name throws E003, a `select` must name at least one field,
|
|
1049
|
+
* and it cannot be combined with `omit`. Narrows the `RETURNING` list itself,
|
|
1050
|
+
* so the unselected columns never cross the wire, which is the point on a
|
|
1051
|
+
* table with a large JSON or text column. Naming a PII-tagged column here is
|
|
1052
|
+
* the explicit opt-in that returns it, exactly as on a read.
|
|
1053
|
+
*/
|
|
1054
|
+
select?: S & FieldFlags<T, S>;
|
|
1055
|
+
/**
|
|
1056
|
+
* Return the inserted or updated row without these fields. Same rules as `select`;
|
|
1057
|
+
* PII-tagged columns stay excluded, as they are from every default write
|
|
1058
|
+
* return.
|
|
1059
|
+
*/
|
|
1060
|
+
omit?: O & FieldFlags<T, O>;
|
|
997
1061
|
/** The row to insert when `where` matches nothing. Plain values only: there is no stored value to operate on yet. */
|
|
998
1062
|
create: Partial<T>;
|
|
999
1063
|
/**
|