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.
Files changed (71) hide show
  1. package/README.md +4 -4
  2. package/dist/checkout.d.ts +53 -0
  3. package/dist/checkout.js +78 -0
  4. package/dist/cjs/checkout.d.ts +53 -0
  5. package/dist/cjs/checkout.js +82 -0
  6. package/dist/cjs/cli/index.js +4 -0
  7. package/dist/cjs/cli/mcp.js +4 -0
  8. package/dist/cjs/cli/migrate.js +6 -0
  9. package/dist/cjs/cli/observe.js +8 -0
  10. package/dist/cjs/cli/studio.js +13 -1
  11. package/dist/cjs/client.d.ts +12 -2
  12. package/dist/cjs/client.js +61 -75
  13. package/dist/cjs/connection-guard.d.ts +120 -0
  14. package/dist/cjs/connection-guard.js +191 -0
  15. package/dist/cjs/errors.d.ts +26 -0
  16. package/dist/cjs/errors.js +85 -1
  17. package/dist/cjs/index.d.ts +1 -1
  18. package/dist/cjs/nested-write.d.ts +12 -2
  19. package/dist/cjs/nested-write.js +4 -10
  20. package/dist/cjs/pipeline.js +12 -7
  21. package/dist/cjs/plan-flip-probe.js +4 -0
  22. package/dist/cjs/powdb-shared.d.ts +22 -2
  23. package/dist/cjs/powdb-shared.js +27 -2
  24. package/dist/cjs/powdb.js +36 -37
  25. package/dist/cjs/powql.d.ts +51 -6
  26. package/dist/cjs/powql.js +199 -45
  27. package/dist/cjs/prisma-compat.js +28 -4
  28. package/dist/cjs/query/builder.d.ts +44 -24
  29. package/dist/cjs/query/builder.js +125 -66
  30. package/dist/cjs/query/deferred.d.ts +9 -0
  31. package/dist/cjs/query/option-surface.js +12 -0
  32. package/dist/cjs/query/types.d.ts +68 -4
  33. package/dist/cjs/query/writes.d.ts +39 -9
  34. package/dist/cjs/query/writes.js +72 -34
  35. package/dist/cjs/realtime.d.ts +46 -2
  36. package/dist/cjs/realtime.js +125 -20
  37. package/dist/cjs/schema-sql.js +6 -0
  38. package/dist/cli/index.js +4 -0
  39. package/dist/cli/mcp.js +4 -0
  40. package/dist/cli/migrate.js +6 -0
  41. package/dist/cli/observe.js +8 -0
  42. package/dist/cli/studio.js +13 -1
  43. package/dist/client.d.ts +12 -2
  44. package/dist/client.js +62 -76
  45. package/dist/connection-guard.d.ts +120 -0
  46. package/dist/connection-guard.js +183 -0
  47. package/dist/errors.d.ts +26 -0
  48. package/dist/errors.js +83 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/nested-write.d.ts +12 -2
  52. package/dist/nested-write.js +4 -10
  53. package/dist/pipeline.js +13 -8
  54. package/dist/plan-flip-probe.js +4 -0
  55. package/dist/powdb-shared.d.ts +22 -2
  56. package/dist/powdb-shared.js +25 -2
  57. package/dist/powdb.js +23 -24
  58. package/dist/powql.d.ts +51 -6
  59. package/dist/powql.js +200 -46
  60. package/dist/prisma-compat.js +28 -4
  61. package/dist/query/builder.d.ts +44 -24
  62. package/dist/query/builder.js +126 -67
  63. package/dist/query/deferred.d.ts +9 -0
  64. package/dist/query/option-surface.js +12 -0
  65. package/dist/query/types.d.ts +68 -4
  66. package/dist/query/writes.d.ts +39 -9
  67. package/dist/query/writes.js +71 -34
  68. package/dist/realtime.d.ts +46 -2
  69. package/dist/realtime.js +125 -20
  70. package/dist/schema-sql.js +6 -0
  71. package/package.json +5 -3
@@ -9,6 +9,8 @@ exports.getErrorMessageMode = getErrorMessageMode;
9
9
  exports.markValueBearingMessage = markValueBearingMessage;
10
10
  exports.describeTargetForMessage = describeTargetForMessage;
11
11
  exports.malformedConnectionStringMessage = malformedConnectionStringMessage;
12
+ exports.explainConnectionLoss = explainConnectionLoss;
13
+ exports.isStaleConnectionError = isStaleConnectionError;
12
14
  exports.wrapPgError = wrapPgError;
13
15
  const node_async_hooks_1 = require("node:async_hooks");
