turbine-orm 0.78.0 → 0.79.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/dist/cjs/client.d.ts +47 -2
- package/dist/cjs/client.js +122 -41
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/powql.d.ts +32 -3
- package/dist/cjs/powql.js +96 -20
- package/dist/cjs/prisma-compat.d.ts +1 -1
- package/dist/cjs/prisma-compat.js +11 -7
- package/dist/cjs/query/deferred.d.ts +15 -0
- package/dist/cjs/query-events.d.ts +82 -0
- package/dist/cjs/query-events.js +137 -0
- package/dist/cjs/typed-sql.d.ts +14 -1
- package/dist/cjs/typed-sql.js +12 -8
- package/dist/client.d.ts +47 -2
- package/dist/client.js +123 -42
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/powql.d.ts +32 -3
- package/dist/powql.js +96 -20
- package/dist/prisma-compat.d.ts +1 -1
- package/dist/prisma-compat.js +11 -7
- package/dist/query/deferred.d.ts +15 -0
- package/dist/query-events.d.ts +82 -0
- package/dist/query-events.js +127 -0
- package/dist/typed-sql.d.ts +14 -1
- package/dist/typed-sql.js +12 -8
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -24,12 +24,13 @@
|
|
|
24
24
|
import pg from 'pg';
|
|
25
25
|
import { mergeConnectionStringOptions } from './connection-url.js';
|
|
26
26
|
import { postgresDialect } from './dialect.js';
|
|
27
|
-
import { ConnectionError, errorMessageModesDiverged, registerClientErrorMessageMode, runWithErrorMessageMode, setErrorMessageMode, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
27
|
+
import { ConnectionError, errorMessageModesDiverged, PipelineError, registerClientErrorMessageMode, runWithErrorMessageMode, setErrorMessageMode, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
28
28
|
import { ObserveEngine } from './observe.js';
|
|
29
29
|
import { executePipeline, pipelineSupported } from './pipeline.js';
|
|
30
30
|
import { QueryInterface, } from './query/index.js';
|
|
31
31
|
import { markTurbineParser, ownLookup, quoteIdent, registerUtcTemporalParsers, suggestKey, warnParserOverwrite, } from './query/utils.js';
|
|
32
32
|
import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
|
|
33
|
+
import { currentQueryTag, deferredIdentity, emitStatementEvent, rawIdentity, resultRowCount, runReportedStatement, runWithQueryTag, } from './query-events.js';
|
|
33
34
|
import { createSubscription, validateChannel, } from './realtime.js';
|
|
34
35
|
import { buildTypedSql, TypedSqlQuery } from './typed-sql.js';
|
|
35
36
|
export async function withRetry(fn, options) {
|
|
@@ -531,13 +532,8 @@ export class TransactionClient {
|
|
|
531
532
|
sql += this.dialect.paramPlaceholder(i + 1);
|
|
532
533
|
}
|
|
533
534
|
});
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return result.rows;
|
|
537
|
-
}
|
|
538
|
-
catch (err) {
|
|
539
|
-
throw wrapPgError(err);
|
|
540
|
-
}
|
|
535
|
+
const result = await runReportedStatement(this.queryOptions?._onQuery, rawIdentity('raw'), sql, values, () => this.client.query(sql, values), wrapPgError);
|
|
536
|
+
return result.rows;
|
|
541
537
|
}
|
|
542
538
|
/**
|
|
543
539
|
* @internal The `turbine-orm/prisma-compat` adapter's transaction seam. NOT
|
|
@@ -561,16 +557,13 @@ export class TransactionClient {
|
|
|
561
557
|
* inside the same BEGIN/COMMIT (and any active SAVEPOINT) as every other
|
|
562
558
|
* statement in the callback. Driver errors are translated by `wrapPgError`,
|
|
563
559
|
* exactly as pool-scoped and table-scoped queries are. Like {@link raw}, it
|
|
564
|
-
*
|
|
560
|
+
* runs no middleware, and it emits one `$on('query')` event with model
|
|
561
|
+
* `'$raw'` and `action` set to the optional `action` label (the adapter
|
|
562
|
+
* passes `'$queryRaw'` / `'$executeRaw'` ...; default `'rawQuery'`).
|
|
565
563
|
*/
|
|
566
|
-
async rawQuery(text, params = []) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
return { rows: result.rows, rowCount: result.rowCount };
|
|
570
|
-
}
|
|
571
|
-
catch (err) {
|
|
572
|
-
throw wrapPgError(err);
|
|
573
|
-
}
|
|
564
|
+
async rawQuery(text, params = [], action = 'rawQuery') {
|
|
565
|
+
const result = await runReportedStatement(this.queryOptions?._onQuery, rawIdentity(action), text, params, () => this.client.query(text, params), wrapPgError);
|
|
566
|
+
return { rows: result.rows, rowCount: result.rowCount };
|
|
574
567
|
}
|
|
575
568
|
/**
|
|
576
569
|
* Create a pool-like wrapper around the transaction client.
|
|
@@ -900,7 +893,11 @@ export class TurbineClient {
|
|
|
900
893
|
_onQuery: (event) => {
|
|
901
894
|
if (this.queryListeners.size === 0)
|
|
902
895
|
return;
|
|
903
|
-
|
|
896
|
+
// The $tag() scope is read here, at the one seam every emitter shares,
|
|
897
|
+
// so a tag reaches model, raw, pipeline and batch events alike.
|
|
898
|
+
const tag = event.tag ?? currentQueryTag();
|
|
899
|
+
const tagged = tag === undefined || event.tag !== undefined ? event : { ...event, tag };
|
|
900
|
+
const emitted = this.queryParamsVisible ? tagged : { ...tagged, params: tagged.params.map(() => '[REDACTED]') };
|
|
904
901
|
for (const listener of this.queryListeners) {
|
|
905
902
|
try {
|
|
906
903
|
listener(emitted);
|
|
@@ -1512,7 +1509,63 @@ export class TurbineClient {
|
|
|
1512
1509
|
// where VALUES as soon as any verbose client existed in the process. The
|
|
1513
1510
|
// scope is established around the await, so every continuation of the batch
|
|
1514
1511
|
// resolves this client's mode.
|
|
1515
|
-
|
|
1512
|
+
const sink = this.queryListeners.size > 0 ? this.queryOptions._onQuery : undefined;
|
|
1513
|
+
if (!sink)
|
|
1514
|
+
return this.withErrorMode(() => executePipeline(this.pool, queries, options));
|
|
1515
|
+
return this.withErrorMode(() => this.reportedPipeline(sink, queries, options));
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* {@link pipeline} with one `$on('query')` event per statement. Only taken
|
|
1519
|
+
* when a listener is registered, so an unobserved pipeline runs exactly as
|
|
1520
|
+
* before. Each statement's `transform` is wrapped to capture the driver's
|
|
1521
|
+
* row count on the way through (the pipeline returns transformed values, not
|
|
1522
|
+
* driver results). The statements share one round trip, so the batch's wall
|
|
1523
|
+
* time is split evenly across them and each event carries
|
|
1524
|
+
* `batch: 'pipeline'`.
|
|
1525
|
+
*/
|
|
1526
|
+
async reportedPipeline(sink, queries, options) {
|
|
1527
|
+
const rowCounts = new Array(queries.length).fill(undefined);
|
|
1528
|
+
const observed = queries.map((q, i) => ({
|
|
1529
|
+
...q,
|
|
1530
|
+
transform: (raw) => {
|
|
1531
|
+
rowCounts[i] = resultRowCount(raw);
|
|
1532
|
+
return q.transform(raw);
|
|
1533
|
+
},
|
|
1534
|
+
}));
|
|
1535
|
+
const start = performance.now();
|
|
1536
|
+
let failure;
|
|
1537
|
+
try {
|
|
1538
|
+
return await executePipeline(this.pool, observed, options);
|
|
1539
|
+
}
|
|
1540
|
+
catch (err) {
|
|
1541
|
+
failure = err;
|
|
1542
|
+
throw err;
|
|
1543
|
+
}
|
|
1544
|
+
finally {
|
|
1545
|
+
const duration = (performance.now() - start) / queries.length;
|
|
1546
|
+
// PipelineError names the failing slot; any other failure (connect,
|
|
1547
|
+
// BEGIN) is charged to the first statement that produced no result.
|
|
1548
|
+
const failedIndex = failure === undefined
|
|
1549
|
+
? -1
|
|
1550
|
+
: failure instanceof PipelineError && failure.failedIndex !== undefined
|
|
1551
|
+
? failure.failedIndex
|
|
1552
|
+
: rowCounts.indexOf(undefined);
|
|
1553
|
+
queries.forEach((q, i) => {
|
|
1554
|
+
const failed = i === failedIndex;
|
|
1555
|
+
// A slot that never ran (or rolled back unseen) is not reported.
|
|
1556
|
+
if (failed || rowCounts[i] !== undefined) {
|
|
1557
|
+
emitStatementEvent(sink, {
|
|
1558
|
+
sql: q.sql,
|
|
1559
|
+
params: q.params,
|
|
1560
|
+
duration,
|
|
1561
|
+
...deferredIdentity(q.tag),
|
|
1562
|
+
rows: rowCounts[i] ?? 0,
|
|
1563
|
+
batch: 'pipeline',
|
|
1564
|
+
...(failed ? { error: failure instanceof Error ? failure : new Error(String(failure)) } : {}),
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1516
1569
|
}
|
|
1517
1570
|
/**
|
|
1518
1571
|
* Check whether the underlying pool supports the real pipeline protocol.
|
|
@@ -1548,13 +1601,43 @@ export class TurbineClient {
|
|
|
1548
1601
|
if (this.logging) {
|
|
1549
1602
|
console.log(`[turbine] Raw SQL: ${sql.trim().substring(0, 120)}...`);
|
|
1550
1603
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1604
|
+
const result = await runReportedStatement(this.queryOptions._onQuery, rawIdentity('raw'), sql, values, () => this.pool.query(sql, values), (err) => this.withErrorMode(() => wrapPgError(err)));
|
|
1605
|
+
return result.rows;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* @internal The `turbine-orm/prisma-compat` adapter's pool-level raw seam,
|
|
1609
|
+
* the twin of {@link TransactionClient.rawQuery}. NOT application API: use
|
|
1610
|
+
* {@link raw} or {@link sql}, whose tagged templates make concatenating a
|
|
1611
|
+
* value into the SQL text impossible. It exists so the adapter's
|
|
1612
|
+
* `$queryRaw` / `$executeRaw` statements emit a `$on('query')` event (model
|
|
1613
|
+
* `'$raw'`, `action` the adapter's label) instead of running unobserved on
|
|
1614
|
+
* the pool. Errors are translated by `wrapPgError`, like {@link raw}.
|
|
1615
|
+
*/
|
|
1616
|
+
async rawQuery(text, params = [], action = 'rawQuery') {
|
|
1617
|
+
const result = await runReportedStatement(this.queryOptions._onQuery, rawIdentity(action), text, params, () => this.pool.query(text, params), (err) => this.withErrorMode(() => wrapPgError(err)));
|
|
1618
|
+
return { rows: result.rows, rowCount: result.rowCount };
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Run `fn` with `tag` attached to every `$on('query')` event it causes, so a
|
|
1622
|
+
* listener can attribute queries to the feature or code path that issued
|
|
1623
|
+
* them. The scope follows async context: queries in awaited helpers,
|
|
1624
|
+
* transactions, pipelines and raw SQL inside `fn` are all tagged. An inner
|
|
1625
|
+
* `$tag` replaces an outer one for its own duration. The tag is event
|
|
1626
|
+
* metadata only and never reaches SQL.
|
|
1627
|
+
*
|
|
1628
|
+
* Throws {@link ValidationError} (E003) for an empty tag or one longer than
|
|
1629
|
+
* {@link MAX_QUERY_TAG_LENGTH} (128) characters.
|
|
1630
|
+
*
|
|
1631
|
+
* @example
|
|
1632
|
+
* ```ts
|
|
1633
|
+
* const order = await db.$tag('checkout', async () => {
|
|
1634
|
+
* const cart = await db.carts.findUnique({ where: { id } });
|
|
1635
|
+
* return db.orders.create({ data: { cartId: cart.id } });
|
|
1636
|
+
* });
|
|
1637
|
+
* ```
|
|
1638
|
+
*/
|
|
1639
|
+
$tag(tag, fn) {
|
|
1640
|
+
return runWithQueryTag(tag, fn);
|
|
1558
1641
|
}
|
|
1559
1642
|
/**
|
|
1560
1643
|
* Execute a **typed** raw SQL query, Turbine's answer to Prisma's TypedSQL.
|
|
@@ -1594,7 +1677,7 @@ export class TurbineClient {
|
|
|
1594
1677
|
// `withErrorMode` docstring already claimed to cover the typed-SQL builder
|
|
1595
1678
|
// and did not, which left two raw-SQL entry points disagreeing about the
|
|
1596
1679
|
// same statement, since the adjacent `raw` tag WAS scoped.
|
|
1597
|
-
return new TypedSqlQuery(this.pool, sql, params, this.logging, (fn) => this.withErrorMode(fn));
|
|
1680
|
+
return new TypedSqlQuery(this.pool, sql, params, this.logging, (fn) => this.withErrorMode(fn), this.queryOptions._onQuery);
|
|
1598
1681
|
}
|
|
1599
1682
|
// -------------------------------------------------------------------------
|
|
1600
1683
|
// Transaction support (raw, legacy)
|
|
@@ -1834,13 +1917,18 @@ export class TurbineClient {
|
|
|
1834
1917
|
if (queries.length === 0) {
|
|
1835
1918
|
return [];
|
|
1836
1919
|
}
|
|
1920
|
+
// One `$on('query')` event per statement, `batch: 'transaction'`, only
|
|
1921
|
+
// when someone is listening; each statement is timed on its own.
|
|
1922
|
+
const sink = this.queryListeners.size > 0 ? this.queryOptions._onQuery : undefined;
|
|
1923
|
+
const reported = (dq, run) => runReportedStatement(sink, { ...deferredIdentity(dq.tag), batch: 'transaction' }, dq.sql, dq.params, run, wrapPgError);
|
|
1837
1924
|
return this.transaction(async (client) => {
|
|
1838
1925
|
const pipelined = client.supportsPipelining === true &&
|
|
1839
1926
|
this.dialect.resultStrategy !== 'reselect';
|
|
1840
1927
|
if (pipelined) {
|
|
1841
1928
|
// Dispatch every statement before awaiting any reply. The driver's
|
|
1842
|
-
// FIFO guarantee makes settled[i] the reply to queries[i].
|
|
1843
|
-
|
|
1929
|
+
// FIFO guarantee makes settled[i] the reply to queries[i]. Each
|
|
1930
|
+
// statement is timed from its dispatch to its own reply.
|
|
1931
|
+
const settled = await Promise.allSettled(queries.map((dq) => reported(dq, () => client.query(dq.sql, dq.params))));
|
|
1844
1932
|
const results = [];
|
|
1845
1933
|
for (let i = 0; i < settled.length; i++) {
|
|
1846
1934
|
const outcome = settled[i];
|
|
@@ -1853,19 +1941,12 @@ export class TurbineClient {
|
|
|
1853
1941
|
}
|
|
1854
1942
|
const results = [];
|
|
1855
1943
|
for (const dq of queries) {
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
this.dialect.resultStrategy === 'reselect' && dq.reselect
|
|
1863
|
-
? await dq.reselect((sql, params) => client.query(sql, params))
|
|
1864
|
-
: await client.query(dq.sql, dq.params);
|
|
1865
|
-
}
|
|
1866
|
-
catch (err) {
|
|
1867
|
-
throw wrapPgError(err);
|
|
1868
|
-
}
|
|
1944
|
+
// Non-RETURNING engines (resultStrategy 'reselect', e.g. MySQL) attach a
|
|
1945
|
+
// reselect plan that runs the write plus a follow-up SELECT; running
|
|
1946
|
+
// dq.sql alone would transform a row-less write result.
|
|
1947
|
+
const raw = await reported(dq, () => this.dialect.resultStrategy === 'reselect' && dq.reselect
|
|
1948
|
+
? dq.reselect((sql, params) => client.query(sql, params))
|
|
1949
|
+
: client.query(dq.sql, dq.params));
|
|
1869
1950
|
results.push(dq.transform(raw));
|
|
1870
1951
|
}
|
|
1871
1952
|
return results;
|
package/dist/index.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type Me
|
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
46
|
export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
47
47
|
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonEncoding, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderBySpec, type OrderDirection, type PrivilegeOption, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationOrderBy, type RelationOrderByChain, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, UNSAFE, type Unsafe, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
48
|
+
export { MAX_QUERY_TAG_LENGTH, RAW_QUERY_MODEL } from './query-events.js';
|
|
48
49
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
49
50
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, PrismaSchemaSource, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
50
51
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
|
package/dist/index.js
CHANGED
|
@@ -55,6 +55,8 @@ export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
|
55
55
|
// `allowFullTableScan` are unlocked by THIS VALUE and nothing else, so a
|
|
56
56
|
// request body spread into query args cannot enable them (JSON has no symbols).
|
|
57
57
|
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, UNSAFE, } from './query/index.js';
|
|
58
|
+
// $on('query') event metadata: raw-statement model name, $tag() label limit
|
|
59
|
+
export { MAX_QUERY_TAG_LENGTH, RAW_QUERY_MODEL } from './query-events.js';
|
|
58
60
|
// Realtime, LISTEN/NOTIFY pub/sub
|
|
59
61
|
export { validateChannel } from './realtime.js';
|
|
60
62
|
// Schema utilities
|
package/dist/powql.d.ts
CHANGED
|
@@ -155,6 +155,34 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
155
155
|
* the two can never disagree about which limit is in force.
|
|
156
156
|
*/
|
|
157
157
|
private effectiveLimit;
|
|
158
|
+
/**
|
|
159
|
+
* Options for the child-side interfaces the relation loaders and the
|
|
160
|
+
* relation-filter resolvers build: the client's, with `defaultLimit` cleared
|
|
161
|
+
* and the unlimited warning off. `defaultLimit` bounds the caller's PAGE; on
|
|
162
|
+
* an internal fetch that serves that page (a relation's children, a filter's
|
|
163
|
+
* key set) it would cap the whole page's children, or the key set, at the
|
|
164
|
+
* page size: a silent wrong answer by another route than the one
|
|
165
|
+
* {@link pageOf} closes. The SQL batched loader clears it the same way
|
|
166
|
+
* (`batchedChildOptions` in query/builder.ts).
|
|
167
|
+
*/
|
|
168
|
+
private childOptions?;
|
|
169
|
+
private loaderChildOptions;
|
|
170
|
+
/**
|
|
171
|
+
* ONE parent's window of its stitched children: the relation's `offset` and
|
|
172
|
+
* `limit` applied to that parent's own list.
|
|
173
|
+
*
|
|
174
|
+
* A relation `limit` means "at most N on EACH parent". The loaders used to
|
|
175
|
+
* spread it onto the flat child fetch (`post filter .author_id in (...)
|
|
176
|
+
* limit 5`), which caps the TOTAL across every parent in the chunk, so ten
|
|
177
|
+
* parents shared five posts and most got none, with no error. It shipped
|
|
178
|
+
* for the loaders' whole life because the one test of the shape compared
|
|
179
|
+
* the join path against the loader path and both were wrong the same way;
|
|
180
|
+
* the cross-engine benchmark found it, where the wrong answer was the fast
|
|
181
|
+
* one. Every loader now fetches its children unbounded, WITH the relation
|
|
182
|
+
* `orderBy` (which decides which rows the window keeps), and applies this
|
|
183
|
+
* at stitch time: the rule the SQL engines' batched loader has always used.
|
|
184
|
+
*/
|
|
185
|
+
private static pageOf;
|
|
158
186
|
/**
|
|
159
187
|
* Reject a negative `limit` / `offset` before it reaches the engine. PowDB
|
|
160
188
|
* casts both with `as usize` at execution, so below engine 0.20 a negative
|
|
@@ -623,9 +651,10 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
623
651
|
* `referenceKey` on the TARGET table (the join's non-fetched side);
|
|
624
652
|
* - m2m keeps any `orderBy`/`limit`/`offset` on the loader (the junction-order
|
|
625
653
|
* stitch can't be reproduced by the 3-table join deterministically);
|
|
626
|
-
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader
|
|
627
|
-
*
|
|
628
|
-
*
|
|
654
|
+
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader. A
|
|
655
|
+
* to-many relation `limit`/`offset` is a PER-PARENT bound on both paths,
|
|
656
|
+
* fetched unbounded and sliced per parent at stitch time (`pageOf`), so
|
|
657
|
+
* the join statement carries neither clause.
|
|
629
658
|
*/
|
|
630
659
|
private joinEligible;
|
|
631
660
|
/**
|
package/dist/powql.js
CHANGED
|
@@ -357,6 +357,42 @@ export class PowqlInterface {
|
|
|
357
357
|
effectiveLimit(args) {
|
|
358
358
|
return args.limit ?? this.defaultLimit;
|
|
359
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Options for the child-side interfaces the relation loaders and the
|
|
362
|
+
* relation-filter resolvers build: the client's, with `defaultLimit` cleared
|
|
363
|
+
* and the unlimited warning off. `defaultLimit` bounds the caller's PAGE; on
|
|
364
|
+
* an internal fetch that serves that page (a relation's children, a filter's
|
|
365
|
+
* key set) it would cap the whole page's children, or the key set, at the
|
|
366
|
+
* page size: a silent wrong answer by another route than the one
|
|
367
|
+
* {@link pageOf} closes. The SQL batched loader clears it the same way
|
|
368
|
+
* (`batchedChildOptions` in query/builder.ts).
|
|
369
|
+
*/
|
|
370
|
+
childOptions;
|
|
371
|
+
loaderChildOptions() {
|
|
372
|
+
this.childOptions ??= { ...this.options, defaultLimit: undefined, warnOnUnlimited: false };
|
|
373
|
+
return this.childOptions;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* ONE parent's window of its stitched children: the relation's `offset` and
|
|
377
|
+
* `limit` applied to that parent's own list.
|
|
378
|
+
*
|
|
379
|
+
* A relation `limit` means "at most N on EACH parent". The loaders used to
|
|
380
|
+
* spread it onto the flat child fetch (`post filter .author_id in (...)
|
|
381
|
+
* limit 5`), which caps the TOTAL across every parent in the chunk, so ten
|
|
382
|
+
* parents shared five posts and most got none, with no error. It shipped
|
|
383
|
+
* for the loaders' whole life because the one test of the shape compared
|
|
384
|
+
* the join path against the loader path and both were wrong the same way;
|
|
385
|
+
* the cross-engine benchmark found it, where the wrong answer was the fast
|
|
386
|
+
* one. Every loader now fetches its children unbounded, WITH the relation
|
|
387
|
+
* `orderBy` (which decides which rows the window keeps), and applies this
|
|
388
|
+
* at stitch time: the rule the SQL engines' batched loader has always used.
|
|
389
|
+
*/
|
|
390
|
+
static pageOf(rows, limit, offset) {
|
|
391
|
+
if (limit === undefined && !offset)
|
|
392
|
+
return rows;
|
|
393
|
+
const start = offset ?? 0;
|
|
394
|
+
return rows.slice(start, limit === undefined ? undefined : start + limit);
|
|
395
|
+
}
|
|
360
396
|
/**
|
|
361
397
|
* Reject a negative `limit` / `offset` before it reaches the engine. PowDB
|
|
362
398
|
* casts both with `as usize` at execution, so below engine 0.20 a negative
|
|
@@ -938,7 +974,7 @@ export class PowqlInterface {
|
|
|
938
974
|
const childCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
|
|
939
975
|
const localField = this.meta.reverseColumnMap[localCol] ?? localCol;
|
|
940
976
|
const childField = targetMeta.reverseColumnMap[childCol] ?? childCol;
|
|
941
|
-
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.
|
|
977
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
942
978
|
const collect = async (w) => {
|
|
943
979
|
const rows = await targetQi.findMany({
|
|
944
980
|
where: w,
|
|
@@ -977,7 +1013,7 @@ export class PowqlInterface {
|
|
|
977
1013
|
const sourceRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
|
|
978
1014
|
const sourceRefColMeta = this.meta.columns.find((c) => c.name === sourceRefCol);
|
|
979
1015
|
const targetPkField = targetMeta.reverseColumnMap[targetPkCol] ?? targetPkCol;
|
|
980
|
-
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.
|
|
1016
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
981
1017
|
const collectTargetPks = async (w) => {
|
|
982
1018
|
const rows = await targetQi.findMany({
|
|
983
1019
|
where: w,
|
|
@@ -1689,7 +1725,7 @@ export class PowqlInterface {
|
|
|
1689
1725
|
const rel = this.meta.relations[relName];
|
|
1690
1726
|
if (!rel)
|
|
1691
1727
|
throw new ValidationError(`Unknown relation "${relName}" on "${this.table}".`);
|
|
1692
|
-
if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args
|
|
1728
|
+
if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args)) {
|
|
1693
1729
|
if (this.capabilities.serverJoins) {
|
|
1694
1730
|
await this.loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii);
|
|
1695
1731
|
continue;
|
|
@@ -1712,7 +1748,19 @@ export class PowqlInterface {
|
|
|
1712
1748
|
throw new UnsupportedFeatureError('composite-key nested reads', 'PowDB', `relation "${relName}"`);
|
|
1713
1749
|
}
|
|
1714
1750
|
const options = (opt === true ? {} : opt);
|
|
1715
|
-
|
|
1751
|
+
// The relation's own page, applied PER PARENT at stitch time (see pageOf),
|
|
1752
|
+
// never on the flat fetch below.
|
|
1753
|
+
const relLimit = options.limit;
|
|
1754
|
+
const relOffset = options.offset;
|
|
1755
|
+
this.assertPagination(relLimit, relOffset, `relation "${relName}"`);
|
|
1756
|
+
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
1757
|
+
if (relLimit === 0) {
|
|
1758
|
+
// `[]` on every parent by construction: nothing to fetch.
|
|
1759
|
+
for (const parent of parents)
|
|
1760
|
+
parent[relName] = single ? null : [];
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
1716
1764
|
const targetMeta = this.schema.tables[rel.to];
|
|
1717
1765
|
// Local key on the parent, remote key on the target child.
|
|
1718
1766
|
const parentKeyCol = rel.type === 'belongsTo' ? fk[0] : rk[0];
|
|
@@ -1770,6 +1818,9 @@ export class PowqlInterface {
|
|
|
1770
1818
|
};
|
|
1771
1819
|
const children = (await targetQi.findMany({
|
|
1772
1820
|
...fetchOptions,
|
|
1821
|
+
// Unbounded: the relation limit/offset are per parent, applied below.
|
|
1822
|
+
limit: undefined,
|
|
1823
|
+
offset: undefined,
|
|
1773
1824
|
where: childWhere,
|
|
1774
1825
|
with: options.with,
|
|
1775
1826
|
timeout: options.timeout ?? timeout,
|
|
@@ -1796,11 +1847,12 @@ export class PowqlInterface {
|
|
|
1796
1847
|
delete child[childKeyField];
|
|
1797
1848
|
}
|
|
1798
1849
|
}
|
|
1799
|
-
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
1800
1850
|
for (const parent of parents) {
|
|
1801
1851
|
const k = this.joinKey(parent[parentKeyField]);
|
|
1802
1852
|
const matches = (k == null ? undefined : childByKey.get(k)) ?? [];
|
|
1803
|
-
parent[relName] = single
|
|
1853
|
+
parent[relName] = single
|
|
1854
|
+
? (matches[0] ?? null)
|
|
1855
|
+
: PowqlInterface.pageOf(matches, relLimit, relOffset);
|
|
1804
1856
|
}
|
|
1805
1857
|
}
|
|
1806
1858
|
}
|
|
@@ -1866,7 +1918,13 @@ export class PowqlInterface {
|
|
|
1866
1918
|
}
|
|
1867
1919
|
// (2) Target rows by PK, honouring the relation's own where/with/select/…
|
|
1868
1920
|
const options = (opt === true ? {} : opt);
|
|
1869
|
-
|
|
1921
|
+
this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
|
|
1922
|
+
if (options.limit === 0) {
|
|
1923
|
+
for (const parent of parents)
|
|
1924
|
+
parent[relName] = [];
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
1870
1928
|
// This loader stitches on the TARGET's own primary key, so the PK has to be
|
|
1871
1929
|
// in the fetch even when the caller's select/omit excludes it, and has to
|
|
1872
1930
|
// come back off afterwards. Exactly the shape `loadRelation` uses for its
|
|
@@ -1903,6 +1961,11 @@ export class PowqlInterface {
|
|
|
1903
1961
|
}
|
|
1904
1962
|
}
|
|
1905
1963
|
const targetByPk = new Map();
|
|
1964
|
+
// Position of each target in the fetch, i.e. its rank under the relation's
|
|
1965
|
+
// `orderBy`; the stitch below sorts a parent's children by it when an
|
|
1966
|
+
// orderBy was given, so `limit` keeps that parent's top-N and not its first
|
|
1967
|
+
// N junction rows. Across fetch CHUNKS the rank is fetch order.
|
|
1968
|
+
const targetRank = new Map();
|
|
1906
1969
|
const targetValList = [...allTargetVals].map((v) => targetPkColMeta ? coerceScalar(v, targetPkColMeta.tsType) : v);
|
|
1907
1970
|
const targetChunk = this.keyChunkSize(targetMeta, targetPkCol);
|
|
1908
1971
|
for (let i = 0; i < targetValList.length; i += targetChunk) {
|
|
@@ -1913,14 +1976,21 @@ export class PowqlInterface {
|
|
|
1913
1976
|
};
|
|
1914
1977
|
const targets = (await targetQi.findMany({
|
|
1915
1978
|
...fetchOptions,
|
|
1979
|
+
// Unbounded: the relation limit/offset are per parent, applied in the stitch.
|
|
1980
|
+
limit: undefined,
|
|
1981
|
+
offset: undefined,
|
|
1916
1982
|
where,
|
|
1917
1983
|
with: options.with,
|
|
1918
1984
|
timeout: options.timeout ?? timeout,
|
|
1919
1985
|
// Public findMany, so the sentinel form. See loadRelation above.
|
|
1920
1986
|
includePii: includePii ? UNSAFE : undefined,
|
|
1921
1987
|
}));
|
|
1922
|
-
for (const t of targets)
|
|
1923
|
-
|
|
1988
|
+
for (const t of targets) {
|
|
1989
|
+
const pk = String(t[targetPkField]);
|
|
1990
|
+
targetByPk.set(pk, t);
|
|
1991
|
+
if (!targetRank.has(pk))
|
|
1992
|
+
targetRank.set(pk, targetRank.size);
|
|
1993
|
+
}
|
|
1924
1994
|
}
|
|
1925
1995
|
// (3) Stitch: each parent → its junction targets (m2m is always a list).
|
|
1926
1996
|
for (const parent of parents) {
|
|
@@ -1932,7 +2002,11 @@ export class PowqlInterface {
|
|
|
1932
2002
|
if (child)
|
|
1933
2003
|
children.push(child);
|
|
1934
2004
|
}
|
|
1935
|
-
|
|
2005
|
+
if (options.orderBy) {
|
|
2006
|
+
const rank = (t) => targetRank.get(String(t[targetPkField])) ?? 0;
|
|
2007
|
+
children.sort((a, b) => rank(a) - rank(b));
|
|
2008
|
+
}
|
|
2009
|
+
parent[relName] = PowqlInterface.pageOf(children, options.limit, options.offset);
|
|
1936
2010
|
}
|
|
1937
2011
|
// Stitching is done: take the forced PK back off. Iterating the map rather
|
|
1938
2012
|
// than the stitched lists is deliberate, one target can be linked from many
|
|
@@ -1976,11 +2050,12 @@ export class PowqlInterface {
|
|
|
1976
2050
|
* `referenceKey` on the TARGET table (the join's non-fetched side);
|
|
1977
2051
|
* - m2m keeps any `orderBy`/`limit`/`offset` on the loader (the junction-order
|
|
1978
2052
|
* stitch can't be reproduced by the 3-table join deterministically);
|
|
1979
|
-
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader
|
|
1980
|
-
*
|
|
1981
|
-
*
|
|
2053
|
+
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader. A
|
|
2054
|
+
* to-many relation `limit`/`offset` is a PER-PARENT bound on both paths,
|
|
2055
|
+
* fetched unbounded and sliced per parent at stitch time (`pageOf`), so
|
|
2056
|
+
* the join statement carries neither clause.
|
|
1982
2057
|
*/
|
|
1983
|
-
joinEligible(rel, opt, args
|
|
2058
|
+
joinEligible(rel, opt, args) {
|
|
1984
2059
|
const effLimit = args.limit ?? this.defaultLimit;
|
|
1985
2060
|
if (effLimit !== undefined || args.offset)
|
|
1986
2061
|
return false;
|
|
@@ -2021,9 +2096,8 @@ export class PowqlInterface {
|
|
|
2021
2096
|
return false;
|
|
2022
2097
|
}
|
|
2023
2098
|
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
2024
|
-
if ((options.limit !== undefined || options.offset) &&
|
|
2099
|
+
if ((options.limit !== undefined || options.offset) && single)
|
|
2025
2100
|
return false;
|
|
2026
|
-
}
|
|
2027
2101
|
// A `limit 0` relation stays off the join statement for the same reason it
|
|
2028
2102
|
// stays off a nested projection: PowDB answered `limit 0` with one row below
|
|
2029
2103
|
// engine 0.20. The loader resolves it client-side, correctly on every version.
|
|
@@ -2070,13 +2144,13 @@ export class PowqlInterface {
|
|
|
2070
2144
|
const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
|
|
2071
2145
|
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout);
|
|
2072
2146
|
const order = targetQi.buildOrder(options.orderBy, params, 'c');
|
|
2147
|
+
// No limit/offset clause: on the join they would bound the TOTAL child
|
|
2148
|
+
// count across every parent. The relation's page is per parent (pageOf).
|
|
2073
2149
|
this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
|
|
2074
|
-
const limitClause = options.limit !== undefined ? ` limit ${this.param(options.limit, params)}` : '';
|
|
2075
|
-
const offsetClause = options.offset ? ` offset ${this.param(options.offset, params)}` : '';
|
|
2076
2150
|
const proj = this.joinProjection(childCols, `p.${quotePowqlIdent(parentKeyCol)}`, 'c');
|
|
2077
2151
|
const powql = `${targetQi.qt} as c join ${this.qt} as p ` +
|
|
2078
2152
|
`on c.${quotePowqlIdent(childKeyCol)} = p.${quotePowqlIdent(parentKeyCol)}` +
|
|
2079
|
-
`${filter}${order}
|
|
2153
|
+
`${filter}${order} ${proj}`;
|
|
2080
2154
|
// A READ: thread a read-shaped action through the exec seam.
|
|
2081
2155
|
const { rows, native } = await targetQi.exec(powql, params, timeout, 'findMany');
|
|
2082
2156
|
const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
@@ -2084,7 +2158,9 @@ export class PowqlInterface {
|
|
|
2084
2158
|
for (const p of parents) {
|
|
2085
2159
|
const key = this.joinKey(p[parentKeyField]);
|
|
2086
2160
|
const matches = (key == null ? undefined : byKey.get(key)) ?? [];
|
|
2087
|
-
p[relName] = single
|
|
2161
|
+
p[relName] = single
|
|
2162
|
+
? (matches[0] ?? null)
|
|
2163
|
+
: PowqlInterface.pageOf(matches, options.limit, options.offset);
|
|
2088
2164
|
}
|
|
2089
2165
|
}
|
|
2090
2166
|
/**
|
package/dist/prisma-compat.d.ts
CHANGED
|
@@ -174,7 +174,7 @@ export interface CompatTransactionClient {
|
|
|
174
174
|
* breaks atomicity. Optional only so a test stub can omit it: when it is
|
|
175
175
|
* absent the raw methods throw instead of escaping the transaction.
|
|
176
176
|
*/
|
|
177
|
-
rawQuery?(text: string, params?: readonly unknown[]): Promise<{
|
|
177
|
+
rawQuery?(text: string, params?: readonly unknown[], action?: string): Promise<{
|
|
178
178
|
rows: unknown[];
|
|
179
179
|
rowCount: number | null;
|
|
180
180
|
}>;
|
package/dist/prisma-compat.js
CHANGED
|
@@ -2067,17 +2067,17 @@ function makeRawSurface(exec, ph) {
|
|
|
2067
2067
|
return {
|
|
2068
2068
|
$queryRaw: async (strings, ...values) => {
|
|
2069
2069
|
const { text, params } = flattenTemplate(strings, values, ph);
|
|
2070
|
-
return (await exec(text, params)).rows;
|
|
2070
|
+
return (await exec(text, params, '$queryRaw')).rows;
|
|
2071
2071
|
},
|
|
2072
2072
|
$queryRawUnsafe: async (sql, ...params) => {
|
|
2073
|
-
return (await exec(sql, params)).rows;
|
|
2073
|
+
return (await exec(sql, params, '$queryRawUnsafe')).rows;
|
|
2074
2074
|
},
|
|
2075
2075
|
$executeRaw: async (strings, ...values) => {
|
|
2076
2076
|
const { text, params } = flattenTemplate(strings, values, ph);
|
|
2077
|
-
return (await exec(text, params)).rowCount ?? 0;
|
|
2077
|
+
return (await exec(text, params, '$executeRaw')).rowCount ?? 0;
|
|
2078
2078
|
},
|
|
2079
2079
|
$executeRawUnsafe: async (sql, ...params) => {
|
|
2080
|
-
return (await exec(sql, params)).rowCount ?? 0;
|
|
2080
|
+
return (await exec(sql, params, '$executeRawUnsafe')).rowCount ?? 0;
|
|
2081
2081
|
},
|
|
2082
2082
|
};
|
|
2083
2083
|
}
|
|
@@ -2334,8 +2334,12 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
2334
2334
|
baseDelegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table), (fn) => db.$transaction((tx) => fn((n) => tx.table(n)))));
|
|
2335
2335
|
}
|
|
2336
2336
|
const ph = placeholderOf(db);
|
|
2337
|
-
|
|
2337
|
+
// Prefer the client's own raw seam, which emits a `$on('query')` event; a
|
|
2338
|
+
// client without one (an older core, a test stub) runs on the pool as before.
|
|
2339
|
+
const runRaw = async (text, params, action) => {
|
|
2338
2340
|
try {
|
|
2341
|
+
if (typeof db.rawQuery === 'function')
|
|
2342
|
+
return await db.rawQuery(text, params, action);
|
|
2339
2343
|
return await poolOf(db).query(text, params);
|
|
2340
2344
|
}
|
|
2341
2345
|
catch (err) {
|
|
@@ -2350,14 +2354,14 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
2350
2354
|
* `wrapPgError` is idempotent (it returns an already-typed TurbineError
|
|
2351
2355
|
* untouched), so the error shape matches the pool path exactly.
|
|
2352
2356
|
*/
|
|
2353
|
-
const txRunRaw = (tx) => async (text, params) => {
|
|
2357
|
+
const txRunRaw = (tx) => async (text, params, action) => {
|
|
2354
2358
|
if (typeof tx.rawQuery !== 'function') {
|
|
2355
2359
|
throw decorate(new ValidationError('prisma-compat: raw SQL inside $transaction needs a transaction client that can execute it ' +
|
|
2356
2360
|
'(core TransactionClient.rawQuery). Refusing to run the statement on a pool connection, which would ' +
|
|
2357
2361
|
'silently place it outside the transaction.'), ctx.options.prismaErrorCodes);
|
|
2358
2362
|
}
|
|
2359
2363
|
try {
|
|
2360
|
-
return await tx.rawQuery(text, params);
|
|
2364
|
+
return await tx.rawQuery(text, params, action);
|
|
2361
2365
|
}
|
|
2362
2366
|
catch (err) {
|
|
2363
2367
|
throw decorate(wrapPgError(err), ctx.options.prismaErrorCodes);
|
package/dist/query/deferred.d.ts
CHANGED
|
@@ -93,6 +93,21 @@ export interface QueryEvent {
|
|
|
93
93
|
* observability can see exactly which queries the auto default re-planned.
|
|
94
94
|
*/
|
|
95
95
|
strategy?: 'auto-batched';
|
|
96
|
+
/**
|
|
97
|
+
* The label of the innermost `db.$tag(label, fn)` scope the query ran in.
|
|
98
|
+
* Absent outside any scope. Lets a listener attribute a query to the feature
|
|
99
|
+
* or code path that issued it, which `model` + `action` cannot.
|
|
100
|
+
*/
|
|
101
|
+
tag?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Set on statements that ran as part of a batch rather than on their own:
|
|
104
|
+
* `'pipeline'` for `db.pipeline(...)`, `'transaction'` for the array form
|
|
105
|
+
* `$transaction([...])`. A pipeline sends every statement in one round trip,
|
|
106
|
+
* so its per-statement `duration` is the batch's wall time divided evenly
|
|
107
|
+
* across the statements (the sum is exact, the split is not). Transaction
|
|
108
|
+
* batch statements are timed individually. Absent for everything else.
|
|
109
|
+
*/
|
|
110
|
+
batch?: 'pipeline' | 'transaction';
|
|
96
111
|
}
|
|
97
112
|
export type QueryEventListener = (event: QueryEvent) => void;
|
|
98
113
|
/** Options passed from TurbineClient to QueryInterface */
|