linkgress-orm 0.4.68 → 0.4.70

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.
@@ -268,23 +268,33 @@ class QueryExecutor {
268
268
  * `.expectedExecutionTime()`. If the query runs longer than this,
269
269
  * `onQueryTakingTooLong` fires. `undefined` means use the context default.
270
270
  */
271
- overrideExpectedMs) {
271
+ overrideExpectedMs,
272
+ /**
273
+ * Per-query prepared-statement override set via `.withPreparedStatements()`.
274
+ * `undefined` means use the context's `preparedStatements` default.
275
+ */
276
+ overridePrepare) {
272
277
  this.client = client;
273
278
  this.options = options;
274
279
  this.overrideTimeoutMs = overrideTimeoutMs;
275
280
  this.overrideExpectedMs = overrideExpectedMs;
281
+ this.overridePrepare = overridePrepare;
276
282
  }
277
283
  /**
278
- * Build the per-query execution options (binary protocol + timeout override),
279
- * or `undefined` when neither is set so the driver takes its fast path.
284
+ * Build the per-query execution options (binary protocol, timeout override, prepared
285
+ * statement), or `undefined` when none is set so the driver takes its fast path.
286
+ * `execution` is a per-call override a caller passes when it knows the statement's
287
+ * text is unique to this call (see {@link QueryOptions.preparedStatements}).
280
288
  */
281
- buildExecutionOptions() {
282
- if (!this.options.useBinaryProtocol && this.overrideTimeoutMs === undefined) {
289
+ buildExecutionOptions(execution) {
290
+ const prepare = execution?.prepare ?? this.overridePrepare ?? this.options.preparedStatements;
291
+ if (!this.options.useBinaryProtocol && this.overrideTimeoutMs === undefined && prepare !== true) {
283
292
  return undefined;
284
293
  }
285
294
  return {
286
295
  useBinaryProtocol: this.options.useBinaryProtocol,
287
296
  timeoutMs: this.overrideTimeoutMs,
297
+ ...(prepare === true ? { prepare: true } : {}),
288
298
  };
289
299
  }
290
300
  /**
@@ -293,7 +303,7 @@ class QueryExecutor {
293
303
  * the derived executor. Used by `.withTimeout()` on the query builders.
294
304
  */
295
305
  withTimeout(timeoutMs) {
296
- return new QueryExecutor(this.client, this.options, timeoutMs, this.overrideExpectedMs);
306
+ return new QueryExecutor(this.client, this.options, timeoutMs, this.overrideExpectedMs, this.overridePrepare);
297
307
  }
298
308
  /**
299
309
  * Return a new executor that flags this query as expected to finish within
@@ -301,7 +311,16 @@ class QueryExecutor {
301
311
  * `.expectedExecutionTime()` on the query builders.
302
312
  */
303
313
  withExpectedExecutionTime(expectedMs) {
304
- return new QueryExecutor(this.client, this.options, this.overrideTimeoutMs, expectedMs);
314
+ return new QueryExecutor(this.client, this.options, this.overrideTimeoutMs, expectedMs, this.overridePrepare);
315
+ }
316
+ /**
317
+ * Return a new executor that runs its statements as named server-side prepared
318
+ * statements (`true`) or as unnamed statements (`false`), overriding the context's
319
+ * `preparedStatements` default. Used by `.withPreparedStatements()` on tables and
320
+ * accessors.
321
+ */
322
+ withPreparedStatements(prepare) {
323
+ return new QueryExecutor(this.client, this.options, this.overrideTimeoutMs, this.overrideExpectedMs, prepare);
305
324
  }
306
325
  /** Whether slow-query detection is active (a callback is configured). */
307
326
  get slowQueryEnabled() {
@@ -341,7 +360,7 @@ class QueryExecutor {
341
360
  fireSlowQueryCallback(callback, sql, params, duration, this.expectedExecutionMs, timing.stackHolder);
342
361
  }
343
362
  }
344
- async query(sql, params) {
363
+ async query(sql, params, execution) {
345
364
  const logger = this.options.logger || defaultLogger;
346
365
  const timing = this.beginTiming();
347
366
  if (this.options.logQueries) {
@@ -352,14 +371,12 @@ class QueryExecutor {
352
371
  }
353
372
  }
354
373
  try {
355
- const result = await this.client.query(sql, params, this.buildExecutionOptions());
374
+ const result = await this.client.query(sql, params, this.buildExecutionOptions(execution));
356
375
  this.finishTiming(timing, logger, sql, params);
357
376
  return result;
358
377
  }
359
378
  catch (error) {
360
- if (this.options.logQueries) {
361
- logger(`[SQL Error] ${error instanceof Error ? error.message : String(error)}`, 'error');
362
- }
379
+ this.logFailure(logger, error, sql, params);
363
380
  throw error;
364
381
  }
365
382
  }
@@ -387,9 +404,7 @@ class QueryExecutor {
387
404
  return result;
388
405
  }
389
406
  catch (error) {
390
- if (this.options.logQueries) {
391
- logger(`[SQL Error] ${error instanceof Error ? error.message : String(error)}`, 'error');
392
- }
407
+ this.logFailure(logger, error, sql);
393
408
  throw error;
394
409
  }
395
410
  }
@@ -416,12 +431,36 @@ class QueryExecutor {
416
431
  }
417
432
  }
418
433
  catch (error) {
419
- if (this.options.logQueries) {
420
- logger(`[SQL Error] ${error instanceof Error ? error.message : String(error)}`, 'error');
421
- }
434
+ this.logFailure(logger, error, sql);
422
435
  throw error;
423
436
  }
424
437
  }