14
16
  /**
@@ -1231,6 +1233,84 @@ const CONNECTION_ERROR_CODES = new Set([
1231
1233
  'CERT_HAS_EXPIRED',
1232
1234
  'ERR_TLS_CERT_ALTNAME_INVALID',
1233
1235
  ]);
1236
+ /**
1237
+ * The driver's own words for "this connection is gone", which carry NO code.
1238
+ *
1239
+ * node-postgres raises these as plain `Error`s with no SQLSTATE and no socket
1240
+ * code: the first when the socket closes under a live client, the next two for
1241
+ * any query sent to a client after that, the last two when a connect or an
1242
+ * orderly `end()` cuts a query off. Keyed on `.code` alone, wrapPgError handed
1243
+ * every one of them back untyped, so the commonest symptom of a database
1244
+ * restart was the one connection failure a `catch (e) { if (e instanceof
1245
+ * ConnectionError) ... }` did not see. Exact matches on the full message, and
1246
+ * only on code-less errors: these strings are pg's and have not changed across
1247
+ * the 8.x line, and nothing else produces them.
1248
+ */
1249
+ const PG_CONNECTION_LOSS_MESSAGES = new Set([
1250
+ 'Connection terminated unexpectedly',
1251
+ 'Client has encountered a connection error and is not queryable',
1252
+ 'Client was closed and is not queryable',
1253
+ 'Connection terminated',
1254
+ 'Connection terminated due to connection timeout',
1255
+ ]);
1256
+ /** The follow-on errors pg raises for a query sent to a client already known to be dead. */
1257
+ const PG_NOT_QUERYABLE_MESSAGES = new Set([
1258
+ 'Client has encountered a connection error and is not queryable',
1259
+ 'Client was closed and is not queryable',
1260
+ ]);
1261
+ /**
1262
+ * Report the error that KILLED a held connection instead of its follow-on.
1263
+ *
1264
+ * When a connection dies while a transaction callback is between queries, the
1265
+ * next query fails with "Client has encountered a connection error and is not
1266
+ * queryable", which says nothing about why. The connection guard recorded the
1267
+ * real cause (`lostWith`, e.g. 57P01 `terminating connection due to
1268
+ * administrator command`), so a not-queryable ConnectionError is swapped for
1269
+ * that one. Anything else passes through: an error the caller threw, or a
1270
+ * query that was IN FLIGHT when the connection died, which already carries the
1271
+ * server's own message.
1272
+ */
1273
+ function explainConnectionLoss(err, lostWith) {
1274
+ if (!lostWith || !(err instanceof ConnectionError))
1275
+ return err;
1276
+ const cause = err.cause;
1277
+ if (!(cause instanceof Error) || !PG_NOT_QUERYABLE_MESSAGES.has(cause.message))
1278
+ return err;
1279
+ const original = wrapPgError(lostWith);
1280
+ return original instanceof ConnectionError ? original : err;
1281
+ }
1282
+ /**
1283
+ * Codes that mean an ESTABLISHED connection went away: the server ended the
1284
+ * backend (57P01, which is also what a dead idle connection hands the next
1285
+ * query, since pg attributes the FATAL it buffered to whatever is sent next),
1286
+ * or the peer reset the socket.
1287
+ */
1288
+ const STALE_CONNECTION_CODES = new Set(['57P01', 'ECONNRESET', 'EPIPE']);
1289
+ /** pg's code-less messages for the same thing. */
1290
+ const STALE_CONNECTION_MESSAGES = new Set(['Connection terminated unexpectedly', ...PG_NOT_QUERYABLE_MESSAGES]);
1291
+ /**
1292
+ * Whether `err` says a connection that was already open is gone, so the same
1293
+ * statement on a fresh connection can succeed. Accepts the raw driver error or
1294
+ * the {@link ConnectionError} wrapPgError made of it.
1295
+ *
1296
+ * Narrower than "is a ConnectionError" on purpose. A connection that could not
1297
+ * be OPENED (refused, DNS, auth, a connect timeout, 57P03 while the server
1298
+ * starts) will not open on an immediate second try either, and retrying it
1299
+ * only doubles the wait before the caller hears about it. Nor does it cover
1300
+ * `Connection terminated` without "unexpectedly", which is the pool itself
1301
+ * shutting down.
1302
+ */
1303
+ function isStaleConnectionError(err) {
1304
+ const raw = err instanceof ConnectionError && err.cause instanceof Error ? err.cause : err;
1305
+ if (!raw || typeof raw !== 'object')
1306
+ return false;
1307
+ const code = raw.code;
1308
+ if (code)
1309
+ return STALE_CONNECTION_CODES.has(code);
1310
+ return raw instanceof Error && STALE_CONNECTION_MESSAGES.has(raw.message);
1311
+ }
1312
+ const CONNECTION_LOSS_HINT = 'The server or the network closed it (a restart, failover, idle timeout, compute suspend, or ' +
1313
+ 'pg_terminate_backend). The pool discards the connection, so retrying the operation opens a fresh one.';
1234
1314
  /**
1235
1315
  * Actionable next step per connection-class code, appended to the driver's own
1236
1316
  * message. The driver message states WHAT happened ("password authentication
@@ -1503,8 +1583,12 @@ function wrapPgError(err) {
1503
1583
  if (!err || typeof err !== 'object')
1504
1584
  return err;
1505
1585
  const e = err;
1506
- if (!e.code)
1586
+ if (!e.code) {
1587
+ if (err instanceof Error && PG_CONNECTION_LOSS_MESSAGES.has(err.message)) {
1588
+ return new ConnectionError(`Database connection lost: ${err.message}. ${CONNECTION_LOSS_HINT}`, { cause: err });
1589
+ }
1507
1590
  return err;
1591
+ }
1508
1592
  switch (e.code) {
1509
1593
  case '23505': {
1510
1594
  const cols = e.detail ? parseColumnsFromDetail(e.detail) : undefined;
@@ -46,7 +46,7 @@ export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSu
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
48
  export { MAX_QUERY_TAG_LENGTH, RAW_QUERY_MODEL } from './query-events.js';
49
- export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
49
+ export { type ActiveSubscription, type ListenOptions, type ListenReconnectOptions, type NotificationHandler, type Subscription, validateChannel, } from './realtime.js';
50
50
  export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, PrismaSchemaSource, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
51
51
  export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
52
52
  export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnIndexDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, type DocFieldIndexDef, defineSchema, isDocFieldIndexDef, type ManyToManyDef, type ReferenceDef, type SchemaDef, type SchemaIndexDef, type TableDef, table, } from './schema-builder.js';
@@ -137,10 +137,20 @@ export declare function createManyShapeRuns<T extends Record<string, unknown>>(r
137
137
  * operation (create, connect, connectOrCreate), and finally reads back the
138
138
  * full tree using `findUnique` with an auto-built `with` clause.
139
139
  */
