turbine-orm 0.78.0 → 0.79.0-next.c53fa34

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.
@@ -648,9 +648,11 @@ export declare class TransactionClient {
648
648
  * inside the same BEGIN/COMMIT (and any active SAVEPOINT) as every other
649
649
  * statement in the callback. Driver errors are translated by `wrapPgError`,
650
650
  * exactly as pool-scoped and table-scoped queries are. Like {@link raw}, it
651
- * emits no `$on('query')` event and runs no middleware.
651
+ * runs no middleware, and it emits one `$on('query')` event with model
652
+ * `'$raw'` and `action` set to the optional `action` label (the adapter
653
+ * passes `'$queryRaw'` / `'$executeRaw'` ...; default `'rawQuery'`).
652
654
  */
653
- rawQuery<T extends object = Record<string, unknown>>(text: string, params?: readonly unknown[]): Promise<{
655
+ rawQuery<T extends object = Record<string, unknown>>(text: string, params?: readonly unknown[], action?: string): Promise<{
654
656
  rows: T[];
655
657
  rowCount: number | null;
656
658
  }>;
@@ -986,6 +988,16 @@ export declare class TurbineClient {
986
988
  * On HTTP-based drivers it falls back to sequential execution.
987
989
  */
988
990
  pipeline<T extends readonly DeferredQuery<unknown>[]>(...args: T | [T, PipelineOptions?]): Promise<PipelineResults<T>>;
991
+ /**
992
+ * {@link pipeline} with one `$on('query')` event per statement. Only taken
993
+ * when a listener is registered, so an unobserved pipeline runs exactly as
994
+ * before. Each statement's `transform` is wrapped to capture the driver's
995
+ * row count on the way through (the pipeline returns transformed values, not
996
+ * driver results). The statements share one round trip, so the batch's wall
997
+ * time is split evenly across them and each event carries
998
+ * `batch: 'pipeline'`.
999
+ */
1000
+ private reportedPipeline;
989
1001
  /**
990
1002
  * Check whether the underlying pool supports the real pipeline protocol.
991
1003
  * Returns `true` for standard pg.Pool TCP connections, `false` for HTTP
@@ -1005,6 +1017,39 @@ export declare class TurbineClient {
1005
1017
  * ```
1006
1018
  */
1007
1019
  raw<T extends Record<string, unknown> = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
1020
+ /**
1021
+ * @internal The `turbine-orm/prisma-compat` adapter's pool-level raw seam,
1022
+ * the twin of {@link TransactionClient.rawQuery}. NOT application API: use
1023
+ * {@link raw} or {@link sql}, whose tagged templates make concatenating a
1024
+ * value into the SQL text impossible. It exists so the adapter's
1025
+ * `$queryRaw` / `$executeRaw` statements emit a `$on('query')` event (model
1026
+ * `'$raw'`, `action` the adapter's label) instead of running unobserved on
1027
+ * the pool. Errors are translated by `wrapPgError`, like {@link raw}.
1028
+ */
1029
+ rawQuery<T extends object = Record<string, unknown>>(text: string, params?: readonly unknown[], action?: string): Promise<{
1030
+ rows: T[];
1031
+ rowCount: number | null;
1032
+ }>;
1033
+ /**
1034
+ * Run `fn` with `tag` attached to every `$on('query')` event it causes, so a
1035
+ * listener can attribute queries to the feature or code path that issued
1036
+ * them. The scope follows async context: queries in awaited helpers,
1037
+ * transactions, pipelines and raw SQL inside `fn` are all tagged. An inner
1038
+ * `$tag` replaces an outer one for its own duration. The tag is event
1039
+ * metadata only and never reaches SQL.
1040
+ *
1041
+ * Throws {@link ValidationError} (E003) for an empty tag or one longer than
1042
+ * {@link MAX_QUERY_TAG_LENGTH} (128) characters.
1043
+ *
1044
+ * @example
1045
+ * ```ts
1046
+ * const order = await db.$tag('checkout', async () => {
1047
+ * const cart = await db.carts.findUnique({ where: { id } });
1048
+ * return db.orders.create({ data: { cartId: cart.id } });
1049
+ * });
1050
+ * ```
1051
+ */
1052
+ $tag<R>(tag: string, fn: () => R): R;
1008
1053
  /**
1009
1054
  * Execute a **typed** raw SQL query, Turbine's answer to Prisma's TypedSQL.
1010
1055
  *
@@ -37,6 +37,7 @@ const pipeline_js_1 = require("./pipeline.js");
37
37
  const index_js_1 = require("./query/index.js");
38
38
  const utils_js_1 = require("./query/utils.js");
39
39
  const warn_registry_js_1 = require("./query/warn-registry.js");
40
+ const query_events_js_1 = require("./query-events.js");
40
41
  const realtime_js_1 = require("./realtime.js");
41
42
  const typed_sql_js_1 = require("./typed-sql.js");
42
43
  async function withRetry(fn, options) {
@@ -538,13 +539,8 @@ class TransactionClient {
538
539
  sql += this.dialect.paramPlaceholder(i + 1);
539
540
  }
540
541
  });
541
- try {
542
- const result = await this.client.query(sql, values);
543
- return result.rows;
544
- }
545
- catch (err) {
546
- throw (0, errors_js_1.wrapPgError)(err);
547
- }
542
+ const result = await (0, query_events_js_1.runReportedStatement)(this.queryOptions?._onQuery, (0, query_events_js_1.rawIdentity)('raw'), sql, values, () => this.client.query(sql, values), errors_js_1.wrapPgError);
543
+ return result.rows;
548
544
  }
549
545
  /**
550
546
  * @internal The `turbine-orm/prisma-compat` adapter's transaction seam. NOT
@@ -568,16 +564,13 @@ class TransactionClient {
568
564
  * inside the same BEGIN/COMMIT (and any active SAVEPOINT) as every other
569
565
  * statement in the callback. Driver errors are translated by `wrapPgError`,
570
566
  * exactly as pool-scoped and table-scoped queries are. Like {@link raw}, it
571
- * emits no `$on('query')` event and runs no middleware.
567
+ * runs no middleware, and it emits one `$on('query')` event with model
568
+ * `'$raw'` and `action` set to the optional `action` label (the adapter
569
+ * passes `'$queryRaw'` / `'$executeRaw'` ...; default `'rawQuery'`).
572
570
  */
573
- async rawQuery(text, params = []) {
574
- try {
575
- const result = await this.client.query(text, params);
576
- return { rows: result.rows, rowCount: result.rowCount };
577
- }
578
- catch (err) {
579
- throw (0, errors_js_1.wrapPgError)(err);
580
- }
571
+ async rawQuery(text, params = [], action = 'rawQuery') {
572
+ const result = await (0, query_events_js_1.runReportedStatement)(this.queryOptions?._onQuery, (0, query_events_js_1.rawIdentity)(action), text, params, () => this.client.query(text, params), errors_js_1.wrapPgError);
573
+ return { rows: result.rows, rowCount: result.rowCount };
581
574
  }
582
575
  /**
583
576
  * Create a pool-like wrapper around the transaction client.
@@ -908,7 +901,11 @@ class TurbineClient {
908
901
  _onQuery: (event) => {
909
902
  if (this.queryListeners.size === 0)
910
903
  return;
911
- const emitted = this.queryParamsVisible ? event : { ...event, params: event.params.map(() => '[REDACTED]') };
904
+ // The $tag() scope is read here, at the one seam every emitter shares,
905
+ // so a tag reaches model, raw, pipeline and batch events alike.
906
+ const tag = event.tag ?? (0, query_events_js_1.currentQueryTag)();
907
+ const tagged = tag === undefined || event.tag !== undefined ? event : { ...event, tag };
908
+ const emitted = this.queryParamsVisible ? tagged : { ...tagged, params: tagged.params.map(() => '[REDACTED]') };
912
909
  for (const listener of this.queryListeners) {
913
910
  try {
914
911
  listener(emitted);
@@ -1520,7 +1517,63 @@ class TurbineClient {
1520
1517
  // where VALUES as soon as any verbose client existed in the process. The
1521
1518
  // scope is established around the await, so every continuation of the batch
1522
1519
  // resolves this client's mode.
1523
- return this.withErrorMode(() => (0, pipeline_js_1.executePipeline)(this.pool, queries, options));
1520
+ const sink = this.queryListeners.size > 0 ? this.queryOptions._onQuery : undefined;
1521
+ if (!sink)
1522
+ return this.withErrorMode(() => (0, pipeline_js_1.executePipeline)(this.pool, queries, options));
1523
+ return this.withErrorMode(() => this.reportedPipeline(sink, queries, options));
1524
+ }
1525
+ /**
1526
+ * {@link pipeline} with one `$on('query')` event per statement. Only taken
1527
+ * when a listener is registered, so an unobserved pipeline runs exactly as
1528
+ * before. Each statement's `transform` is wrapped to capture the driver's
1529
+ * row count on the way through (the pipeline returns transformed values, not
1530
+ * driver results). The statements share one round trip, so the batch's wall
1531
+ * time is split evenly across them and each event carries
1532
+ * `batch: 'pipeline'`.
1533
+ */
1534
+ async reportedPipeline(sink, queries, options) {
1535
+ const rowCounts = new Array(queries.length).fill(undefined);
1536
+ const observed = queries.map((q, i) => ({
1537
+ ...q,
1538
+ transform: (raw) => {
1539
+ rowCounts[i] = (0, query_events_js_1.resultRowCount)(raw);
1540
+ return q.transform(raw);
1541
+ },
1542
+ }));
1543
+ const start = performance.now();
1544
+ let failure;
1545
+ try {
1546
+ return await (0, pipeline_js_1.executePipeline)(this.pool, observed, options);
1547
+ }
1548
+ catch (err) {
1549
+ failure = err;
1550
+ throw err;
1551
+ }
1552
+ finally {
1553
+ const duration = (performance.now() - start) / queries.length;
1554
+ // PipelineError names the failing slot; any other failure (connect,
1555
+ // BEGIN) is charged to the first statement that produced no result.
1556
+ const failedIndex = failure === undefined
1557
+ ? -1
1558
+ : failure instanceof errors_js_1.PipelineError && failure.failedIndex !== undefined
1559
+ ? failure.failedIndex
1560
+ : rowCounts.indexOf(undefined);
1561
+ queries.forEach((q, i) => {
1562
+ const failed = i === failedIndex;
1563
+ // A slot that never ran (or rolled back unseen) is not reported.
1564
+ if (failed || rowCounts[i] !== undefined) {
1565
+ (0, query_events_js_1.emitStatementEvent)(sink, {
1566
+ sql: q.sql,
1567
+ params: q.params,
1568
+ duration,
1569
+ ...(0, query_events_js_1.deferredIdentity)(q.tag),
1570
+ rows: rowCounts[i] ?? 0,
1571
+ batch: 'pipeline',
1572
+ ...(failed ? { error: failure instanceof Error ? failure : new Error(String(failure)) } : {}),
1573
+ });
1574
+ }
1575
+ });
1576
+ }
1524
1577
  }
1525
1578
  /**
1526
1579
  * Check whether the underlying pool supports the real pipeline protocol.
@@ -1556,13 +1609,43 @@ class TurbineClient {
1556
1609
  if (this.logging) {
1557
1610
  console.log(`[turbine] Raw SQL: ${sql.trim().substring(0, 120)}...`);
1558
1611
  }
1559
- try {
1560
- const result = await this.pool.query(sql, values);
1561
- return result.rows;
1562
- }
1563
- catch (err) {
1564
- throw this.withErrorMode(() => (0, errors_js_1.wrapPgError)(err));
1565
- }
1612
+ const result = await (0, query_events_js_1.runReportedStatement)(this.queryOptions._onQuery, (0, query_events_js_1.rawIdentity)('raw'), sql, values, () => this.pool.query(sql, values), (err) => this.withErrorMode(() => (0, errors_js_1.wrapPgError)(err)));
1613
+ return result.rows;
1614
+ }
1615
+ /**
1616
+ * @internal The `turbine-orm/prisma-compat` adapter's pool-level raw seam,
1617
+ * the twin of {@link TransactionClient.rawQuery}. NOT application API: use
1618
+ * {@link raw} or {@link sql}, whose tagged templates make concatenating a
1619
+ * value into the SQL text impossible. It exists so the adapter's
1620
+ * `$queryRaw` / `$executeRaw` statements emit a `$on('query')` event (model
1621
+ * `'$raw'`, `action` the adapter's label) instead of running unobserved on
1622
+ * the pool. Errors are translated by `wrapPgError`, like {@link raw}.
1623
+ */
1624
+ async rawQuery(text, params = [], action = 'rawQuery') {
1625
+ const result = await (0, query_events_js_1.runReportedStatement)(this.queryOptions._onQuery, (0, query_events_js_1.rawIdentity)(action), text, params, () => this.pool.query(text, params), (err) => this.withErrorMode(() => (0, errors_js_1.wrapPgError)(err)));
1626
+ return { rows: result.rows, rowCount: result.rowCount };
1627
+ }
1628
+ /**
1629
+ * Run `fn` with `tag` attached to every `$on('query')` event it causes, so a
1630
+ * listener can attribute queries to the feature or code path that issued
1631
+ * them. The scope follows async context: queries in awaited helpers,
1632
+ * transactions, pipelines and raw SQL inside `fn` are all tagged. An inner
1633
+ * `$tag` replaces an outer one for its own duration. The tag is event
1634
+ * metadata only and never reaches SQL.
1635
+ *
1636
+ * Throws {@link ValidationError} (E003) for an empty tag or one longer than
1637
+ * {@link MAX_QUERY_TAG_LENGTH} (128) characters.
1638
+ *
1639
+ * @example
1640
+ * ```ts
1641
+ * const order = await db.$tag('checkout', async () => {
1642
+ * const cart = await db.carts.findUnique({ where: { id } });
1643
+ * return db.orders.create({ data: { cartId: cart.id } });
1644
+ * });
1645
+ * ```
1646
+ */
1647
+ $tag(tag, fn) {
1648
+ return (0, query_events_js_1.runWithQueryTag)(tag, fn);
1566
1649
  }
1567
1650
  /**
1568
1651
  * Execute a **typed** raw SQL query, Turbine's answer to Prisma's TypedSQL.
@@ -1602,7 +1685,7 @@ class TurbineClient {
1602
1685
  // `withErrorMode` docstring already claimed to cover the typed-SQL builder
1603
1686
  // and did not, which left two raw-SQL entry points disagreeing about the
1604
1687
  // same statement, since the adjacent `raw` tag WAS scoped.
1605
- return new typed_sql_js_1.TypedSqlQuery(this.pool, sql, params, this.logging, (fn) => this.withErrorMode(fn));
1688
+ return new typed_sql_js_1.TypedSqlQuery(this.pool, sql, params, this.logging, (fn) => this.withErrorMode(fn), this.queryOptions._onQuery);
1606
1689
  }
1607
1690
  // -------------------------------------------------------------------------
1608
1691
  // Transaction support (raw, legacy)
@@ -1842,13 +1925,18 @@ class TurbineClient {
1842
1925
  if (queries.length === 0) {
1843
1926
  return [];
1844
1927
  }
1928
+ // One `$on('query')` event per statement, `batch: 'transaction'`, only
1929
+ // when someone is listening; each statement is timed on its own.
1930
+ const sink = this.queryListeners.size > 0 ? this.queryOptions._onQuery : undefined;
1931
+ const reported = (dq, run) => (0, query_events_js_1.runReportedStatement)(sink, { ...(0, query_events_js_1.deferredIdentity)(dq.tag), batch: 'transaction' }, dq.sql, dq.params, run, errors_js_1.wrapPgError);
1845
1932
  return this.transaction(async (client) => {
1846
1933
  const pipelined = client.supportsPipelining === true &&
1847
1934
  this.dialect.resultStrategy !== 'reselect';
1848
1935
  if (pipelined) {
1849
1936
  // Dispatch every statement before awaiting any reply. The driver's
1850
- // FIFO guarantee makes settled[i] the reply to queries[i].
1851
- const settled = await Promise.allSettled(queries.map((dq) => client.query(dq.sql, dq.params)));
1937
+ // FIFO guarantee makes settled[i] the reply to queries[i]. Each
1938
+ // statement is timed from its dispatch to its own reply.
1939
+ const settled = await Promise.allSettled(queries.map((dq) => reported(dq, () => client.query(dq.sql, dq.params))));
1852
1940
  const results = [];
1853
1941
  for (let i = 0; i < settled.length; i++) {
1854
1942
  const outcome = settled[i];
@@ -1861,19 +1949,12 @@ class TurbineClient {
1861
1949
  }
1862
1950
  const results = [];
1863
1951
  for (const dq of queries) {
1864
- let raw;
1865
- try {
1866
- // Non-RETURNING engines (resultStrategy 'reselect', e.g. MySQL)
1867
- // attach a reselect plan that runs the write plus a follow-up SELECT;
1868
- // running dq.sql alone would transform a row-less write result.
1869
- raw =
1870
- this.dialect.resultStrategy === 'reselect' && dq.reselect
1871
- ? await dq.reselect((sql, params) => client.query(sql, params))
1872
- : await client.query(dq.sql, dq.params);
1873
- }
1874
- catch (err) {
1875
- throw (0, errors_js_1.wrapPgError)(err);
1876
- }
1952
+ // Non-RETURNING engines (resultStrategy 'reselect', e.g. MySQL) attach a
1953
+ // reselect plan that runs the write plus a follow-up SELECT; running
1954
+ // dq.sql alone would transform a row-less write result.
1955
+ const raw = await reported(dq, () => this.dialect.resultStrategy === 'reselect' && dq.reselect
1956
+ ? dq.reselect((sql, params) => client.query(sql, params))
1957
+ : client.query(dq.sql, dq.params));
1877
1958
  results.push(dq.transform(raw));
1878
1959
  }
1879
1960
  return results;
@@ -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/cjs/index.js CHANGED
@@ -35,7 +35,7 @@
35
35
  */
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.fingerprintPrismaSchema = exports.pipelineSupported = exports.executePipeline = exports.PgMetricsSink = exports.HttpJsonSink = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.ReadOnlyError = exports.REDACTED_DETAIL = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
38
- exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.DestructivePushRefusal = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.withDbFieldNames = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.UNSAFE = void 0;
38
+ exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.DestructivePushRefusal = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.applyManyToManyRelations = exports.withDbFieldNames = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.RAW_QUERY_MODEL = exports.MAX_QUERY_TAG_LENGTH = exports.UNSAFE = void 0;
39
39
  var index_js_1 = require("./adapters/index.js");
40
40
  Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
41
41
  Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
@@ -110,6 +110,10 @@ Object.defineProperty(exports, "AUTO_TO_ONE_JOIN_ROWS_MAX", { enumerable: true,
110
110
  Object.defineProperty(exports, "AUTO_TO_ONE_JOIN_ROWS_MIN", { enumerable: true, get: function () { return index_js_2.AUTO_TO_ONE_JOIN_ROWS_MIN; } });
111
111
  Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return index_js_2.QueryInterface; } });
112
112
  Object.defineProperty(exports, "UNSAFE", { enumerable: true, get: function () { return index_js_2.UNSAFE; } });
113
+ // $on('query') event metadata: raw-statement model name, $tag() label limit
114
+ var query_events_js_1 = require("./query-events.js");
115
+ Object.defineProperty(exports, "MAX_QUERY_TAG_LENGTH", { enumerable: true, get: function () { return query_events_js_1.MAX_QUERY_TAG_LENGTH; } });
116
+ Object.defineProperty(exports, "RAW_QUERY_MODEL", { enumerable: true, get: function () { return query_events_js_1.RAW_QUERY_MODEL; } });
113
117
  // Realtime, LISTEN/NOTIFY pub/sub
114
118
  var realtime_js_1 = require("./realtime.js");
115
119
  Object.defineProperty(exports, "validateChannel", { enumerable: true, get: function () { return realtime_js_1.validateChannel; } });
@@ -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, as
627
- * does a to-many relation `limit`/`offset` when the parent set spills past
628
- * one loader chunk (the loader limits per chunk, the join once globally).
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/cjs/powql.js CHANGED
@@ -393,6 +393,42 @@ class PowqlInterface {
393
393
  effectiveLimit(args) {
394
394
  return args.limit ?? this.defaultLimit;
395
395
  }
396
+ /**
397
+ * Options for the child-side interfaces the relation loaders and the
398
+ * relation-filter resolvers build: the client's, with `defaultLimit` cleared
399
+ * and the unlimited warning off. `defaultLimit` bounds the caller's PAGE; on
400
+ * an internal fetch that serves that page (a relation's children, a filter's
401
+ * key set) it would cap the whole page's children, or the key set, at the
402
+ * page size: a silent wrong answer by another route than the one
403
+ * {@link pageOf} closes. The SQL batched loader clears it the same way
404
+ * (`batchedChildOptions` in query/builder.ts).
405
+ */
406
+ childOptions;
407
+ loaderChildOptions() {
408
+ this.childOptions ??= { ...this.options, defaultLimit: undefined, warnOnUnlimited: false };
409
+ return this.childOptions;
410
+ }
411
+ /**
412
+ * ONE parent's window of its stitched children: the relation's `offset` and
413
+ * `limit` applied to that parent's own list.
414
+ *
415
+ * A relation `limit` means "at most N on EACH parent". The loaders used to
416
+ * spread it onto the flat child fetch (`post filter .author_id in (...)
417
+ * limit 5`), which caps the TOTAL across every parent in the chunk, so ten
418
+ * parents shared five posts and most got none, with no error. It shipped
419
+ * for the loaders' whole life because the one test of the shape compared
420
+ * the join path against the loader path and both were wrong the same way;
421
+ * the cross-engine benchmark found it, where the wrong answer was the fast
422
+ * one. Every loader now fetches its children unbounded, WITH the relation
423
+ * `orderBy` (which decides which rows the window keeps), and applies this
424
+ * at stitch time: the rule the SQL engines' batched loader has always used.
425
+ */
426
+ static pageOf(rows, limit, offset) {
427
+ if (limit === undefined && !offset)
428
+ return rows;
429
+ const start = offset ?? 0;
430
+ return rows.slice(start, limit === undefined ? undefined : start + limit);
431
+ }
396
432
  /**
397
433
  * Reject a negative `limit` / `offset` before it reaches the engine. PowDB
398
434
  * casts both with `as usize` at execution, so below engine 0.20 a negative
@@ -974,7 +1010,7 @@ class PowqlInterface {
974
1010
  const childCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
975
1011
  const localField = this.meta.reverseColumnMap[localCol] ?? localCol;
976
1012
  const childField = targetMeta.reverseColumnMap[childCol] ?? childCol;
977
- const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
1013
+ const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
978
1014
  const collect = async (w) => {
979
1015
  const rows = await targetQi.findMany({
980
1016
  where: w,
@@ -1013,7 +1049,7 @@ class PowqlInterface {
1013
1049
  const sourceRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
1014
1050
  const sourceRefColMeta = this.meta.columns.find((c) => c.name === sourceRefCol);
1015
1051
  const targetPkField = targetMeta.reverseColumnMap[targetPkCol] ?? targetPkCol;
1016
- const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
1052
+ const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
1017
1053
  const collectTargetPks = async (w) => {
1018
1054
  const rows = await targetQi.findMany({
1019
1055
  where: w,
@@ -1725,7 +1761,7 @@ class PowqlInterface {
1725
1761
  const rel = this.meta.relations[relName];
1726
1762
  if (!rel)
1727
1763
  throw new errors_js_1.ValidationError(`Unknown relation "${relName}" on "${this.table}".`);
1728
- if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args, parents.length)) {
1764
+ if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args)) {
1729
1765
  if (this.capabilities.serverJoins) {
1730
1766
  await this.loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii);
1731
1767
  continue;
@@ -1748,7 +1784,19 @@ class PowqlInterface {
1748
1784
  throw new errors_js_1.UnsupportedFeatureError('composite-key nested reads', 'PowDB', `relation "${relName}"`);
1749
1785
  }
1750
1786
  const options = (opt === true ? {} : opt);
1751
- const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
1787
+ // The relation's own page, applied PER PARENT at stitch time (see pageOf),
1788
+ // never on the flat fetch below.
1789
+ const relLimit = options.limit;
1790
+ const relOffset = options.offset;
1791
+ this.assertPagination(relLimit, relOffset, `relation "${relName}"`);
1792
+ const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
1793
+ if (relLimit === 0) {
1794
+ // `[]` on every parent by construction: nothing to fetch.
1795
+ for (const parent of parents)
1796
+ parent[relName] = single ? null : [];
1797
+ continue;
1798
+ }
1799
+ const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
1752
1800
  const targetMeta = this.schema.tables[rel.to];
1753
1801
  // Local key on the parent, remote key on the target child.
1754
1802
  const parentKeyCol = rel.type === 'belongsTo' ? fk[0] : rk[0];
@@ -1806,6 +1854,9 @@ class PowqlInterface {
1806
1854
  };
1807
1855
  const children = (await targetQi.findMany({
1808
1856
  ...fetchOptions,
1857
+ // Unbounded: the relation limit/offset are per parent, applied below.
1858
+ limit: undefined,
1859
+ offset: undefined,
1809
1860
  where: childWhere,
1810
1861
  with: options.with,
1811
1862
  timeout: options.timeout ?? timeout,
@@ -1832,11 +1883,12 @@ class PowqlInterface {
1832
1883
  delete child[childKeyField];
1833
1884
  }
1834
1885
  }
1835
- const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
1836
1886
  for (const parent of parents) {
1837
1887
  const k = this.joinKey(parent[parentKeyField]);
1838
1888
  const matches = (k == null ? undefined : childByKey.get(k)) ?? [];
1839
- parent[relName] = single ? (matches[0] ?? null) : matches;
1889
+ parent[relName] = single
1890
+ ? (matches[0] ?? null)
1891
+ : PowqlInterface.pageOf(matches, relLimit, relOffset);
1840
1892
  }
1841
1893
  }
1842
1894
  }
@@ -1902,7 +1954,13 @@ class PowqlInterface {
1902
1954
  }
1903
1955
  // (2) Target rows by PK, honouring the relation's own where/with/select/…
1904
1956
  const options = (opt === true ? {} : opt);
1905
- const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
1957
+ this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
1958
+ if (options.limit === 0) {
1959
+ for (const parent of parents)
1960
+ parent[relName] = [];
1961
+ return;
1962
+ }
1963
+ const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
1906
1964
  // This loader stitches on the TARGET's own primary key, so the PK has to be
1907
1965
  // in the fetch even when the caller's select/omit excludes it, and has to
1908
1966
  // come back off afterwards. Exactly the shape `loadRelation` uses for its
@@ -1939,6 +1997,11 @@ class PowqlInterface {
1939
1997
  }
1940
1998
  }
1941
1999
  const targetByPk = new Map();
2000
+ // Position of each target in the fetch, i.e. its rank under the relation's
2001
+ // `orderBy`; the stitch below sorts a parent's children by it when an
2002
+ // orderBy was given, so `limit` keeps that parent's top-N and not its first
2003
+ // N junction rows. Across fetch CHUNKS the rank is fetch order.
2004
+ const targetRank = new Map();
1942
2005
  const targetValList = [...allTargetVals].map((v) => targetPkColMeta ? coerceScalar(v, targetPkColMeta.tsType) : v);
1943
2006
  const targetChunk = this.keyChunkSize(targetMeta, targetPkCol);
1944
2007
  for (let i = 0; i < targetValList.length; i += targetChunk) {
@@ -1949,14 +2012,21 @@ class PowqlInterface {
1949
2012
  };
1950
2013
  const targets = (await targetQi.findMany({
1951
2014
  ...fetchOptions,
2015
+ // Unbounded: the relation limit/offset are per parent, applied in the stitch.
2016
+ limit: undefined,
2017
+ offset: undefined,
1952
2018
  where,
1953
2019
  with: options.with,
1954
2020
  timeout: options.timeout ?? timeout,
1955
2021
  // Public findMany, so the sentinel form. See loadRelation above.
1956
2022
  includePii: includePii ? types_js_1.UNSAFE : undefined,
1957
2023
  }));
1958
- for (const t of targets)
1959
- targetByPk.set(String(t[targetPkField]), t);
2024
+ for (const t of targets) {
2025
+ const pk = String(t[targetPkField]);
2026
+ targetByPk.set(pk, t);
2027
+ if (!targetRank.has(pk))
2028
+ targetRank.set(pk, targetRank.size);
2029
+ }
1960
2030
  }
1961
2031
  // (3) Stitch: each parent → its junction targets (m2m is always a list).
1962
2032
  for (const parent of parents) {
@@ -1968,7 +2038,11 @@ class PowqlInterface {
1968
2038
  if (child)
1969
2039
  children.push(child);
1970
2040
  }
1971
- parent[relName] = children;
2041
+ if (options.orderBy) {
2042
+ const rank = (t) => targetRank.get(String(t[targetPkField])) ?? 0;
2043
+ children.sort((a, b) => rank(a) - rank(b));
2044
+ }
2045
+ parent[relName] = PowqlInterface.pageOf(children, options.limit, options.offset);
1972
2046
  }
1973
2047
  // Stitching is done: take the forced PK back off. Iterating the map rather
1974
2048
  // than the stitched lists is deliberate, one target can be linked from many
@@ -2012,11 +2086,12 @@ class PowqlInterface {
2012
2086
  * `referenceKey` on the TARGET table (the join's non-fetched side);
2013
2087
  * - m2m keeps any `orderBy`/`limit`/`offset` on the loader (the junction-order
2014
2088
  * stitch can't be reproduced by the 3-table join deterministically);
2015
- * - a to-one relation `limit`/`offset` (meaningless) stays on the loader, as
2016
- * does a to-many relation `limit`/`offset` when the parent set spills past
2017
- * one loader chunk (the loader limits per chunk, the join once globally).
2089
+ * - a to-one relation `limit`/`offset` (meaningless) stays on the loader. A
2090
+ * to-many relation `limit`/`offset` is a PER-PARENT bound on both paths,
2091
+ * fetched unbounded and sliced per parent at stitch time (`pageOf`), so
2092
+ * the join statement carries neither clause.
2018
2093
  */
2019
- joinEligible(rel, opt, args, parentCount) {
2094
+ joinEligible(rel, opt, args) {
2020
2095
  const effLimit = args.limit ?? this.defaultLimit;
2021
2096
  if (effLimit !== undefined || args.offset)
2022
2097
  return false;
@@ -2057,9 +2132,8 @@ class PowqlInterface {
2057
2132
  return false;
2058
2133
  }
2059
2134
  const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
2060
- if ((options.limit !== undefined || options.offset) && (single || parentCount > MAX_RELATION_KEYS)) {
2135
+ if ((options.limit !== undefined || options.offset) && single)
2061
2136
  return false;
2062
- }
2063
2137
  // A `limit 0` relation stays off the join statement for the same reason it
2064
2138
  // stays off a nested projection: PowDB answered `limit 0` with one row below
2065
2139
  // engine 0.20. The loader resolves it client-side, correctly on every version.
@@ -2106,13 +2180,13 @@ class PowqlInterface {
2106
2180
  const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
2107
2181
  const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout);
2108
2182
  const order = targetQi.buildOrder(options.orderBy, params, 'c');
2183
+ // No limit/offset clause: on the join they would bound the TOTAL child
2184
+ // count across every parent. The relation's page is per parent (pageOf).
2109
2185
  this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
2110
- const limitClause = options.limit !== undefined ? ` limit ${this.param(options.limit, params)}` : '';
2111
- const offsetClause = options.offset ? ` offset ${this.param(options.offset, params)}` : '';
2112
2186
  const proj = this.joinProjection(childCols, `p.${(0, powdb_shared_js_1.quotePowqlIdent)(parentKeyCol)}`, 'c');
2113
2187
  const powql = `${targetQi.qt} as c join ${this.qt} as p ` +
2114
2188
  `on c.${(0, powdb_shared_js_1.quotePowqlIdent)(childKeyCol)} = p.${(0, powdb_shared_js_1.quotePowqlIdent)(parentKeyCol)}` +
2115
- `${filter}${order}${limitClause}${offsetClause} ${proj}`;
2189
+ `${filter}${order} ${proj}`;
2116
2190
  // A READ: thread a read-shaped action through the exec seam.
2117
2191
  const { rows, native } = await targetQi.exec(powql, params, timeout, 'findMany');
2118
2192
  const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
@@ -2120,7 +2194,9 @@ class PowqlInterface {
2120
2194
  for (const p of parents) {
2121
2195
  const key = this.joinKey(p[parentKeyField]);
2122
2196
  const matches = (key == null ? undefined : byKey.get(key)) ?? [];
2123
- p[relName] = single ? (matches[0] ?? null) : matches;
2197
+ p[relName] = single
2198
+ ? (matches[0] ?? null)
2199
+ : PowqlInterface.pageOf(matches, options.limit, options.offset);
2124
2200
  }
2125
2201
  }
2126
2202
  /**
@@ -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
  }>;