438
+ /**
439
+ * Whether these options call for an executor at all — any logging, failure reporting,
440
+ * timing or slow-query duty. A context without any of them talks to the client directly.
441
+ */
442
+ static isNeeded(options) {
443
+ return !!options && !!(options.logQueries
444
+ || options.logFailedQueries
445
+ || options.logExecutionTime
446
+ || options.onQueryTakingTooLong
447
+ || options.preparedStatements);
448
+ }
449
+ /**
450
+ * The `[SQL Error]` line for a failed statement: the driver's message, the statement text
451
+ * and — only while `logParameters` is on — the parameters. Gated on `logFailedQueries`,
452
+ * which defaults to `logQueries` (see {@link QueryOptions.logFailedQueries}).
453
+ */
454
+ logFailure(logger, error, sql, params) {
455
+ if (!(this.options.logFailedQueries ?? this.options.logQueries)) {
456
+ return;
457
+ }
458
+ const message = error instanceof Error ? error.message : String(error);
459
+ const parameters = this.options.logParameters && params && params.length > 0
460
+ ? `\n[Parameters] ${JSON.stringify(params)}`
461
+ : '';
462
+ logger(`[SQL Error] ${message}\n${sql.trim()}${parameters}`, 'error');
463
+ }
425
464
  /**
426
465
  * Get the query options for this executor
427
466
  */