140
- export declare function executeNestedCreate(ctx: NestedWriteContext, tableName: string, data: Record<string, unknown>, depth?: number, path?: string[]): Promise<Record<string, unknown>>;
140
+ /**
141
+ * The caller's `select` / `omit` for the TOP-LEVEL row of a nested write. Only
142
+ * the final read-back applies it (see executeNestedCreate), so it narrows the
143
+ * returned scalars exactly as it does on a read, while every intermediate
144
+ * statement keeps the full row it needs for keys.
145
+ */
146
+ export interface NestedReturnShape {
147
+ select?: Record<string, boolean>;
148
+ omit?: Record<string, boolean>;
149
+ }
150
+ export declare function executeNestedCreate(ctx: NestedWriteContext, tableName: string, data: Record<string, unknown>, depth?: number, path?: string[], returnShape?: NestedReturnShape): Promise<Record<string, unknown>>;
141
151
  /**
142
152
  * Tree-walking update: updates the parent row with scalar data, then
143
153
  * processes each relation operation (create, connect, connectOrCreate,
144
154
  * disconnect, set, delete), and reads back the full tree.
145
155
  */
146
- export declare function executeNestedUpdate(ctx: NestedWriteContext, tableName: string, where: Record<string, unknown>, data: Record<string, unknown>, depth?: number, path?: string[]): Promise<Record<string, unknown>>;
156
+ export declare function executeNestedUpdate(ctx: NestedWriteContext, tableName: string, where: Record<string, unknown>, data: Record<string, unknown>, depth?: number, path?: string[], returnShape?: NestedReturnShape): Promise<Record<string, unknown>>;
@@ -836,15 +836,7 @@ function rethrowAsNotRelated(err, op, relName, rel, target) {
836
836
  }
837
837
  throw err;
838
838
  }
