linkgress-orm 0.4.33 → 0.4.35
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/database/database-client.interface.d.ts +37 -4
- package/dist/database/database-client.interface.d.ts.map +1 -1
- package/dist/database/database-client.interface.js +27 -3
- package/dist/database/database-client.interface.js.map +1 -1
- package/dist/database/index.d.ts +2 -1
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/index.js +2 -1
- package/dist/database/index.js.map +1 -1
- package/dist/database/postgres-client.d.ts +12 -1
- package/dist/database/postgres-client.d.ts.map +1 -1
- package/dist/database/postgres-client.js +151 -21
- package/dist/database/postgres-client.js.map +1 -1
- package/dist/database/types.d.ts +12 -0
- package/dist/database/types.d.ts.map +1 -1
- package/dist/entity/db-context.d.ts +221 -8
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +335 -59
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/query/grouped-query.d.ts +38 -1
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +67 -0
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/join-builder.d.ts +14 -1
- package/dist/query/join-builder.d.ts.map +1 -1
- package/dist/query/join-builder.js +23 -0
- package/dist/query/join-builder.js.map +1 -1
- package/dist/query/query-builder.d.ts +33 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +51 -0
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/union-builder.d.ts +14 -1
- package/dist/query/union-builder.d.ts.map +1 -1
- package/dist/query/union-builder.js +23 -0
- package/dist/query/union-builder.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DatabaseContext = exports.DbEntityTable = exports.EntityInsertBuilder = exports.DataContext = exports.TableAccessor = exports.InsertBuilder = exports.QueryExecutor = exports.TimeTracer = void 0;
|
|
4
|
+
exports.defaultLogger = defaultLogger;
|
|
4
5
|
const database_client_interface_1 = require("../database/database-client.interface");
|
|
5
6
|
const entity_base_1 = require("./entity-base");
|
|
6
7
|
const model_config_1 = require("./model-config");
|
|
@@ -9,6 +10,113 @@ const query_builder_1 = require("../query/query-builder");
|
|
|
9
10
|
const db_schema_manager_1 = require("../migration/db-schema-manager");
|
|
10
11
|
const sequence_builder_1 = require("../schema/sequence-builder");
|
|
11
12
|
const sql_utils_1 = require("../query/sql-utils");
|
|
13
|
+
/**
|
|
14
|
+
* Default logger used when no custom `logger` is provided. Routes by section to
|
|
15
|
+
* the appropriate console method.
|
|
16
|
+
*/
|
|
17
|
+
function defaultLogger(message, section) {
|
|
18
|
+
if (section === 'error') {
|
|
19
|
+
console.error(message);
|
|
20
|
+
}
|
|
21
|
+
else if (section === 'warn') {
|
|
22
|
+
console.warn(message);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.log(message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Basenames (without extension) of the linkgress source files that can appear in
|
|
30
|
+
* a query's call stack between the public terminal method (`.toList()` etc.) and
|
|
31
|
+
* the user's code. Internal frames are stripped from {@link SlowQueryInfo.stack}
|
|
32
|
+
* by matching these file IDENTITIES — not an install path — so it works wherever
|
|
33
|
+
* the package lives (node_modules, a monorepo, ts-node/ts-jest, a global link).
|
|
34
|
+
*/
|
|
35
|
+
const LINKGRESS_INTERNAL_FILES = new Set([
|
|
36
|
+
'db-context',
|
|
37
|
+
'query-builder',
|
|
38
|
+
'grouped-query',
|
|
39
|
+
'union-builder',
|
|
40
|
+
'join-builder',
|
|
41
|
+
'future-query',
|
|
42
|
+
'prepared-query',
|
|
43
|
+
]);
|
|
44
|
+
/** Extract the source-file basename (without extension) from a V8 stack frame line. */
|
|
45
|
+
function stackFrameFile(line) {
|
|
46
|
+
// Matches the "<file>:<line>:<col>" tail, e.g.
|
|
47
|
+
// " at SelectQueryBuilder.toList (C:\repo\src\query\query-builder.ts:2019:17)"
|
|
48
|
+
// " at /repo/dist/entity/db-context.js:574:22"
|
|
49
|
+
const match = line.match(/([^()\s]+):\d+:\d+\)?\s*$/);
|
|
50
|
+
if (!match)
|
|
51
|
+
return undefined;
|
|
52
|
+
const base = match[1].split(/[\\/]/).pop();
|
|
53
|
+
return base ? base.replace(/\.[cm]?[jt]s$/, '') : undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Capture a stack-trace holder cheaply — the `.stack` string is only formatted
|
|
57
|
+
* on access, so this is paid for fully only when the query turns out slow. Call
|
|
58
|
+
* it from within the user's synchronous call chain (before awaiting), so the
|
|
59
|
+
* user's frames are present. Temporarily raises the capture depth so deep user
|
|
60
|
+
* frames survive the internal frames sitting above them.
|
|
61
|
+
*/
|
|
62
|
+
function captureStackHolder() {
|
|
63
|
+
const holder = {};
|
|
64
|
+
const previousLimit = Error.stackTraceLimit;
|
|
65
|
+
Error.stackTraceLimit = 50;
|
|
66
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
67
|
+
Error.captureStackTrace(holder, captureStackHolder);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
holder.stack = new Error().stack;
|
|
71
|
+
}
|
|
72
|
+
Error.stackTraceLimit = previousLimit;
|
|
73
|
+
return holder;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Turn a captured stack into the user-facing call stack: drop the leading run of
|
|
77
|
+
* linkgress-internal frames (identified by source-file basename, so it is robust
|
|
78
|
+
* to wherever the package is installed and catches anonymous internal closures
|
|
79
|
+
* too), leaving the user's terminal-method call site as the first frame. Falls
|
|
80
|
+
* back to all frames if filtering would leave nothing.
|
|
81
|
+
*/
|
|
82
|
+
function extractUserStack(holder) {
|
|
83
|
+
const raw = holder?.stack;
|
|
84
|
+
if (!raw)
|
|
85
|
+
return '';
|
|
86
|
+
const frames = raw.split('\n').filter(line => line.trim().startsWith('at '));
|
|
87
|
+
let i = 0;
|
|
88
|
+
while (i < frames.length) {
|
|
89
|
+
const file = stackFrameFile(frames[i]);
|
|
90
|
+
if (file && LINKGRESS_INTERNAL_FILES.has(file)) {
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const userFrames = frames.slice(i);
|
|
98
|
+
return (userFrames.length > 0 ? userFrames : frames).join('\n');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Invoke the slow-query callback defensively: resolve the user stack lazily and
|
|
102
|
+
* swallow any error from the callback so a diagnostic notice can never break the
|
|
103
|
+
* query that triggered it.
|
|
104
|
+
*/
|
|
105
|
+
function fireSlowQueryCallback(callback, sql, params, durationMs, thresholdMs, stackHolder) {
|
|
106
|
+
let stack = '';
|
|
107
|
+
try {
|
|
108
|
+
stack = extractUserStack(stackHolder);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* ignore stack-extraction failures */
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
callback({ sql, params, durationMs, thresholdMs, stack });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* swallow — the slow-query notice must never affect the query */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
12
120
|
/**
|
|
13
121
|
* Time tracer utility for measuring query phases
|
|
14
122
|
*/
|
|
@@ -107,28 +215,28 @@ class TimeTracer {
|
|
|
107
215
|
if (!this.enabled)
|
|
108
216
|
return;
|
|
109
217
|
const trace = this.getTrace(rowCount);
|
|
110
|
-
const log = this.logger ||
|
|
111
|
-
log('\n[Time Trace Summary]', '
|
|
112
|
-
log(` Total: ${trace.totalMs.toFixed(2)}ms`, '
|
|
218
|
+
const log = this.logger || defaultLogger;
|
|
219
|
+
log('\n[Time Trace Summary]', 'timing');
|
|
220
|
+
log(` Total: ${trace.totalMs.toFixed(2)}ms`, 'timing');
|
|
113
221
|
if (trace.phases.queryBuild !== undefined) {
|
|
114
|
-
log(` Query Build: ${trace.phases.queryBuild.toFixed(2)}ms`, '
|
|
222
|
+
log(` Query Build: ${trace.phases.queryBuild.toFixed(2)}ms`, 'timing');
|
|
115
223
|
}
|
|
116
224
|
if (trace.phases.queryExecution !== undefined) {
|
|
117
|
-
log(` Query Execution: ${trace.phases.queryExecution.toFixed(2)}ms`, '
|
|
225
|
+
log(` Query Execution: ${trace.phases.queryExecution.toFixed(2)}ms`, 'timing');
|
|
118
226
|
}
|
|
119
227
|
if (trace.phases.resultProcessing !== undefined) {
|
|
120
|
-
log(` Result Processing: ${trace.phases.resultProcessing.toFixed(2)}ms`, '
|
|
228
|
+
log(` Result Processing: ${trace.phases.resultProcessing.toFixed(2)}ms`, 'timing');
|
|
121
229
|
}
|
|
122
230
|
if (rowCount !== undefined) {
|
|
123
|
-
log(` Rows: ${rowCount}`, '
|
|
231
|
+
log(` Rows: ${rowCount}`, 'timing');
|
|
124
232
|
}
|
|
125
233
|
// Log detailed entries if there are any significant operations
|
|
126
234
|
const significantEntries = this.entries.filter(e => e.durationMs > 0.1);
|
|
127
235
|
if (significantEntries.length > 0) {
|
|
128
|
-
log('\n[Detailed Trace]', '
|
|
236
|
+
log('\n[Detailed Trace]', 'timing');
|
|
129
237
|
for (const entry of significantEntries) {
|
|
130
238
|
const details = entry.details ? ` (${JSON.stringify(entry.details)})` : '';
|
|
131
|
-
log(` [${entry.phase}] ${entry.operation}: ${entry.durationMs.toFixed(2)}ms${details}`, '
|
|
239
|
+
log(` [${entry.phase}] ${entry.operation}: ${entry.durationMs.toFixed(2)}ms${details}`, 'timing');
|
|
132
240
|
}
|
|
133
241
|
}
|
|
134
242
|
}
|
|
@@ -138,30 +246,103 @@ exports.TimeTracer = TimeTracer;
|
|
|
138
246
|
* Query executor with optional logging
|
|
139
247
|
*/
|
|
140
248
|
class QueryExecutor {
|
|
141
|
-
constructor(client, options = {}
|
|
249
|
+
constructor(client, options = {},
|
|
250
|
+
/**
|
|
251
|
+
* Per-query timeout override (ms) set via `.withTimeout()`. Threaded down to
|
|
252
|
+
* the driver as `QueryExecutionOptions.timeoutMs`. `undefined` means no
|
|
253
|
+
* override (the connection-level default, if any, applies).
|
|
254
|
+
*/
|
|
255
|
+
overrideTimeoutMs,
|
|
256
|
+
/**
|
|
257
|
+
* Per-query "expected execution time" override (ms) set via
|
|
258
|
+
* `.expectedExecutionTime()`. If the query runs longer than this,
|
|
259
|
+
* `onQueryTakingTooLong` fires. `undefined` means use the context default.
|
|
260
|
+
*/
|
|
261
|
+
overrideExpectedMs) {
|
|
142
262
|
this.client = client;
|
|
143
263
|
this.options = options;
|
|
264
|
+
this.overrideTimeoutMs = overrideTimeoutMs;
|
|
265
|
+
this.overrideExpectedMs = overrideExpectedMs;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Build the per-query execution options (binary protocol + timeout override),
|
|
269
|
+
* or `undefined` when neither is set so the driver takes its fast path.
|
|
270
|
+
*/
|
|
271
|
+
buildExecutionOptions() {
|
|
272
|
+
if (!this.options.useBinaryProtocol && this.overrideTimeoutMs === undefined) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
useBinaryProtocol: this.options.useBinaryProtocol,
|
|
277
|
+
timeoutMs: this.overrideTimeoutMs,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Return a new executor sharing this one's client and options but applying the
|
|
282
|
+
* given per-query timeout override (ms). Pass `0` to disable the timeout for
|
|
283
|
+
* the derived executor. Used by `.withTimeout()` on the query builders.
|
|
284
|
+
*/
|
|
285
|
+
withTimeout(timeoutMs) {
|
|
286
|
+
return new QueryExecutor(this.client, this.options, timeoutMs, this.overrideExpectedMs);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Return a new executor that flags this query as expected to finish within
|
|
290
|
+
* `expectedMs` — if it runs longer, `onQueryTakingTooLong` fires. Used by
|
|
291
|
+
* `.expectedExecutionTime()` on the query builders.
|
|
292
|
+
*/
|
|
293
|
+
withExpectedExecutionTime(expectedMs) {
|
|
294
|
+
return new QueryExecutor(this.client, this.options, this.overrideTimeoutMs, expectedMs);
|
|
295
|
+
}
|
|
296
|
+
/** Whether slow-query detection is active (a callback is configured). */
|
|
297
|
+
get slowQueryEnabled() {
|
|
298
|
+
return typeof this.options.onQueryTakingTooLong === 'function';
|
|
299
|
+
}
|
|
300
|
+
/** Effective "too long" threshold for the current query (ms). */
|
|
301
|
+
get expectedExecutionMs() {
|
|
302
|
+
return this.overrideExpectedMs ?? this.options.longRunningQueryThreshold ?? 10000;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Begin timing/stack capture for a query. Returns `undefined` when neither
|
|
306
|
+
* execution-time logging nor slow-query detection is active (zero overhead).
|
|
307
|
+
* The stack is captured here — synchronously, inside the caller's call chain —
|
|
308
|
+
* so the slow-query callback can report the user's code, not an async frame.
|
|
309
|
+
*/
|
|
310
|
+
beginTiming() {
|
|
311
|
+
if (!this.options.logExecutionTime && !this.slowQueryEnabled) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const stackHolder = this.slowQueryEnabled ? captureStackHolder() : undefined;
|
|
315
|
+
return { startTime: performance.now(), stackHolder };
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Finish timing: log execution time (if enabled) and fire the slow-query
|
|
319
|
+
* callback (if enabled and the expected threshold was exceeded).
|
|
320
|
+
*/
|
|
321
|
+
finishTiming(timing, logger, sql, params) {
|
|
322
|
+
if (!timing)
|
|
323
|
+
return;
|
|
324
|
+
const duration = performance.now() - timing.startTime;
|
|
325
|
+
if (this.options.logExecutionTime) {
|
|
326
|
+
logger(`[Execution Time] ${duration.toFixed(2)}ms`, 'timing');
|
|
327
|
+
}
|
|
328
|
+
const callback = this.options.onQueryTakingTooLong;
|
|
329
|
+
if (callback && duration > this.expectedExecutionMs) {
|
|
330
|
+
fireSlowQueryCallback(callback, sql, params, duration, this.expectedExecutionMs, timing.stackHolder);
|
|
331
|
+
}
|
|
144
332
|
}
|
|
145
333
|
async query(sql, params) {
|
|
146
|
-
const logger = this.options.logger ||
|
|
147
|
-
const
|
|
334
|
+
const logger = this.options.logger || defaultLogger;
|
|
335
|
+
const timing = this.beginTiming();
|
|
148
336
|
if (this.options.logQueries) {
|
|
149
|
-
logger(`\n[SQL Query]`, '
|
|
150
|
-
logger(sql.trim(), '
|
|
337
|
+
logger(`\n[SQL Query]`, 'sql');
|
|
338
|
+
logger(sql.trim(), 'sql');
|
|
151
339
|
if (this.options.logParameters && params && params.length > 0) {
|
|
152
|
-
logger(`[Parameters] ${JSON.stringify(params)}`, '
|
|
340
|
+
logger(`[Parameters] ${JSON.stringify(params)}`, 'params');
|
|
153
341
|
}
|
|
154
342
|
}
|
|
155
343
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
? { useBinaryProtocol: true }
|
|
159
|
-
: undefined;
|
|
160
|
-
const result = await this.client.query(sql, params, queryOptions);
|
|
161
|
-
if (this.options.logExecutionTime) {
|
|
162
|
-
const duration = (performance.now() - startTime).toFixed(2);
|
|
163
|
-
logger(`[Execution Time] ${duration}ms`, 'debug');
|
|
164
|
-
}
|
|
344
|
+
const result = await this.client.query(sql, params, this.buildExecutionOptions());
|
|
345
|
+
this.finishTiming(timing, logger, sql, params);
|
|
165
346
|
return result;
|
|
166
347
|
}
|
|
167
348
|
catch (error) {
|
|
@@ -176,31 +357,23 @@ class QueryExecutor {
|
|
|
176
357
|
* Only available for clients that support it (e.g., PostgresClient)
|
|
177
358
|
*/
|
|
178
359
|
async querySimple(sql) {
|
|
179
|
-
const logger = this.options.logger ||
|
|
180
|
-
const
|
|
360
|
+
const logger = this.options.logger || defaultLogger;
|
|
361
|
+
const timing = this.beginTiming();
|
|
181
362
|
if (this.options.logQueries) {
|
|
182
|
-
logger(`\n[SQL Query - Multi-Statement]`, '
|
|
183
|
-
logger(sql.trim(), '
|
|
363
|
+
logger(`\n[SQL Query - Multi-Statement]`, 'sql');
|
|
364
|
+
logger(sql.trim(), 'sql');
|
|
184
365
|
}
|
|
185
366
|
try {
|
|
186
|
-
|
|
367
|
+
let result;
|
|
187
368
|
if ('querySimple' in this.client && typeof this.client.querySimple === 'function') {
|
|
188
|
-
|
|
189
|
-
if (this.options.logExecutionTime) {
|
|
190
|
-
const duration = (performance.now() - startTime).toFixed(2);
|
|
191
|
-
logger(`[Execution Time] ${duration}ms`, 'debug');
|
|
192
|
-
}
|
|
193
|
-
return result;
|
|
369
|
+
result = await this.client.querySimple(sql);
|
|
194
370
|
}
|
|
195
371
|
else {
|
|
196
372
|
// Fallback to regular query
|
|
197
|
-
|
|
198
|
-
if (this.options.logExecutionTime) {
|
|
199
|
-
const duration = (performance.now() - startTime).toFixed(2);
|
|
200
|
-
logger(`[Execution Time] ${duration}ms`, 'debug');
|
|
201
|
-
}
|
|
202
|
-
return result;
|
|
373
|
+
result = await this.client.query(sql, []);
|
|
203
374
|
}
|
|
375
|
+
this.finishTiming(timing, logger, sql);
|
|
376
|
+
return result;
|
|
204
377
|
}
|
|
205
378
|
catch (error) {
|
|
206
379
|
if (this.options.logQueries) {
|
|
@@ -214,20 +387,17 @@ class QueryExecutor {
|
|
|
214
387
|
* Only available for PostgresClient
|
|
215
388
|
*/
|
|
216
389
|
async querySimpleMulti(sql) {
|
|
217
|
-
const logger = this.options.logger ||
|
|
218
|
-
const
|
|
390
|
+
const logger = this.options.logger || defaultLogger;
|
|
391
|
+
const timing = this.beginTiming();
|
|
219
392
|
if (this.options.logQueries) {
|
|
220
|
-
logger(`\n[SQL Query - Fully Optimized Multi-Statement]`, '
|
|
221
|
-
logger(sql.trim(), '
|
|
393
|
+
logger(`\n[SQL Query - Fully Optimized Multi-Statement]`, 'sql');
|
|
394
|
+
logger(sql.trim(), 'sql');
|
|
222
395
|
}
|
|
223
396
|
try {
|
|
224
397
|
// Check if client has querySimpleMulti method
|
|
225
398
|
if ('querySimpleMulti' in this.client && typeof this.client.querySimpleMulti === 'function') {
|
|
226
399
|
const results = await this.client.querySimpleMulti(sql);
|
|
227
|
-
|
|
228
|
-
const duration = (performance.now() - startTime).toFixed(2);
|
|
229
|
-
logger(`[Execution Time] ${duration}ms`, 'debug');
|
|
230
|
-
}
|
|
400
|
+
this.finishTiming(timing, logger, sql);
|
|
231
401
|
return results;
|
|
232
402
|
}
|
|
233
403
|
else {
|
|
@@ -456,7 +626,7 @@ class TableAccessor {
|
|
|
456
626
|
const mergedStrategy = options.collectionStrategy ?? this.collectionStrategy;
|
|
457
627
|
// Create new executor if logging options are provided
|
|
458
628
|
let newExecutor = this.executor;
|
|
459
|
-
if (options.logQueries || options.logExecutionTime) {
|
|
629
|
+
if (options.logQueries || options.logExecutionTime || options.onQueryTakingTooLong) {
|
|
460
630
|
newExecutor = new QueryExecutor(this.client, {
|
|
461
631
|
...options,
|
|
462
632
|
collectionStrategy: mergedStrategy,
|
|
@@ -465,6 +635,33 @@ class TableAccessor {
|
|
|
465
635
|
// Return new instance with updated options
|
|
466
636
|
return new TableAccessor(this.tableBuilder, this.client, this.schemaRegistry, newExecutor, mergedStrategy);
|
|
467
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* Set a per-query timeout (ms) applied to every query and CRUD operation
|
|
640
|
+
* started from the returned accessor. Each such query is wrapped individually
|
|
641
|
+
* (`SET LOCAL statement_timeout`); pass `0` to disable. Overrides the
|
|
642
|
+
* connection-level default. On timeout a `QueryTimeoutError` is thrown.
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* await db.users.withTimeout(5000).where(u => gt(u.id, 0)).toList();
|
|
646
|
+
*/
|
|
647
|
+
withTimeout(timeoutMs) {
|
|
648
|
+
const newExecutor = this.executor
|
|
649
|
+
? this.executor.withTimeout(timeoutMs)
|
|
650
|
+
: new QueryExecutor(this.client, undefined, timeoutMs);
|
|
651
|
+
return new TableAccessor(this.tableBuilder, this.client, this.schemaRegistry, newExecutor, this.collectionStrategy);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Mark queries started from the returned accessor as expected to finish within
|
|
655
|
+
* `expectedMs` (ms). If a query runs longer, the context's
|
|
656
|
+
* `onQueryTakingTooLong` callback fires — the query is NOT cancelled (use
|
|
657
|
+
* `.withTimeout()` for that). Overrides the context's `longRunningQueryThreshold`.
|
|
658
|
+
*/
|
|
659
|
+
expectedExecutionTime(expectedMs) {
|
|
660
|
+
const newExecutor = this.executor
|
|
661
|
+
? this.executor.withExpectedExecutionTime(expectedMs)
|
|
662
|
+
: new QueryExecutor(this.client, undefined, undefined, expectedMs);
|
|
663
|
+
return new TableAccessor(this.tableBuilder, this.client, this.schemaRegistry, newExecutor, this.collectionStrategy);
|
|
664
|
+
}
|
|
468
665
|
/**
|
|
469
666
|
* Start a select query with automatic type inference
|
|
470
667
|
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
@@ -776,7 +973,7 @@ class DataContext {
|
|
|
776
973
|
this.client = client;
|
|
777
974
|
this.queryOptions = queryOptions;
|
|
778
975
|
// Create executor if logging is enabled
|
|
779
|
-
if (queryOptions?.logQueries || queryOptions?.logExecutionTime) {
|
|
976
|
+
if (queryOptions?.logQueries || queryOptions?.logExecutionTime || queryOptions?.onQueryTakingTooLong) {
|
|
780
977
|
this.executor = new QueryExecutor(client, queryOptions);
|
|
781
978
|
}
|
|
782
979
|
this.initializeSchema(schema);
|
|
@@ -853,12 +1050,26 @@ class DataContext {
|
|
|
853
1050
|
* Creates a scoped transactional context to avoid race conditions with concurrent transactions.
|
|
854
1051
|
* Each transaction gets its own isolated context instance with fresh table accessors.
|
|
855
1052
|
*/
|
|
856
|
-
async transaction(fn) {
|
|
1053
|
+
async transaction(fn, options) {
|
|
857
1054
|
return await this.client.transaction(async (queryFn) => {
|
|
858
1055
|
// Create a transactional client that routes all queries through the transaction
|
|
859
1056
|
const txClient = new database_client_interface_1.TransactionalClient(queryFn, this.client);
|
|
1057
|
+
// Raise the per-statement timeout for the WHOLE transaction up-front. `SET LOCAL`
|
|
1058
|
+
// is transaction-scoped (auto-resets at COMMIT/ROLLBACK) and applies to every
|
|
1059
|
+
// subsequent statement — including bulk inserts/upserts that don't expose a
|
|
1060
|
+
// per-query `.withTimeout()`. Clamp to a safe non-negative integer (it is
|
|
1061
|
+
// inlined into the SQL); `0` disables the timeout for the transaction.
|
|
1062
|
+
if (options?.timeoutMs !== undefined) {
|
|
1063
|
+
const ms = Math.max(0, Math.floor(Number(options.timeoutMs) || 0));
|
|
1064
|
+
await txClient.query(`SET LOCAL statement_timeout = ${ms}`);
|
|
1065
|
+
}
|
|
1066
|
+
// Within the transaction, raise the slow-query "expected" threshold so a
|
|
1067
|
+
// deliberately long unit of work doesn't trip the global threshold. Defaults
|
|
1068
|
+
// to the transaction timeout when not given explicitly.
|
|
1069
|
+
const expectedMs = options?.expectedExecutionMs ?? options?.timeoutMs;
|
|
1070
|
+
const optionsOverride = expectedMs !== undefined ? { longRunningQueryThreshold: expectedMs } : undefined;
|
|
860
1071
|
// Create an isolated transactional context instead of mutating this.client
|
|
861
|
-
const txContext = this.createTransactionalContext(txClient);
|
|
1072
|
+
const txContext = this.createTransactionalContext(txClient, optionsOverride);
|
|
862
1073
|
return await fn(txContext);
|
|
863
1074
|
});
|
|
864
1075
|
}
|
|
@@ -867,22 +1078,27 @@ class DataContext {
|
|
|
867
1078
|
* Used internally for transaction isolation to prevent race conditions
|
|
868
1079
|
* when multiple transactions run concurrently.
|
|
869
1080
|
*/
|
|
870
|
-
createTransactionalContext(txClient) {
|
|
1081
|
+
createTransactionalContext(txClient, queryOptionsOverride) {
|
|
871
1082
|
// Create new instance preserving the prototype chain (including subclass methods/getters)
|
|
872
1083
|
const txContext = Object.create(Object.getPrototypeOf(this));
|
|
873
1084
|
// Set up the transactional client
|
|
874
1085
|
txContext.client = txClient;
|
|
875
1086
|
// Share read-only schema registry
|
|
876
1087
|
txContext.schemaRegistry = this.schemaRegistry;
|
|
877
|
-
|
|
1088
|
+
// Merge any per-transaction option override (e.g. a raised slow-query threshold)
|
|
1089
|
+
// over the context's base options so the transaction's executor + accessors use it.
|
|
1090
|
+
const effectiveOptions = queryOptionsOverride
|
|
1091
|
+
? { ...(this.queryOptions ?? {}), ...queryOptionsOverride }
|
|
1092
|
+
: this.queryOptions;
|
|
1093
|
+
txContext.queryOptions = effectiveOptions;
|
|
878
1094
|
// Create executor for the transactional client if logging is enabled
|
|
879
|
-
if (
|
|
880
|
-
txContext.executor = new QueryExecutor(txClient,
|
|
1095
|
+
if (effectiveOptions?.logQueries || effectiveOptions?.logExecutionTime || effectiveOptions?.onQueryTakingTooLong) {
|
|
1096
|
+
txContext.executor = new QueryExecutor(txClient, effectiveOptions);
|
|
881
1097
|
}
|
|
882
1098
|
// Create fresh table accessors bound to the transactional client
|
|
883
1099
|
txContext.tableAccessors = new Map();
|
|
884
1100
|
for (const [key, accessor] of this.tableAccessors) {
|
|
885
|
-
const newAccessor = new TableAccessor(accessor.tableBuilder, txClient, this.schemaRegistry, txContext.executor,
|
|
1101
|
+
const newAccessor = new TableAccessor(accessor.tableBuilder, txClient, this.schemaRegistry, txContext.executor, effectiveOptions?.collectionStrategy);
|
|
886
1102
|
txContext.tableAccessors.set(key, newAccessor);
|
|
887
1103
|
// Only attach as direct property if not a getter on the prototype
|
|
888
1104
|
// (DatabaseContext subclasses use getters like `get users()` that call this.table())
|
|
@@ -1219,7 +1435,7 @@ class DbEntityTable {
|
|
|
1219
1435
|
const mergedOptions = { ...originalOptions, ...options };
|
|
1220
1436
|
// Create new executor if logging options are provided
|
|
1221
1437
|
let newExecutor = originalContext.executor;
|
|
1222
|
-
if (mergedOptions.logQueries || mergedOptions.logExecutionTime) {
|
|
1438
|
+
if (mergedOptions.logQueries || mergedOptions.logExecutionTime || mergedOptions.onQueryTakingTooLong) {
|
|
1223
1439
|
newExecutor = new QueryExecutor(originalContext.client, mergedOptions);
|
|
1224
1440
|
}
|
|
1225
1441
|
// Create a proxy context that overrides queryOptions, executor, and getTable
|
|
@@ -1255,6 +1471,66 @@ class DbEntityTable {
|
|
|
1255
1471
|
// Return new instance with proxy context
|
|
1256
1472
|
return new DbEntityTable(proxyContext, this.tableName, this.tableBuilder);
|
|
1257
1473
|
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Set a per-query timeout (ms) applied to every query and CRUD operation
|
|
1476
|
+
* started from the returned table. Each such query is wrapped individually
|
|
1477
|
+
* (`SET LOCAL statement_timeout`); pass `0` to disable. Overrides the
|
|
1478
|
+
* connection-level default. On timeout a `QueryTimeoutError` is thrown.
|
|
1479
|
+
*
|
|
1480
|
+
* @example
|
|
1481
|
+
* await db.users.withTimeout(5000).where(u => gt(u.id, 0)).toList();
|
|
1482
|
+
*/
|
|
1483
|
+
withTimeout(timeoutMs) {
|
|
1484
|
+
return this._deriveWithExecutor((current, client) => current ? current.withTimeout(timeoutMs) : new QueryExecutor(client, undefined, timeoutMs));
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Mark queries started from the returned table as expected to finish within
|
|
1488
|
+
* `expectedMs` (ms). If a query runs longer, the context's
|
|
1489
|
+
* `onQueryTakingTooLong` callback fires — the query is NOT cancelled (use
|
|
1490
|
+
* `.withTimeout()` for that). Overrides the context's `longRunningQueryThreshold`.
|
|
1491
|
+
*
|
|
1492
|
+
* @example
|
|
1493
|
+
* await db.users.expectedExecutionTime(2000).where(u => gt(u.id, 0)).toList();
|
|
1494
|
+
*/
|
|
1495
|
+
expectedExecutionTime(expectedMs) {
|
|
1496
|
+
return this._deriveWithExecutor((current, client) => current ? current.withExpectedExecutionTime(expectedMs) : new QueryExecutor(client, undefined, undefined, expectedMs));
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Build a derived table whose context surfaces a transformed executor (via a
|
|
1500
|
+
* proxy context, threaded into this table's accessor). Shared by
|
|
1501
|
+
* `.withTimeout()` and `.expectedExecutionTime()`.
|
|
1502
|
+
* @internal
|
|
1503
|
+
*/
|
|
1504
|
+
_deriveWithExecutor(makeExecutor) {
|
|
1505
|
+
const originalContext = this.context;
|
|
1506
|
+
const tableName = this.tableName;
|
|
1507
|
+
const client = originalContext.client;
|
|
1508
|
+
const newExecutor = makeExecutor(originalContext.executor, client);
|
|
1509
|
+
const collectionStrategy = originalContext.queryOptions?.collectionStrategy;
|
|
1510
|
+
const proxyContext = new Proxy(originalContext, {
|
|
1511
|
+
get(target, prop) {
|
|
1512
|
+
if (prop === 'executor') {
|
|
1513
|
+
return newExecutor;
|
|
1514
|
+
}
|
|
1515
|
+
if (prop === 'getTable') {
|
|
1516
|
+
return (name) => {
|
|
1517
|
+
const originalAccessor = target.tableAccessors.get(name);
|
|
1518
|
+
if (!originalAccessor) {
|
|
1519
|
+
return target.getTable(name);
|
|
1520
|
+
}
|
|
1521
|
+
if (name === tableName) {
|
|
1522
|
+
const schemaRegistry = target.schemaRegistry;
|
|
1523
|
+
const originalTableBuilder = originalAccessor.tableBuilder;
|
|
1524
|
+
return new TableAccessor(originalTableBuilder, client, schemaRegistry, newExecutor, collectionStrategy);
|
|
1525
|
+
}
|
|
1526
|
+
return originalAccessor;
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
return target[prop];
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
return new DbEntityTable(proxyContext, this.tableName, this.tableBuilder);
|
|
1533
|
+
}
|
|
1258
1534
|
/**
|
|
1259
1535
|
* Select all records - returns full entities with unwrapped DbColumns
|
|
1260
1536
|
*/
|