@@ -637,7 +676,7 @@ class TableAccessor {
637
676
  const mergedStrategy = options.collectionStrategy ?? this.collectionStrategy;
638
677
  // Create new executor if logging options are provided
639
678
  let newExecutor = this.executor;
640
- if (options.logQueries || options.logExecutionTime || options.onQueryTakingTooLong) {
679
+ if (QueryExecutor.isNeeded(options)) {
641
680
  newExecutor = new QueryExecutor(this.client, {
642
681
  ...options,
643
682
  collectionStrategy: mergedStrategy,
@@ -661,6 +700,20 @@ class TableAccessor {
661
700
  : new QueryExecutor(this.client, undefined, timeoutMs);
662
701
  return new TableAccessor(this.tableBuilder, this.client, this.schemaRegistry, newExecutor, this.collectionStrategy);
663
702
  }
703
+ /**
704
+ * Run every query started from the returned accessor as a named server-side prepared
705
+ * statement (`true`) or as an unnamed statement (`false`), overriding the context's
706
+ * `preparedStatements` default. See {@link QueryOptions.preparedStatements}.
707
+ *
708
+ * @example
709
+ * await db.users.withPreparedStatements(false).where(u => gt(u.id, 0)).toList();
710
+ */
711
+ withPreparedStatements(prepare) {
712
+ const newExecutor = this.executor
713
+ ? this.executor.withPreparedStatements(prepare)
714
+ : new QueryExecutor(this.client, undefined, undefined, undefined, prepare);
715
+ return new TableAccessor(this.tableBuilder, this.client, this.schemaRegistry, newExecutor, this.collectionStrategy);
716
+ }
664
717
  /**
665
718
  * Mark queries started from the returned accessor as expected to finish within
666
719
  * `expectedMs` (ms). If a query runs longer, the context's
@@ -984,7 +1037,7 @@ class DataContext {
984
1037
  this.client = client;
985
1038
  this.queryOptions = queryOptions;
986
1039
  // Create executor if logging is enabled
987
- if (queryOptions?.logQueries || queryOptions?.logExecutionTime || queryOptions?.onQueryTakingTooLong) {
1040
+ if (QueryExecutor.isNeeded(queryOptions)) {
988
1041
  this.executor = new QueryExecutor(client, queryOptions);
989
1042
  }
990
1043
  this.initializeSchema(schema);
@@ -1141,7 +1194,7 @@ class DataContext {
1141
1194
  : this.queryOptions;
1142
1195
  txContext.queryOptions = effectiveOptions;
1143
1196
  // Create executor for the transactional client if logging is enabled
1144
- if (effectiveOptions?.logQueries || effectiveOptions?.logExecutionTime || effectiveOptions?.onQueryTakingTooLong) {
1197
+ if (QueryExecutor.isNeeded(effectiveOptions)) {
1145
1198
  txContext.executor = new QueryExecutor(txClient, effectiveOptions);
1146
1199
  }
1147
1200
  // Create fresh table accessors bound to the transactional client
@@ -1484,7 +1537,7 @@ class DbEntityTable {
1484
1537
  const mergedOptions = { ...originalOptions, ...options };
1485
1538
  // Create new executor if logging options are provided
1486
1539
  let newExecutor = originalContext.executor;
1487
- if (mergedOptions.logQueries || mergedOptions.logExecutionTime || mergedOptions.onQueryTakingTooLong) {
1540
+ if (QueryExecutor.isNeeded(mergedOptions)) {
1488
1541
  newExecutor = new QueryExecutor(originalContext.client, mergedOptions);
1489
1542
  }
1490
1543
  // Create a proxy context that overrides queryOptions, executor, and getTable
@@ -1532,6 +1585,19 @@ class DbEntityTable {
1532
1585
  withTimeout(timeoutMs) {
1533
1586
  return this._deriveWithExecutor((current, client) => current ? current.withTimeout(timeoutMs) : new QueryExecutor(client, undefined, timeoutMs));
1534
1587
  }
1588
+ /**
1589
+ * Run every query and CRUD operation started from the returned table as a named
1590
+ * server-side prepared statement (`true`) or as an unnamed statement (`false`),
1591
+ * overriding the context's `preparedStatements` default. Use `false` to keep a wide
1592
+ * analytical query on custom plans inside a prepared context, `true` to prepare a
1593
+ * single hot lookup on an unprepared one. See {@link QueryOptions.preparedStatements}.
1594
+ *
1595
+ * @example
1596
+ * await db.products.withPreparedStatements(false).where(p => eq(p.active, true)).toList();
1597
+ */
1598
+ withPreparedStatements(prepare) {
1599
+ return this._deriveWithExecutor((current, client) => current ? current.withPreparedStatements(prepare) : new QueryExecutor(client, undefined, undefined, undefined, prepare));
1600
+ }
1535
1601
  /**
1536
1602
  * Mark queries started from the returned table as expected to finish within
1537
1603
  * `expectedMs` (ms). If a query runs longer, the context's
@@ -2168,7 +2234,8 @@ ${parentSql}
2168
2234
  '__iwc_parent__',
2169
2235
  ]]),
2170
2236
  });
2171
- const result = executor ? await executor.query(built.sql, built.params) : await client.query(built.sql, built.params);
2237
+ // Per-call VALUES list unique text never a prepared statement (see QueryOptions.preparedStatements).
2238
+ const result = executor ? await executor.query(built.sql, built.params, { prepare: false }) : await client.query(built.sql, built.params);
2172
2239
  rawRows = result.rows;
2173
2240
  mapChildren = stripped => childTable.mapReturningResultsWithNavigation(stripped, navigationInfo.navigationFields, built.nestedPaths);
2174
2241
  }
@@ -2184,7 +2251,8 @@ RETURNING ${returningClause.sql}${pkExtra}
2184
2251
  SELECT "__mutation__".*, ${extraSelects.join(', ')}
2185
2252
  FROM "__mutation__"
2186
2253
  ${extraJoins.join('\n')}${orderBy}`;
2187
- const result = executor ? await executor.query(sql, params) : await client.query(sql, params);
2254
+ // Per-call VALUES list unique text never a prepared statement (see QueryOptions.preparedStatements).
2255
+ const result = executor ? await executor.query(sql, params, { prepare: false }) : await client.query(sql, params);
2188
2256
  rawRows = result.rows;
2189
2257
  mapChildren = stripped => childTable.mapReturningResults(stripped.map(({ __iwc_child_pk__: _pk, ...rest }) => rest), returningClause.aliasToProperty);
2190
2258
  }
@@ -2361,7 +2429,8 @@ SELECT "__mutation__".*, ${parentJoinSelects.join(', ')}
2361
2429
  FROM "__mutation__"
2362
2430
  JOIN "__ibwc_pord__" "__ibwc_pj__" ON "__ibwc_pj__"."${parentPkDbName}" = "__mutation__"."__ibwc_child_fk__"
2363
2431
  ORDER BY "__mutation__"."__ibwc_child_pk__"`;
2364
- const result = executor ? await executor.query(sql, params) : await client.query(sql, params);
2432
+ // Per-call VALUES lists unique text never a prepared statement (see QueryOptions.preparedStatements).
2433
+ const result = executor ? await executor.query(sql, params, { prepare: false }) : await client.query(sql, params);
2365
2434
  const rawRows = result.rows;
2366
2435
  const parentsByOrd = new Map();
2367
2436
  const strippedRows = [];