839
- // ---------------------------------------------------------------------------
840
- // executeNestedCreate
841
- // ---------------------------------------------------------------------------
842
- /**
843
- * Tree-walking create: inserts the parent row, then processes each relation
844
- * operation (create, connect, connectOrCreate), and finally reads back the
845
- * full tree using `findUnique` with an auto-built `with` clause.
846
- */
847
- async function executeNestedCreate(ctx, tableName, data, depth = 0, path = []) {
839
+ async function executeNestedCreate(ctx, tableName, data, depth = 0, path = [], returnShape) {
848
840
  if (depth > MAX_DEPTH) {
849
841
  throw new errors_js_1.CircularRelationError(path);
850
842
  }
@@ -910,6 +902,7 @@ async function executeNestedCreate(ctx, tableName, data, depth = 0, path = []) {
910
902
  const fullRow = await ctx.tx.table(tableName).findUnique({
911
903
  where: pkWhere(tableMeta, parentRow),
912
904
  with: withClause,
905
+ ...returnShape,
913
906
  });
914
907
  return (fullRow ?? parentRow);
915
908
  }
@@ -921,7 +914,7 @@ async function executeNestedCreate(ctx, tableName, data, depth = 0, path = []) {
921
914
  * processes each relation operation (create, connect, connectOrCreate,
922
915
  * disconnect, set, delete), and reads back the full tree.
923
916
  */
924
- async function executeNestedUpdate(ctx, tableName, where, data, depth = 0, path = []) {
917
+ async function executeNestedUpdate(ctx, tableName, where, data, depth = 0, path = [], returnShape) {
925
918
  if (depth > MAX_DEPTH) {
926
919
  throw new errors_js_1.CircularRelationError(path);
927
920
  }
@@ -1016,6 +1009,7 @@ async function executeNestedUpdate(ctx, tableName, where, data, depth = 0, path
1016
1009
  const fullRow = await ctx.tx.table(tableName).findUnique({
1017
1010
  where: pkWhere(tableMeta, parentRow),
1018
1011
  with: readBackWith(ctx.schema, tableName, relations),
1012
+ ...returnShape,
1019
1013
  });
1020
1014
  return (fullRow ?? parentRow);
1021
1015
  }
@@ -22,6 +22,7 @@
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.executePipeline = executePipeline;
24
24
  exports.pipelineSupported = pipelineSupported;
25
+ const connection_guard_js_1 = require("./connection-guard.js");
25
26
  const dialect_js_1 = require("./dialect.js");
26
27
  const errors_js_1 = require("./errors.js");
27
28
  const pipeline_submittable_js_1 = require("./pipeline-submittable.js");
@@ -206,6 +207,9 @@ async function executePipeline(pool, queries, options) {
206
207
  catch (err) {
207
208
  throw (0, errors_js_1.wrapPgError)(err);
208
209
  }
210
+ // Guarded for the checkout window, see connection-guard.ts: a connection that
211
+ // dies mid-batch emits 'error', which with no listener exits the process.
212
+ const checkout = (0, connection_guard_js_1.guardCheckout)(client);
209
213
  try {
210
214
  if ((0, pipeline_submittable_js_1.supportsExtendedPipeline)(client)) {
211
215
  // Real pipeline path, uses extended-query protocol wire methods
@@ -227,8 +231,8 @@ async function executePipeline(pool, queries, options) {
227
231
  // driver error that must not escape with a SQLSTATE sitting in the same
228
232
  // `.code` slot Turbine puts TURBINE_E0NN in.
229
233
  if (err instanceof errors_js_1.TurbineError)
230
- throw err;
231
- throw (0, errors_js_1.wrapPgError)(err);
234
+ throw (0, errors_js_1.explainConnectionLoss)(err, checkout.lostWith);
235
+ throw (0, errors_js_1.explainConnectionLoss)((0, errors_js_1.wrapPgError)(err), checkout.lostWith);
232
236
  }
233
237
  finally {
234
238
  // A client the pipeline could not return to a clean state is released WITH
@@ -239,10 +243,10 @@ async function executePipeline(pool, queries, options) {
239
243
  // normally is what turned one failed batch into `25P02` on somebody else's
240
244
  // query.
241
245
  if ((0, pipeline_submittable_js_1.pipelineClientNeedsDiscard)(client)) {
242
- client.release(new Error('turbine: pipeline connection left an open transaction and was discarded'));
246
+ checkout.release(new Error('turbine: pipeline connection left an open transaction and was discarded'));
243
247
  }
244
248
  else {
245
- client.release();
249
+ checkout.release();
246
250
  }
247
251
  }
248
252
  }
@@ -254,15 +258,16 @@ async function executePipeline(pool, queries, options) {
254
258
  * Note: This acquires and immediately releases a connection to inspect it.
255
259
  */
256
260
  async function pipelineSupported(pool) {
257
- let client;
261
+ let checkout;
258
262
  try {
259
- client = await pool.connect();
263
+ const client = await pool.connect();
264
+ checkout = (0, connection_guard_js_1.guardCheckout)(client);
260
265
  return (0, pipeline_submittable_js_1.supportsExtendedPipeline)(client);
261
266
  }
262
267
  catch {
263
268
  return false;
264
269
  }
265
270
  finally {
266
- client?.release();
271
+ checkout?.release();
267
272
  }
268
273
  }
@@ -118,6 +118,7 @@ exports.buildFlipProbeSql = buildFlipProbeSql;
118
118
  exports.verdictFromPlanJson = verdictFromPlanJson;
119
119
  exports.probePlanFlips = probePlanFlips;
120
120
  exports.applyFlipVerdicts = applyFlipVerdicts;
121
+ const connection_guard_js_1 = require("./connection-guard.js");
121
122
  const utils_js_1 = require("./query/utils.js");
122
123
  /** An empty result, which keeps every finding. Used when probing is off. */
123
124
  function emptyFlipProbeResult() {
@@ -262,6 +263,9 @@ async function probePlanFlips(options) {
262
263
  }
263
264
  const { Client } = (await Promise.resolve().then(() => __importStar(require('pg')))).default;
264
265
  const client = new Client({ connectionString: options.connectionString });
266
+ // A dropped connection must fail the probe (which then reports 'unknown'),
267
+ // not exit the process through an unheard 'error' event.
268
+ (0, connection_guard_js_1.guardConnection)(client);
265
269
  try {
266
270
  await client.connect();
267
271
  // READ ONLY is belt-and-braces: EXPLAIN without ANALYZE cannot write, and the
@@ -19,8 +19,9 @@
19
19
  *
20
20
  * Everything here is re-exported by powdb.ts under its original name, so the
21
21
  * public `turbine-orm/powdb` surface is byte-identical to before the split.
22
- * The few helpers powdb.ts consumes but never published (`isDateColumn`) are
23
- * exported from this module and NOT re-exported from powdb.ts.
22
+ * The few helpers powdb.ts consumes but never published (`isDateColumn`,
23
+ * `parsePowdbSemver`, `atLeastVersion`) are exported from this module and NOT
24
+ * re-exported from powdb.ts.
24
25
  *
25
26
  * @module
26
27
  */
@@ -140,6 +141,25 @@ export interface PowdbCapabilities {
140
141
  }
141
142
  /** The feature-gate capability keys (everything except the version/nativeRaw metadata). */
142
143
  type PowdbFeatureKey = 'jsonDocs' | 'docFieldIndexes' | 'introspection' | 'serverJoins' | 'nestedProjections' | 'entityLinks' | 'linkIntrospection' | 'linkPaths' | 'datetimeCompare' | 'projectedCountNonNull';
144
+ /** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
145
+ export declare function parsePowdbSemver(version: string | undefined | null): {
146
+ major: number;
147
+ minor: number;
148
+ patch: number;
149
+ } | null;
150
+ /**
151
+ * Is `sem` at least `major.minor.patch`? PATCH-AWARE: `patch` defaults to `0`,
152
+ * so a two-component floor (`atLeastVersion(sem, 0, 19)`) behaves exactly as the
153
+ * old major/minor comparison did (matches every patch of 0.19), while a
154
+ * three-component floor (`atLeastVersion(sem, 0, 19, 1)`) additionally requires
155
+ * the patch, the distinction the link lanes need (0.19.1, never 0.19.0). Every
156
+ * existing two-argument call keeps its prior semantics unchanged.
157
+ */
158
+ export declare function atLeastVersion(sem: {
159
+ major: number;
160
+ minor: number;
161
+ patch: number;
162
+ }, major: number, minor: number, patch?: number): boolean;
143
163
  /**
144
164
  * Trusted-caller default: every FEATURE gate on, engine version unknown. Used
145
165
  * for a directly-constructed {@link PowdbPool} / {@link PowdbEmbeddedPool} that
@@ -20,13 +20,16 @@
20
20
  *
21
21
  * Everything here is re-exported by powdb.ts under its original name, so the
22
22
  * public `turbine-orm/powdb` surface is byte-identical to before the split.
23
- * The few helpers powdb.ts consumes but never published (`isDateColumn`) are
24
- * exported from this module and NOT re-exported from powdb.ts.
23
+ * The few helpers powdb.ts consumes but never published (`isDateColumn`,
24
+ * `parsePowdbSemver`, `atLeastVersion`) are exported from this module and NOT
25
+ * re-exported from powdb.ts.
25
26
  *
26
27
  * @module
27
28
  */
28
29
  Object.defineProperty(exports, "__esModule", { value: true });
29
30
  exports.POWQL_KEYWORDS = exports.ALL_POWDB_CAPABILITIES = exports.PowdbJsonParam = exports.PowdbFloatParam = void 0;
31
+ exports.parsePowdbSemver = parsePowdbSemver;
32
+ exports.atLeastVersion = atLeastVersion;
30
33
  exports.requireCapability = requireCapability;
31
34
  exports.baseTsType = baseTsType;
32
35
  exports.isJsonColumn = isJsonColumn;
@@ -77,6 +80,28 @@ class PowdbJsonParam {
77
80
  }
78
81
  }
79
82
  exports.PowdbJsonParam = PowdbJsonParam;
83
+ /** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
84
+ function parsePowdbSemver(version) {
85
+ const m = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(version ?? '').trim());
86
+ if (!m)
87
+ return null;
88
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
89
+ }
90
+ /**
91
+ * Is `sem` at least `major.minor.patch`? PATCH-AWARE: `patch` defaults to `0`,
92
+ * so a two-component floor (`atLeastVersion(sem, 0, 19)`) behaves exactly as the
93
+ * old major/minor comparison did (matches every patch of 0.19), while a
94
+ * three-component floor (`atLeastVersion(sem, 0, 19, 1)`) additionally requires
95
+ * the patch, the distinction the link lanes need (0.19.1, never 0.19.0). Every
96
+ * existing two-argument call keeps its prior semantics unchanged.
97
+ */
98
+ function atLeastVersion(sem, major, minor, patch = 0) {
99
+ if (sem.major !== major)
100
+ return sem.major > major;
101
+ if (sem.minor !== minor)
102
+ return sem.minor > minor;
103
+ return sem.patch >= patch;
104
+ }
80
105
  /**
81
106
  * Minimum engine version each gated feature needs, for the E017 hint text.
82
107
  * Most gates carry a `major.minor` floor (patch-insensitive); the two link
package/dist/cjs/powdb.js CHANGED
@@ -270,28 +270,6 @@ function assertSupportedPowdbVersion(version) {
270
270
  * powql.ts.
271
271
  */
272
272
  exports.POWQL_MAX_NESTING_DEPTH = 64;
273
- /** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
274
- function parsePowdbSemver(version) {
275
- const m = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(version ?? '').trim());
276
- if (!m)
277
- return null;
278
- return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
279
- }
280
- /**
281
- * Is `sem` at least `major.minor.patch`? PATCH-AWARE: `patch` defaults to `0`,
282
- * so a two-component floor (`atLeastVersion(sem, 0, 19)`) behaves exactly as the
283
- * old major/minor comparison did (matches every patch of 0.19), while a
284
- * three-component floor (`atLeastVersion(sem, 0, 19, 1)`) additionally requires
285
- * the patch, the distinction the link lanes need (0.19.1, never 0.19.0). Every
286
- * existing two-argument call keeps its prior semantics unchanged.
287
- */
288
- function atLeastVersion(sem, major, minor, patch = 0) {
289
- if (sem.major !== major)
290
- return sem.major > major;
291
- if (sem.minor !== minor)
292
- return sem.minor > minor;
293
- return sem.patch >= patch;
294
- }
295
273
  /**
296
274
  * Derive {@link PowdbCapabilities} from an engine version string. A non-semver /
297
275
  * unknown version turns every gate OFF (the E017 hint then tells the caller to
@@ -299,7 +277,7 @@ function atLeastVersion(sem, major, minor, patch = 0) {
299
277
  * to expose `queryNativeRaw` (passed in) AND server ≥ 0.13.
300
278
  */
301
279
  function capabilitiesFromVersion(version, opts = {}) {
302
- const sem = parsePowdbSemver(version);
280
+ const sem = (0, powdb_shared_js_1.parsePowdbSemver)(version);
303
281
  if (!sem) {
304
282
  return {
305
283
  engineVersion: version ?? null,
@@ -318,20 +296,20 @@ function capabilitiesFromVersion(version, opts = {}) {
318
296
  }
319
297
  return {
320
298
  engineVersion: `${sem.major}.${sem.minor}.${sem.patch}`,
321
- introspection: atLeastVersion(sem, 0, 10),
322
- jsonDocs: atLeastVersion(sem, 0, 12),
323
- docFieldIndexes: atLeastVersion(sem, 0, 13),
324
- serverJoins: atLeastVersion(sem, 0, 13),
325
- nestedProjections: atLeastVersion(sem, 0, 18),
326
- entityLinks: atLeastVersion(sem, 0, 19),
299
+ introspection: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 10),
300
+ jsonDocs: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 12),
301
+ docFieldIndexes: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 13),
302
+ serverJoins: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 13),
303
+ nestedProjections: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 18),
304
+ entityLinks: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 19),
327
305
  // PATCH-floored at 0.19.1: the `schema links` listing + `describe` link rows
328
306
  // and the safe (hard-erroring) scalar-path traversal both landed in 0.19.1,
329
307
  // never 0.19.0.
330
- linkIntrospection: atLeastVersion(sem, 0, 19, 1),
331
- linkPaths: atLeastVersion(sem, 0, 19, 1),
332
- datetimeCompare: atLeastVersion(sem, 0, 20),
333
- projectedCountNonNull: atLeastVersion(sem, 0, 20),
334
- nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
308
+ linkIntrospection: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 19, 1),
309
+ linkPaths: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 19, 1),
310
+ datetimeCompare: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 20),
311
+ projectedCountNonNull: (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 20),
312
+ nativeRaw: Boolean(opts.hasNativeRaw) && (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 13),
335
313
  };
336
314
  }
337
315
  /**
@@ -396,6 +374,19 @@ function powqlSchemaDDL(schema, opts = {}) {
396
374
  // m2m junction's `(source_id, target_id)`) marks its columns `required` but
397
375
  // cannot enforce the tuple's uniqueness at the engine level.
398
376
  const pkIsSingle = meta.primaryKey.length === 1;
377
+ // The primary key's OWN index: introspection lists it in `indexes` (the
378
+ // `<table>_pkey` a PRIMARY KEY constraint creates), so a composite key used
379
+ // to reach the composite-index refusal below even though the type body
380
+ // already handles it. Identified by what it guarantees rather than by name:
381
+ // a full (non-partial) unique index over exactly the PK's column SET states
382
+ // the PK constraint and nothing else, whatever order it lists the columns in.
383
+ // Anything short of that (non-unique, partial, a subset or superset) is a
384
+ // genuine composite index and is still refused.
385
+ const isPrimaryKeyIndex = (idx) => idx.unique &&
386
+ !idx.partial &&
387
+ !idx.docPath &&
388
+ new Set(idx.columns).size === pkSet.size &&
389
+ idx.columns.every((c) => pkSet.has(c));
399
390
  const fields = meta.columns.map((col) => {
400
391
  const powqlType = (0, powdb_shared_js_1.powqlColumnType)(col);
401
392
  // Gate `json` columns behind the engine's jsonDocs capability when a
@@ -449,8 +440,16 @@ function powqlSchemaDDL(schema, opts = {}) {
449
440
  stmts.push(`alter ${(0, powdb_shared_js_1.quotePowqlIdent)(meta.name)} add ${kind} (.${(0, powdb_shared_js_1.quotePowqlIdent)(column)}${segs})`);
450
441
  }
451
442
  else {
443
+ // The composite PK's index is already the type body's `required` columns
444
+ // (see isPrimaryKeyIndex); a single-column PK's index is skipped by the
445
+ // emittedUnique check below.
446
+ if (idx.columns.length > 1 && isPrimaryKeyIndex(idx))
447
+ continue;
452
448
  // Plain column index. PowDB has no composite index (`add index` takes a
453
- // single `.column`), so a multi-column entry is a typed E017.
449
+ // single `.column`), so a multi-column entry is a typed E017. That
450
+ // includes a composite UNIQUE index other than the PK: it is an
451
+ // integrity rule the engine cannot enforce, so it is refused rather than
452
+ // dropped without a word.
454
453
  if (idx.columns.length !== 1) {
455
454
  throw new errors_js_1.UnsupportedFeatureError('composite indexes', 'PowDB', `PowDB has no composite index. Index "${idx.name}" on ${meta.name} lists ` +
456
455
  `${idx.columns.length} columns; declare a single-column index (or a doc-field index) instead.`);
@@ -1634,8 +1633,8 @@ class PowdbEmbeddedPool {
1634
1633
  // reach this branch with a newer-than-ceiling engine is the anomalous
1635
1634
  // newer-addon-without-native case (feature-detect failed). Refuse it rather
1636
1635
  // than inline-encode against an unverified lexer.
1637
- const engineSem = parsePowdbSemver(this.capabilities.engineVersion);
1638
- const ceiling = parsePowdbSemver(exports.POWQL_LEXER_TESTED_CEILING);
1636
+ const engineSem = (0, powdb_shared_js_1.parsePowdbSemver)(this.capabilities.engineVersion);
1637
+ const ceiling = (0, powdb_shared_js_1.parsePowdbSemver)(exports.POWQL_LEXER_TESTED_CEILING);
1639
1638
  if (engineSem &&
1640
1639
  ceiling &&
1641
1640
  (engineSem.major > ceiling.major || (engineSem.major === ceiling.major && engineSem.minor > ceiling.minor))) {
@@ -499,6 +499,21 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
499
499
  * becomes a no-op like {@link parseWriteRow} on the SQL engines.
500
500
  */
501
501
  private stripWritePii;
502
+ /**
503
+ * A single-row write's `select` / `omit`, resolved by {@link projectionPlan}
504
+ * so the rules and messages are the read path's, and the SQL engines' (an
505
+ * unknown name is E003, `select` must name something, the pair is refused).
506
+ * `undefined` for the default return shape. Resolved BEFORE the write is sent,
507
+ * so a bad projection writes nothing.
508
+ *
509
+ * The narrowing itself happens on the returned row ({@link shapeWriteRow}):
510
+ * PowQL's `returning` takes no column list (see {@link stripWritePii}), so
511
+ * unlike the SQL engines the unselected columns still cross the wire here.
512
+ * The RESULT is identical across engines; the byte saving is SQL-only.
513
+ */
514
+ private writeReturnPlan;
515
+ /** Apply a write's return plan to its row, or the default PII strip without one. */
516
+ private shapeWriteRow;
502
517
  /** `{ .c1, .c2, … }` projection clause. */
503
518
  private projection;
504
519
  /**
@@ -874,12 +889,21 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
874
889
  }>;
875
890
  upsert(args: UpsertArgs<T>): Promise<T>;
876
891
  /**
877
- * Composite-key upsert: PowQL's `upsert … on .col` only takes one conflict
878
- * column, so reselect by the full composite PK and update-or-create inside one
879
- * flat transaction (PowDB single-writer makes the read-then-write safe from
880
- * concurrent writers; the transaction makes it atomic with the write).
881
- */
882
- private upsertComposite;
892
+ * Upsert as a reselect-or-write inside one flat transaction, for every shape
893
+ * the native `upsert … on .col` statement cannot express: a conflict target
894
+ * other than a single-column primary key (it takes one column and PowDB has
895
+ * no composite unique), and a table under a global filter (its conflict
896
+ * branch takes no predicate). PowDB's single writer makes the read-then-write
897
+ * safe from concurrent writers; the transaction makes it atomic with the
898
+ * write.
899
+ *
900
+ * The row is looked up by the conflict columns with `create`'s values for
901
+ * them, which is what `ON CONFLICT (<cols>)` compares on the SQL engines. The
902
+ * find and the update run through the transaction's table interface, so a
903
+ * configured global filter applies to both exactly as it does to any other
904
+ * read or write, and `skipGlobalFilters` is forwarded to both.
905
+ */
906
+ private upsertLookupFirst;
883
907
  count(args?: CountArgs<T>): Promise<number>;
884
908
  /**
885
909
  * Gate ONE field of a per-field `_count`.
@@ -902,6 +926,27 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
902
926
  * instead of values below 0.20, the same drift any stale-metadata query has.
903
927
  */
904
928
  private assertProjectedCountSupported;
929
+ /**
930
+ * Can `aggregate()` compute several aggregates in ONE statement on this engine?
931
+ *
932
+ * PowQL refuses a bare multi-aggregate projection (`T { a: sum(.x), b: sum(.y) }`
933
+ * is "aggregate function in an unsupported position" on every engine version),
934
+ * so the only one-statement form is a grouping over a literal key,
935
+ * `T filter … group 1 { agg_0: sum(.x), agg_1: sum(.y) }`, which yields a
936
+ * single group holding every row the filter matched. Measured against the
937
+ * embedded addon at 0.7.1 through 0.28: below 0.13 a literal group key does not
938
+ * parse, from 0.13 to 0.19.1 the grouped per-field `count` disagrees with the
939
+ * scalar `count(T { .col })` on a nullable column, and from 0.20 every
940
+ * aggregate kind (count / sum / avg / min / max over int, float, str, an
941
+ * all-null column, and several filters) answers identically on both wires.
942
+ *
943
+ * Derived from the PROBED engine version rather than from a capability flag,
944
+ * so an unprobed pool (`engineVersion: null`, e.g. an injected pool carrying
945
+ * {@link ALL_POWDB_CAPABILITIES}) keeps the per-field statements: this changes
946
+ * the emitted PowQL, and an older engine would reject it outright, the same
947
+ * probe-only discipline `nestedProjections` follows.
948
+ */
949
+ private get groupsAggregatesInOneStatement();
905
950
  aggregate(args: AggregateArgs<T>): Promise<AggregateResult<T>>;
906
951
  groupBy(args: GroupByArgs<T>): Promise<Record<string, unknown>[]>;
907
952
  /** Validate a JSON-path target (group key / aggregate target): non-empty array of keys/indexes. */