turbine-orm 0.90.1 → 0.90.2-next.91484a2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/client.d.ts +14 -1
- package/dist/cjs/client.js +45 -3
- package/dist/cjs/query/aggregates.js +6 -0
- package/dist/cjs/query/builder.d.ts +19 -2
- package/dist/cjs/query/builder.js +106 -12
- package/dist/cjs/query/writes.js +65 -15
- package/dist/client.d.ts +14 -1
- package/dist/client.js +45 -3
- package/dist/query/aggregates.js +6 -0
- package/dist/query/builder.d.ts +19 -2
- package/dist/query/builder.js +105 -12
- package/dist/query/writes.js +65 -15
- package/package.json +1 -1
package/dist/cjs/client.d.ts
CHANGED
|
@@ -579,7 +579,9 @@ export interface TurbineConfig {
|
|
|
579
579
|
* connection, so a statement named on one is missing (or already present) on
|
|
580
580
|
* the next, unless the pooler tracks prepared statements (PgBouncer 1.21+
|
|
581
581
|
* with `max_prepared_statements` above 0). Set `true` on such a pooler only
|
|
582
|
-
* when it does
|
|
582
|
+
* when it does. Turbine warns once (`prepared-statements-on-pooler`) when you
|
|
583
|
+
* set `true` and the connection settings name a pooler: Turbine's own, or
|
|
584
|
+
* the `options` of a `pg.Pool` you pass as `pool` (since 0.90.2).
|
|
583
585
|
*
|
|
584
586
|
* Override with `TURBINE_DISABLE_PREPARED=1` env var.
|
|
585
587
|
*
|
|
@@ -1132,6 +1134,17 @@ export declare class TurbineClient {
|
|
|
1132
1134
|
* the target leaves unset is `PGHOST` / `PGPORT`.
|
|
1133
1135
|
*/
|
|
1134
1136
|
private static poolerEndpoint;
|
|
1137
|
+
/**
|
|
1138
|
+
* The pooler a pool the CALLER supplied connects to, read from the settings
|
|
1139
|
+
* node-postgres keeps on the pool (`pool.options`, which `pg.Pool` and the
|
|
1140
|
+
* drivers built on it expose), the way {@link poolerEndpoint} reads Turbine's
|
|
1141
|
+
* own: the connection string wins over `host` / `port`, and an unset host or
|
|
1142
|
+
* port is `PGHOST` / `PGPORT`. `undefined` for a pool that exposes no
|
|
1143
|
+
* settings (another driver) or names no pooler.
|
|
1144
|
+
*/
|
|
1145
|
+
private static suppliedPoolEndpoint;
|
|
1146
|
+
/** The first of `targets` that names a pooler, as `host:port`, after filling unset parts from the environment. */
|
|
1147
|
+
private static firstPooledTarget;
|
|
1135
1148
|
/** Always on: named statements behind a transaction pooler fail in production first. */
|
|
1136
1149
|
private static warnPreparedOnPooler;
|
|
1137
1150
|
/**
|
package/dist/cjs/client.js
CHANGED
|
@@ -40,6 +40,7 @@ const logger_js_1 = require("./logger.js");
|
|
|
40
40
|
const nested_write_js_1 = require("./nested-write.js");
|
|
41
41
|
const observe_js_1 = require("./observe.js");
|
|
42
42
|
const pipeline_js_1 = require("./pipeline.js");
|
|
43
|
+
const builder_js_1 = require("./query/builder.js");
|
|
43
44
|
const index_js_1 = require("./query/index.js");
|
|
44
45
|
const row_lock_js_1 = require("./query/row-lock.js");
|
|
45
46
|
const sql_comment_js_1 = require("./query/sql-comment.js");
|
|
@@ -814,7 +815,7 @@ class TransactionClient {
|
|
|
814
815
|
// Create a QueryInterface that uses the transaction client as its "pool"
|
|
815
816
|
// We use a proxy pool that routes queries through the transaction client
|
|
816
817
|
const txPool = this.createTxPool();
|
|
817
|
-
const txOpts =
|
|
818
|
+
const txOpts = (0, builder_js_1.txQueryOptions)(this.queryOptions);
|
|
818
819
|
qi = txOpts.queryInterfaceFactory
|
|
819
820
|
? (0, table_client_js_1.asQueryInterface)(txOpts.queryInterfaceFactory(txPool, name, this.schema, this.middlewares, txOpts))
|
|
820
821
|
: new index_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, txOpts);
|
|
@@ -1360,8 +1361,12 @@ class TurbineClient {
|
|
|
1360
1361
|
// Respect env var kill switch
|
|
1361
1362
|
const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
|
|
1362
1363
|
const pooler = config.pool ? undefined : TurbineClient.poolerEndpoint(config);
|
|
1363
|
-
|
|
1364
|
-
|
|
1364
|
+
// A supplied pool defaults to unnamed statements, so its endpoint is read
|
|
1365
|
+
// only to warn when they are turned on behind a pooler.
|
|
1366
|
+
if (config.preparedStatements === true) {
|
|
1367
|
+
const endpoint = config.pool ? TurbineClient.suppliedPoolEndpoint(config.pool) : pooler;
|
|
1368
|
+
if (endpoint !== undefined)
|
|
1369
|
+
TurbineClient.warnPreparedOnPooler(endpoint, logger);
|
|
1365
1370
|
}
|
|
1366
1371
|
this.errorMessageMode = config.errorMessages ?? 'safe';
|
|
1367
1372
|
this.errorMessagesSafe = this.errorMessageMode === 'safe';
|
|
@@ -1732,6 +1737,43 @@ class TurbineClient {
|
|
|
1732
1737
|
if (typeof replica === 'string' && replica)
|
|
1733
1738
|
targets.push((0, connection_url_js_1.parseConnectionTarget)(replica));
|
|
1734
1739
|
}
|
|
1740
|
+
return TurbineClient.firstPooledTarget(targets);
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* The pooler a pool the CALLER supplied connects to, read from the settings
|
|
1744
|
+
* node-postgres keeps on the pool (`pool.options`, which `pg.Pool` and the
|
|
1745
|
+
* drivers built on it expose), the way {@link poolerEndpoint} reads Turbine's
|
|
1746
|
+
* own: the connection string wins over `host` / `port`, and an unset host or
|
|
1747
|
+
* port is `PGHOST` / `PGPORT`. `undefined` for a pool that exposes no
|
|
1748
|
+
* settings (another driver) or names no pooler.
|
|
1749
|
+
*/
|
|
1750
|
+
static suppliedPoolEndpoint(pool) {
|
|
1751
|
+
// A diagnostic: a pool whose settings cannot be read is simply not checked,
|
|
1752
|
+
// and nothing here may fail the constructor.
|
|
1753
|
+
let target;
|
|
1754
|
+
try {
|
|
1755
|
+
const options = pool.options;
|
|
1756
|
+
if (options === null || typeof options !== 'object')
|
|
1757
|
+
return undefined;
|
|
1758
|
+
const { connectionString, host, port } = options;
|
|
1759
|
+
if (typeof connectionString === 'string' && connectionString) {
|
|
1760
|
+
target = (0, connection_url_js_1.parseConnectionTarget)(connectionString);
|
|
1761
|
+
}
|
|
1762
|
+
else {
|
|
1763
|
+
const portNumber = typeof port === 'string' ? Number.parseInt(port, 10) : port;
|
|
1764
|
+
target = {
|
|
1765
|
+
host: typeof host === 'string' && host ? host.toLowerCase() : null,
|
|
1766
|
+
port: typeof portNumber === 'number' && Number.isInteger(portNumber) ? portNumber : null,
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
catch {
|
|
1771
|
+
return undefined;
|
|
1772
|
+
}
|
|
1773
|
+
return TurbineClient.firstPooledTarget([target]);
|
|
1774
|
+
}
|
|
1775
|
+
/** The first of `targets` that names a pooler, as `host:port`, after filling unset parts from the environment. */
|
|
1776
|
+
static firstPooledTarget(targets) {
|
|
1735
1777
|
const environment = { host: pgEnv('PGHOST'), port: pgEnv('PGPORT') };
|
|
1736
1778
|
for (const target of targets) {
|
|
1737
1779
|
const found = (0, connection_url_js_1.detectPoolerTarget)((0, connection_url_js_1.fillTargetFromEnvironment)(target, environment));
|
|
@@ -212,6 +212,10 @@ function buildGroupBy(qi, args) {
|
|
|
212
212
|
}
|
|
213
213
|
else {
|
|
214
214
|
const col = resolveJsonPathTarget(qi, 'group key', entry.field, entry.path);
|
|
215
|
+
// The alias below is caller text (`alias`, else the last path segment),
|
|
216
|
+
// written into the statement, so the statement goes unnamed: a key
|
|
217
|
+
// chosen per request would otherwise claim a prepared name per value.
|
|
218
|
+
qi.markVariableArity();
|
|
215
219
|
assertAggregatePiiOptIn(qi.table, meta, entry.field, col, 'groupBy JSON `by` key', includePii);
|
|
216
220
|
params.push(whereMod.jsonPathParam(qi, entry.path));
|
|
217
221
|
const extract = qi.dialect.buildJsonPathExtract(qi.q(col), qi.p(params.length));
|
|
@@ -295,6 +299,8 @@ function buildGroupBy(qi, args) {
|
|
|
295
299
|
continue;
|
|
296
300
|
}
|
|
297
301
|
const col = resolveJsonPathTarget(qi, `${aggKey} target "${key}"`, target.field, target.path);
|
|
302
|
+
// `key` is a caller-chosen label written into the alias: unnamed, as above.
|
|
303
|
+
qi.markVariableArity();
|
|
298
304
|
if (aggKey === '_min' || aggKey === '_max') {
|
|
299
305
|
assertAggregatePiiOptIn(qi.table, meta, target.field, col, `groupBy ${aggKey} JSON target`, includePii);
|
|
300
306
|
}
|
|
@@ -177,6 +177,7 @@ export declare const AUTO_COUNT_BATCH_MIN_PARENT_ROWS = 2;
|
|
|
177
177
|
export declare function unlockNestedWriteTx<X extends {
|
|
178
178
|
table(name: string): Record<string, unknown>;
|
|
179
179
|
}>(tx: X): X;
|
|
180
|
+
export declare function txQueryOptions(options: QueryInterfaceOptions | undefined): QueryInterfaceOptions;
|
|
180
181
|
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './deferred.js';
|
|
181
182
|
import type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './deferred.js';
|
|
182
183
|
export declare class QueryInterface<T extends object, R extends object = {}, M extends TableInputTypes = DefaultInputTypes<T, R>> {
|
|
@@ -904,8 +905,11 @@ export declare class QueryInterface<T extends object, R extends object = {}, M e
|
|
|
904
905
|
*/
|
|
905
906
|
private prepareBatchedBase;
|
|
906
907
|
/**
|
|
907
|
-
*
|
|
908
|
-
*
|
|
908
|
+
* SQL template cache statistics. `hits`, `misses` and `hitRate` count this
|
|
909
|
+
* accessor's own lookups. `size` is the entries in the cache it reads, which
|
|
910
|
+
* since 0.90.2 is shared with every accessor built from the same client
|
|
911
|
+
* options for this table: a client's primary and replica accessors share
|
|
912
|
+
* one, and all of its transactions share another (see `templateCacheFor`).
|
|
909
913
|
*/
|
|
910
914
|
cacheStats(): {
|
|
911
915
|
hits: number;
|
|
@@ -951,6 +955,19 @@ export declare class QueryInterface<T extends object, R extends object = {}, M e
|
|
|
951
955
|
* variable-arity and the measured reason it must not be named.
|
|
952
956
|
*/
|
|
953
957
|
private buildCacheEntry;
|
|
958
|
+
/**
|
|
959
|
+
* The prepared-statement name for `sql`: empty (sent unnamed) for a
|
|
960
|
+
* variable-arity shape, else derived from the text.
|
|
961
|
+
*/
|
|
962
|
+
private preparedNameOf;
|
|
963
|
+
/**
|
|
964
|
+
* Name a statement the SQL template cache does not hold (`aggregate`,
|
|
965
|
+
* `groupBy`): the build runs between a reset and a read of the
|
|
966
|
+
* variable-arity flag, exactly as in {@link buildCacheEntry}, so the same
|
|
967
|
+
* shapes stay unnamed. These used to be sent unnamed always, so with
|
|
968
|
+
* prepared statements on the server re-parsed every `aggregate`.
|
|
969
|
+
*/
|
|
970
|
+
private withPreparedName;
|
|
954
971
|
/**
|
|
955
972
|
* The SQL-cache lockstep checks (see the comment above `CACHE_MISMATCH_WARN_NS`).
|
|
956
973
|
*
|
|
@@ -47,6 +47,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
48
|
exports.QueryInterface = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = void 0;
|
|
49
49
|
exports.unlockNestedWriteTx = unlockNestedWriteTx;
|
|
50
|
+
exports.txQueryOptions = txQueryOptions;
|
|
50
51
|
const checkout_js_1 = require("../checkout.js");
|
|
51
52
|
const connection_guard_js_1 = require("../connection-guard.js");
|
|
52
53
|
const diagnostics_js_1 = require("../diagnostics.js");
|
|
@@ -539,6 +540,69 @@ const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
|
|
|
539
540
|
function defaultJsonEncoding(dialect) {
|
|
540
541
|
return dialect.capabilities.positionalJson ? 'positional' : 'object';
|
|
541
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* SQL template caches shared by every QueryInterface built from ONE options
|
|
545
|
+
* object, for one schema and one table.
|
|
546
|
+
*
|
|
547
|
+
* A transaction builds its own QueryInterface per table, so before this each
|
|
548
|
+
* `$transaction` started with an empty cache and rebuilt the SQL of every
|
|
549
|
+
* statement it sent. TransactionClient now hands every transaction of a client
|
|
550
|
+
* the same options object (`txQueryOptions` in client.ts), so they share one
|
|
551
|
+
* cache per table and a statement is built once per process, as it is outside
|
|
552
|
+
* a transaction.
|
|
553
|
+
*
|
|
554
|
+
* THE KEY IS THE OPTIONS OBJECT'S IDENTITY, deliberately, not a fingerprint of
|
|
555
|
+
* its fields. Options change the SQL (`defaultLimit`, `jsonEncoding`,
|
|
556
|
+
* `globalFilters`, the dialect, ...), and a cache key does not carry them,
|
|
557
|
+
* because one QueryInterface never sees two values. A copy that changes any
|
|
558
|
+
* field is a new object and so gets its own caches: the batched loader's
|
|
559
|
+
* children (`defaultLimit: undefined`) can never be served the parent's SQL,
|
|
560
|
+
* and no list of SQL-affecting options exists to fall out of date. Nothing
|
|
561
|
+
* mutates an options object after construction.
|
|
562
|
+
*/
|
|
563
|
+
const sharedTemplateCaches = new WeakMap();
|
|
564
|
+
/**
|
|
565
|
+
* The options a transaction's query interfaces are built with: `options` plus
|
|
566
|
+
* `_txScoped`. ONE object per options object, so every transaction of a client
|
|
567
|
+
* (and every nested write inside one) builds its query interfaces from the same
|
|
568
|
+
* object and shares their template caches (see {@link templateCacheFor}). A
|
|
569
|
+
* fresh copy per transaction gave each `$transaction` an empty cache, so every
|
|
570
|
+
* statement in it rebuilt its SQL. Options that are already transaction-scoped
|
|
571
|
+
* are returned as they are.
|
|
572
|
+
*/
|
|
573
|
+
const txOptionsCache = new WeakMap();
|
|
574
|
+
function txQueryOptions(options) {
|
|
575
|
+
if (options === undefined)
|
|
576
|
+
return { _txScoped: true };
|
|
577
|
+
if (options._txScoped === true)
|
|
578
|
+
return options;
|
|
579
|
+
let tx = txOptionsCache.get(options);
|
|
580
|
+
if (tx === undefined) {
|
|
581
|
+
tx = { ...options, _txScoped: true };
|
|
582
|
+
txOptionsCache.set(options, tx);
|
|
583
|
+
}
|
|
584
|
+
return tx;
|
|
585
|
+
}
|
|
586
|
+
function templateCacheFor(options, schema, table, capacity) {
|
|
587
|
+
if (options === undefined)
|
|
588
|
+
return new utils_js_1.LRUCache(capacity);
|
|
589
|
+
let bySchema = sharedTemplateCaches.get(options);
|
|
590
|
+
if (bySchema === undefined) {
|
|
591
|
+
bySchema = new WeakMap();
|
|
592
|
+
sharedTemplateCaches.set(options, bySchema);
|
|
593
|
+
}
|
|
594
|
+
let byTable = bySchema.get(schema);
|
|
595
|
+
if (byTable === undefined) {
|
|
596
|
+
byTable = new Map();
|
|
597
|
+
bySchema.set(schema, byTable);
|
|
598
|
+
}
|
|
599
|
+
let cache = byTable.get(table);
|
|
600
|
+
if (cache === undefined) {
|
|
601
|
+
cache = new utils_js_1.LRUCache(capacity);
|
|
602
|
+
byTable.set(table, cache);
|
|
603
|
+
}
|
|
604
|
+
return cache;
|
|
605
|
+
}
|
|
542
606
|
class QueryInterface {
|
|
543
607
|
pool;
|
|
544
608
|
table;
|
|
@@ -832,7 +896,7 @@ class QueryInterface {
|
|
|
832
896
|
// treated as the default rather than throwing.
|
|
833
897
|
const sqlCacheSize = options?.sqlCacheSize;
|
|
834
898
|
this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
|
|
835
|
-
this.sqlTemplateCache =
|
|
899
|
+
this.sqlTemplateCache = templateCacheFor(options, schema, table, sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
|
|
836
900
|
this.dialect = options?.dialect ?? dialect_js_1.postgresDialect;
|
|
837
901
|
(0, dialect_js_1.assertDialectCapabilities)(this.dialect);
|
|
838
902
|
this.sqlCommenter =
|
|
@@ -2221,8 +2285,11 @@ class QueryInterface {
|
|
|
2221
2285
|
return { baseArgs, strip: proj.strip };
|
|
2222
2286
|
}
|
|
2223
2287
|
/**
|
|
2224
|
-
*
|
|
2225
|
-
*
|
|
2288
|
+
* SQL template cache statistics. `hits`, `misses` and `hitRate` count this
|
|
2289
|
+
* accessor's own lookups. `size` is the entries in the cache it reads, which
|
|
2290
|
+
* since 0.90.2 is shared with every accessor built from the same client
|
|
2291
|
+
* options for this table: a client's primary and replica accessors share
|
|
2292
|
+
* one, and all of its transactions share another (see `templateCacheFor`).
|
|
2226
2293
|
*/
|
|
2227
2294
|
cacheStats() {
|
|
2228
2295
|
const total = this.cacheHits + this.cacheMisses;
|
|
@@ -2295,15 +2362,33 @@ class QueryInterface {
|
|
|
2295
2362
|
this.variableArityShape = false;
|
|
2296
2363
|
this.cacheMisses++;
|
|
2297
2364
|
this.lastMissParams = built;
|
|
2365
|
+
return { sql, name: this.preparedNameOf(sql, variableArity), paramCount: built.length };
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* The prepared-statement name for `sql`: empty (sent unnamed) for a
|
|
2369
|
+
* variable-arity shape, else derived from the text.
|
|
2370
|
+
*/
|
|
2371
|
+
preparedNameOf(sql, variableArity) {
|
|
2372
|
+
if (variableArity)
|
|
2373
|
+
return '';
|
|
2298
2374
|
// With `sqlComment` putting a comment on every statement, the template's own
|
|
2299
2375
|
// name is never sent (the execute seam sends a `c_` name derived from it),
|
|
2300
2376
|
// so it is not claimed from the budget: only names that reach the wire do.
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2377
|
+
return this.sqlCommenter?.alwaysComments ? (0, utils_js_1.sqlToPreparedName)(sql) : (0, utils_js_1.boundedPreparedName)(sql);
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Name a statement the SQL template cache does not hold (`aggregate`,
|
|
2381
|
+
* `groupBy`): the build runs between a reset and a read of the
|
|
2382
|
+
* variable-arity flag, exactly as in {@link buildCacheEntry}, so the same
|
|
2383
|
+
* shapes stay unnamed. These used to be sent unnamed always, so with
|
|
2384
|
+
* prepared statements on the server re-parsed every `aggregate`.
|
|
2385
|
+
*/
|
|
2386
|
+
withPreparedName(build) {
|
|
2387
|
+
this.variableArityShape = false;
|
|
2388
|
+
const deferred = build();
|
|
2389
|
+
const variableArity = this.variableArityShape;
|
|
2390
|
+
this.variableArityShape = false;
|
|
2391
|
+
return { ...deferred, preparedName: this.preparedNameOf(deferred.sql, variableArity) };
|
|
2307
2392
|
}
|
|
2308
2393
|
/**
|
|
2309
2394
|
* The SQL-cache lockstep checks (see the comment above `CACHE_MISMATCH_WARN_NS`).
|
|
@@ -4264,6 +4349,8 @@ class QueryInterface {
|
|
|
4264
4349
|
return rows.length > 0 ? rows[0] : null;
|
|
4265
4350
|
},
|
|
4266
4351
|
tag: `${this.table}.findFirst`,
|
|
4352
|
+
// findMany's name: the same statement, so the same prepared statement.
|
|
4353
|
+
...(deferred.preparedName !== undefined ? { preparedName: deferred.preparedName } : {}),
|
|
4267
4354
|
...(deferred.rowLock ? { rowLock: true } : {}),
|
|
4268
4355
|
};
|
|
4269
4356
|
}
|
|
@@ -4297,6 +4384,7 @@ class QueryInterface {
|
|
|
4297
4384
|
return row;
|
|
4298
4385
|
},
|
|
4299
4386
|
tag: `${this.table}.findFirstOrThrow`,
|
|
4387
|
+
...(inner.preparedName !== undefined ? { preparedName: inner.preparedName } : {}),
|
|
4300
4388
|
...(inner.rowLock ? { rowLock: true } : {}),
|
|
4301
4389
|
};
|
|
4302
4390
|
}
|
|
@@ -4330,6 +4418,7 @@ class QueryInterface {
|
|
|
4330
4418
|
return row;
|
|
4331
4419
|
},
|
|
4332
4420
|
tag: `${this.table}.findUniqueOrThrow`,
|
|
4421
|
+
...(inner.preparedName !== undefined ? { preparedName: inner.preparedName } : {}),
|
|
4333
4422
|
...(inner.rowLock ? { rowLock: true } : {}),
|
|
4334
4423
|
};
|
|
4335
4424
|
}
|
|
@@ -4499,7 +4588,10 @@ class QueryInterface {
|
|
|
4499
4588
|
const pool = this.pool;
|
|
4500
4589
|
const schema = this.schema;
|
|
4501
4590
|
const middlewares = this.middlewares;
|
|
4502
|
-
|
|
4591
|
+
// The client's transaction options object, not a copy per call: the SQL
|
|
4592
|
+
// template cache is shared by options-object identity, so a fresh copy
|
|
4593
|
+
// rebuilt the SQL of every child statement of every nested write.
|
|
4594
|
+
const opts = txQueryOptions(this.options);
|
|
4503
4595
|
return {
|
|
4504
4596
|
schema,
|
|
4505
4597
|
tx: unlockNestedWriteTx(this.makeTxProxy(pool, schema, middlewares, opts)),
|
|
@@ -4690,10 +4782,12 @@ class QueryInterface {
|
|
|
4690
4782
|
// Aggregate / groupBy compilation (extracted to aggregates.ts).
|
|
4691
4783
|
// ---------------------------------------------------------------------------
|
|
4692
4784
|
buildGroupBy(args) {
|
|
4693
|
-
|
|
4785
|
+
const normalized = this.normalizeArgs(args);
|
|
4786
|
+
return this.withPreparedName(() => aggMod.buildGroupBy(this.ctx, normalized));
|
|
4694
4787
|
}
|
|
4695
4788
|
buildAggregate(args) {
|
|
4696
|
-
|
|
4789
|
+
const normalized = this.normalizeArgs(args);
|
|
4790
|
+
return this.withPreparedName(() => aggMod.buildAggregate(this.ctx, normalized));
|
|
4697
4791
|
}
|
|
4698
4792
|
// -------------------------------------------------------------------------
|
|
4699
4793
|
// aggregate, standalone aggregation without groupBy
|
package/dist/cjs/query/writes.js
CHANGED
|
@@ -75,7 +75,6 @@ exports.fingerprintSet = fingerprintSet;
|
|
|
75
75
|
exports.collectSetParams = collectSetParams;
|
|
76
76
|
const dialect_js_1 = require("../dialect.js");
|
|
77
77
|
const errors_js_1 = require("../errors.js");
|
|
78
|
-
const schema_js_1 = require("../schema.js");
|
|
79
78
|
const column_ref_js_1 = require("./column-ref.js");
|
|
80
79
|
const compound_unique_js_1 = require("./compound-unique.js");
|
|
81
80
|
const expr_js_1 = require("./expr.js");
|
|
@@ -818,6 +817,10 @@ function definedKeys(qi, row) {
|
|
|
818
817
|
function assertUniformCreateManyRows(qi, rows, firstKeys) {
|
|
819
818
|
const expected = new Set(firstKeys);
|
|
820
819
|
for (let i = 1; i < rows.length; i++) {
|
|
820
|
+
// The common case, decided without ordering the row's keys: as many
|
|
821
|
+
// defined keys as the first row, each one of its keys.
|
|
822
|
+
if (namesExactly(rows[i], expected))
|
|
823
|
+
continue;
|
|
821
824
|
const rowKeys = definedKeys(qi, rows[i]);
|
|
822
825
|
const unexpected = rowKeys.filter((k) => !expected.has(k));
|
|
823
826
|
// No stranger and the same count means the same set (object keys are unique).
|
|
@@ -838,6 +841,18 @@ function assertUniformCreateManyRows(qi, rows, firstKeys) {
|
|
|
838
841
|
'row shape.');
|
|
839
842
|
}
|
|
840
843
|
}
|
|
844
|
+
/** Whether `row`'s defined keys are exactly `keys` (object keys are unique, so count plus membership decides it). */
|
|
845
|
+
function namesExactly(row, keys) {
|
|
846
|
+
let count = 0;
|
|
847
|
+
for (const key in row) {
|
|
848
|
+
if (!Object.hasOwn(row, key) || row[key] === undefined)
|
|
849
|
+
continue;
|
|
850
|
+
if (!keys.has(key))
|
|
851
|
+
return false;
|
|
852
|
+
count++;
|
|
853
|
+
}
|
|
854
|
+
return count === keys.size;
|
|
855
|
+
}
|
|
841
856
|
/** `a, b` → `"a", "b"`, for the field lists in {@link assertUniformCreateManyRows}. */
|
|
842
857
|
function quoteList(fields) {
|
|
843
858
|
return fields.map((f) => `"${f}"`).join(', ');
|
|
@@ -920,8 +935,15 @@ function prepareCreateMany(qi, args, projection) {
|
|
|
920
935
|
};
|
|
921
936
|
}
|
|
922
937
|
assertWritable(qi, 'createMany');
|
|
923
|
-
|
|
924
|
-
|
|
938
|
+
args.data.forEach((row, i) => {
|
|
939
|
+
if (row === null || typeof row !== 'object') {
|
|
940
|
+
throw new errors_js_1.ValidationError(`createMany on "${qi.table}": row ${i} is ${row === null ? 'null' : `a ${typeof row}`}, not an object of fields.`);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
const generated = generatedStoredColumns(qi);
|
|
944
|
+
if (generated !== null) {
|
|
945
|
+
for (const row of args.data)
|
|
946
|
+
assertNoGeneratedKeys(qi, generated, row, 'createMany');
|
|
925
947
|
}
|
|
926
948
|
const keys = definedKeys(qi, args.data[0]);
|
|
927
949
|
assertUniformCreateManyRows(qi, args.data, keys);
|
|
@@ -1117,11 +1139,13 @@ function describeConflictTargets(qi) {
|
|
|
1117
1139
|
}
|
|
1118
1140
|
/** The conflict-UPDATE's data: the field-list shorthand becomes `{ f: expr.excluded(f) }`. */
|
|
1119
1141
|
function resolveConflictUpdate(qi, raw) {
|
|
1120
|
-
|
|
1142
|
+
// A function, not an error built up front: constructing one captures a
|
|
1143
|
+
// stack trace, and this runs on every onConflict call.
|
|
1144
|
+
const empty = () => new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update is empty; use onConflict: { target, ignore: true } to skip ` +
|
|
1121
1145
|
'conflicting rows.');
|
|
1122
1146
|
if (Array.isArray(raw)) {
|
|
1123
1147
|
if (raw.length === 0)
|
|
1124
|
-
throw empty;
|
|
1148
|
+
throw empty();
|
|
1125
1149
|
const update = {};
|
|
1126
1150
|
for (const field of raw) {
|
|
1127
1151
|
if (typeof field !== 'string') {
|
|
@@ -1138,7 +1162,7 @@ function resolveConflictUpdate(qi, raw) {
|
|
|
1138
1162
|
throw new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update takes a list of field names or a { field: value } object.`);
|
|
1139
1163
|
}
|
|
1140
1164
|
if (!Object.values(raw).some((v) => v !== undefined))
|
|
1141
|
-
throw empty;
|
|
1165
|
+
throw empty();
|
|
1142
1166
|
assertNoGeneratedColumns(qi, raw, 'createMany');
|
|
1143
1167
|
return raw;
|
|
1144
1168
|
}
|
|
@@ -1194,12 +1218,15 @@ function keyToken(value) {
|
|
|
1194
1218
|
function assertNoDuplicateKeys(qi, conflict, columns, rowValues) {
|
|
1195
1219
|
const at = conflict.target.map((c) => columns.indexOf(c));
|
|
1196
1220
|
const seen = new Map();
|
|
1197
|
-
for (let i = 0; i < rowValues.length; i++) {
|
|
1221
|
+
rows: for (let i = 0; i < rowValues.length; i++) {
|
|
1198
1222
|
const row = rowValues[i];
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1223
|
+
let key = '';
|
|
1224
|
+
for (let k = 0; k < at.length; k++) {
|
|
1225
|
+
const value = row[at[k]];
|
|
1226
|
+
if (value === null || value === undefined)
|
|
1227
|
+
continue rows;
|
|
1228
|
+
key += k === 0 ? keyToken(value) : `|${keyToken(value)}`;
|
|
1229
|
+
}
|
|
1203
1230
|
const first = seen.get(key);
|
|
1204
1231
|
if (first !== undefined) {
|
|
1205
1232
|
throw new errors_js_1.ValidationError(`createMany onConflict on "${qi.table}": rows ${first} and ${i} share the conflict key ` +
|
|
@@ -2164,11 +2191,34 @@ function assertWritable(qi, operation) {
|
|
|
2164
2191
|
* (they're stripped from the statement anyway).
|
|
2165
2192
|
*/
|
|
2166
2193
|
function assertNoGeneratedColumns(qi, data, operation) {
|
|
2167
|
-
|
|
2168
|
-
|
|
2194
|
+
const generated = generatedStoredColumns(qi);
|
|
2195
|
+
if (generated !== null)
|
|
2196
|
+
assertNoGeneratedKeys(qi, generated, data, operation);
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* The table's generated stored columns, or `null` when it has none (most
|
|
2200
|
+
* tables, and then a write checks nothing). Read once per call, so
|
|
2201
|
+
* `createMany` checks every row against one answer: it used to search the
|
|
2202
|
+
* column list, with a `camelToSnake` per comparison, for every field of every
|
|
2203
|
+
* row, which was most of a 150-row upsert's build time.
|
|
2204
|
+
*/
|
|
2205
|
+
function generatedStoredColumns(qi) {
|
|
2206
|
+
let columns = null;
|
|
2207
|
+
for (const c of qi.tableMeta.columns) {
|
|
2208
|
+
if (!c.isGeneratedStored)
|
|
2169
2209
|
continue;
|
|
2170
|
-
|
|
2171
|
-
|
|
2210
|
+
columns ??= new Set();
|
|
2211
|
+
columns.add(c.name);
|
|
2212
|
+
}
|
|
2213
|
+
return columns;
|
|
2214
|
+
}
|
|
2215
|
+
/** Refuse a key of `data` that names one of `generated` (by THE key rule, {@link resolveColumnName}). */
|
|
2216
|
+
function assertNoGeneratedKeys(qi, generated, data, operation) {
|
|
2217
|
+
for (const key in data) {
|
|
2218
|
+
if (!Object.hasOwn(data, key) || data[key] === undefined)
|
|
2219
|
+
continue;
|
|
2220
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
2221
|
+
if (column !== undefined && generated.has(column)) {
|
|
2172
2222
|
throw new errors_js_1.ValidationError(`Cannot ${operation} "${qi.table}": column "${key}" is a GENERATED ALWAYS AS (…) STORED ` +
|
|
2173
2223
|
'column whose value the database computes, remove it from your data.');
|
|
2174
2224
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -579,7 +579,9 @@ export interface TurbineConfig {
|
|
|
579
579
|
* connection, so a statement named on one is missing (or already present) on
|
|
580
580
|
* the next, unless the pooler tracks prepared statements (PgBouncer 1.21+
|
|
581
581
|
* with `max_prepared_statements` above 0). Set `true` on such a pooler only
|
|
582
|
-
* when it does
|
|
582
|
+
* when it does. Turbine warns once (`prepared-statements-on-pooler`) when you
|
|
583
|
+
* set `true` and the connection settings name a pooler: Turbine's own, or
|
|
584
|
+
* the `options` of a `pg.Pool` you pass as `pool` (since 0.90.2).
|
|
583
585
|
*
|
|
584
586
|
* Override with `TURBINE_DISABLE_PREPARED=1` env var.
|
|
585
587
|
*
|
|
@@ -1132,6 +1134,17 @@ export declare class TurbineClient {
|
|
|
1132
1134
|
* the target leaves unset is `PGHOST` / `PGPORT`.
|
|
1133
1135
|
*/
|
|
1134
1136
|
private static poolerEndpoint;
|
|
1137
|
+
/**
|
|
1138
|
+
* The pooler a pool the CALLER supplied connects to, read from the settings
|
|
1139
|
+
* node-postgres keeps on the pool (`pool.options`, which `pg.Pool` and the
|
|
1140
|
+
* drivers built on it expose), the way {@link poolerEndpoint} reads Turbine's
|
|
1141
|
+
* own: the connection string wins over `host` / `port`, and an unset host or
|
|
1142
|
+
* port is `PGHOST` / `PGPORT`. `undefined` for a pool that exposes no
|
|
1143
|
+
* settings (another driver) or names no pooler.
|
|
1144
|
+
*/
|
|
1145
|
+
private static suppliedPoolEndpoint;
|
|
1146
|
+
/** The first of `targets` that names a pooler, as `host:port`, after filling unset parts from the environment. */
|
|
1147
|
+
private static firstPooledTarget;
|
|
1135
1148
|
/** Always on: named statements behind a transaction pooler fail in production first. */
|
|
1136
1149
|
private static warnPreparedOnPooler;
|
|
1137
1150
|
/**
|
package/dist/client.js
CHANGED
|
@@ -33,6 +33,7 @@ import { CLIENT_LOG_CONTEXT, logTo, resolveLogger } from './logger.js';
|
|
|
33
33
|
import { hasRelationFields } from './nested-write.js';
|
|
34
34
|
import { ObserveEngine } from './observe.js';
|
|
35
35
|
import { executePipeline, pipelineSupported } from './pipeline.js';
|
|
36
|
+
import { txQueryOptions } from './query/builder.js';
|
|
36
37
|
import { QueryInterface, } from './query/index.js';
|
|
37
38
|
import { assertNoLockInAutocommitPipeline } from './query/row-lock.js';
|
|
38
39
|
import { commentDeferred, resolveSqlComment } from './query/sql-comment.js';
|
|
@@ -807,7 +808,7 @@ export class TransactionClient {
|
|
|
807
808
|
// Create a QueryInterface that uses the transaction client as its "pool"
|
|
808
809
|
// We use a proxy pool that routes queries through the transaction client
|
|
809
810
|
const txPool = this.createTxPool();
|
|
810
|
-
const txOpts =
|
|
811
|
+
const txOpts = txQueryOptions(this.queryOptions);
|
|
811
812
|
qi = txOpts.queryInterfaceFactory
|
|
812
813
|
? asQueryInterface(txOpts.queryInterfaceFactory(txPool, name, this.schema, this.middlewares, txOpts))
|
|
813
814
|
: new QueryInterface(txPool, name, this.schema, this.middlewares, txOpts);
|
|
@@ -1352,8 +1353,12 @@ export class TurbineClient {
|
|
|
1352
1353
|
// Respect env var kill switch
|
|
1353
1354
|
const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
|
|
1354
1355
|
const pooler = config.pool ? undefined : TurbineClient.poolerEndpoint(config);
|
|
1355
|
-
|
|
1356
|
-
|
|
1356
|
+
// A supplied pool defaults to unnamed statements, so its endpoint is read
|
|
1357
|
+
// only to warn when they are turned on behind a pooler.
|
|
1358
|
+
if (config.preparedStatements === true) {
|
|
1359
|
+
const endpoint = config.pool ? TurbineClient.suppliedPoolEndpoint(config.pool) : pooler;
|
|
1360
|
+
if (endpoint !== undefined)
|
|
1361
|
+
TurbineClient.warnPreparedOnPooler(endpoint, logger);
|
|
1357
1362
|
}
|
|
1358
1363
|
this.errorMessageMode = config.errorMessages ?? 'safe';
|
|
1359
1364
|
this.errorMessagesSafe = this.errorMessageMode === 'safe';
|
|
@@ -1724,6 +1729,43 @@ export class TurbineClient {
|
|
|
1724
1729
|
if (typeof replica === 'string' && replica)
|
|
1725
1730
|
targets.push(parseConnectionTarget(replica));
|
|
1726
1731
|
}
|
|
1732
|
+
return TurbineClient.firstPooledTarget(targets);
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* The pooler a pool the CALLER supplied connects to, read from the settings
|
|
1736
|
+
* node-postgres keeps on the pool (`pool.options`, which `pg.Pool` and the
|
|
1737
|
+
* drivers built on it expose), the way {@link poolerEndpoint} reads Turbine's
|
|
1738
|
+
* own: the connection string wins over `host` / `port`, and an unset host or
|
|
1739
|
+
* port is `PGHOST` / `PGPORT`. `undefined` for a pool that exposes no
|
|
1740
|
+
* settings (another driver) or names no pooler.
|
|
1741
|
+
*/
|
|
1742
|
+
static suppliedPoolEndpoint(pool) {
|
|
1743
|
+
// A diagnostic: a pool whose settings cannot be read is simply not checked,
|
|
1744
|
+
// and nothing here may fail the constructor.
|
|
1745
|
+
let target;
|
|
1746
|
+
try {
|
|
1747
|
+
const options = pool.options;
|
|
1748
|
+
if (options === null || typeof options !== 'object')
|
|
1749
|
+
return undefined;
|
|
1750
|
+
const { connectionString, host, port } = options;
|
|
1751
|
+
if (typeof connectionString === 'string' && connectionString) {
|
|
1752
|
+
target = parseConnectionTarget(connectionString);
|
|
1753
|
+
}
|
|
1754
|
+
else {
|
|
1755
|
+
const portNumber = typeof port === 'string' ? Number.parseInt(port, 10) : port;
|
|
1756
|
+
target = {
|
|
1757
|
+
host: typeof host === 'string' && host ? host.toLowerCase() : null,
|
|
1758
|
+
port: typeof portNumber === 'number' && Number.isInteger(portNumber) ? portNumber : null,
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
catch {
|
|
1763
|
+
return undefined;
|
|
1764
|
+
}
|
|
1765
|
+
return TurbineClient.firstPooledTarget([target]);
|
|
1766
|
+
}
|
|
1767
|
+
/** The first of `targets` that names a pooler, as `host:port`, after filling unset parts from the environment. */
|
|
1768
|
+
static firstPooledTarget(targets) {
|
|
1727
1769
|
const environment = { host: pgEnv('PGHOST'), port: pgEnv('PGPORT') };
|
|
1728
1770
|
for (const target of targets) {
|
|
1729
1771
|
const found = detectPoolerTarget(fillTargetFromEnvironment(target, environment));
|
package/dist/query/aggregates.js
CHANGED
|
@@ -168,6 +168,10 @@ export function buildGroupBy(qi, args) {
|
|
|
168
168
|
}
|
|
169
169
|
else {
|
|
170
170
|
const col = resolveJsonPathTarget(qi, 'group key', entry.field, entry.path);
|
|
171
|
+
// The alias below is caller text (`alias`, else the last path segment),
|
|
172
|
+
// written into the statement, so the statement goes unnamed: a key
|
|
173
|
+
// chosen per request would otherwise claim a prepared name per value.
|
|
174
|
+
qi.markVariableArity();
|
|
171
175
|
assertAggregatePiiOptIn(qi.table, meta, entry.field, col, 'groupBy JSON `by` key', includePii);
|
|
172
176
|
params.push(whereMod.jsonPathParam(qi, entry.path));
|
|
173
177
|
const extract = qi.dialect.buildJsonPathExtract(qi.q(col), qi.p(params.length));
|
|
@@ -251,6 +255,8 @@ export function buildGroupBy(qi, args) {
|
|
|
251
255
|
continue;
|
|
252
256
|
}
|
|
253
257
|
const col = resolveJsonPathTarget(qi, `${aggKey} target "${key}"`, target.field, target.path);
|
|
258
|
+
// `key` is a caller-chosen label written into the alias: unnamed, as above.
|
|
259
|
+
qi.markVariableArity();
|
|
254
260
|
if (aggKey === '_min' || aggKey === '_max') {
|
|
255
261
|
assertAggregatePiiOptIn(qi.table, meta, target.field, col, `groupBy ${aggKey} JSON target`, includePii);
|
|
256
262
|
}
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -177,6 +177,7 @@ export declare const AUTO_COUNT_BATCH_MIN_PARENT_ROWS = 2;
|
|
|
177
177
|
export declare function unlockNestedWriteTx<X extends {
|
|
178
178
|
table(name: string): Record<string, unknown>;
|
|
179
179
|
}>(tx: X): X;
|
|
180
|
+
export declare function txQueryOptions(options: QueryInterfaceOptions | undefined): QueryInterfaceOptions;
|
|
180
181
|
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './deferred.js';
|
|
181
182
|
import type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './deferred.js';
|
|
182
183
|
export declare class QueryInterface<T extends object, R extends object = {}, M extends TableInputTypes = DefaultInputTypes<T, R>> {
|
|
@@ -904,8 +905,11 @@ export declare class QueryInterface<T extends object, R extends object = {}, M e
|
|
|
904
905
|
*/
|
|
905
906
|
private prepareBatchedBase;
|
|
906
907
|
/**
|
|
907
|
-
*
|
|
908
|
-
*
|
|
908
|
+
* SQL template cache statistics. `hits`, `misses` and `hitRate` count this
|
|
909
|
+
* accessor's own lookups. `size` is the entries in the cache it reads, which
|
|
910
|
+
* since 0.90.2 is shared with every accessor built from the same client
|
|
911
|
+
* options for this table: a client's primary and replica accessors share
|
|
912
|
+
* one, and all of its transactions share another (see `templateCacheFor`).
|
|
909
913
|
*/
|
|
910
914
|
cacheStats(): {
|
|
911
915
|
hits: number;
|
|
@@ -951,6 +955,19 @@ export declare class QueryInterface<T extends object, R extends object = {}, M e
|
|
|
951
955
|
* variable-arity and the measured reason it must not be named.
|
|
952
956
|
*/
|
|
953
957
|
private buildCacheEntry;
|
|
958
|
+
/**
|
|
959
|
+
* The prepared-statement name for `sql`: empty (sent unnamed) for a
|
|
960
|
+
* variable-arity shape, else derived from the text.
|
|
961
|
+
*/
|
|
962
|
+
private preparedNameOf;
|
|
963
|
+
/**
|
|
964
|
+
* Name a statement the SQL template cache does not hold (`aggregate`,
|
|
965
|
+
* `groupBy`): the build runs between a reset and a read of the
|
|
966
|
+
* variable-arity flag, exactly as in {@link buildCacheEntry}, so the same
|
|
967
|
+
* shapes stay unnamed. These used to be sent unnamed always, so with
|
|
968
|
+
* prepared statements on the server re-parsed every `aggregate`.
|
|
969
|
+
*/
|
|
970
|
+
private withPreparedName;
|
|
954
971
|
/**
|
|
955
972
|
* The SQL-cache lockstep checks (see the comment above `CACHE_MISMATCH_WARN_NS`).
|
|
956
973
|
*
|
package/dist/query/builder.js
CHANGED
|
@@ -502,6 +502,69 @@ const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
|
|
|
502
502
|
function defaultJsonEncoding(dialect) {
|
|
503
503
|
return dialect.capabilities.positionalJson ? 'positional' : 'object';
|
|
504
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* SQL template caches shared by every QueryInterface built from ONE options
|
|
507
|
+
* object, for one schema and one table.
|
|
508
|
+
*
|
|
509
|
+
* A transaction builds its own QueryInterface per table, so before this each
|
|
510
|
+
* `$transaction` started with an empty cache and rebuilt the SQL of every
|
|
511
|
+
* statement it sent. TransactionClient now hands every transaction of a client
|
|
512
|
+
* the same options object (`txQueryOptions` in client.ts), so they share one
|
|
513
|
+
* cache per table and a statement is built once per process, as it is outside
|
|
514
|
+
* a transaction.
|
|
515
|
+
*
|
|
516
|
+
* THE KEY IS THE OPTIONS OBJECT'S IDENTITY, deliberately, not a fingerprint of
|
|
517
|
+
* its fields. Options change the SQL (`defaultLimit`, `jsonEncoding`,
|
|
518
|
+
* `globalFilters`, the dialect, ...), and a cache key does not carry them,
|
|
519
|
+
* because one QueryInterface never sees two values. A copy that changes any
|
|
520
|
+
* field is a new object and so gets its own caches: the batched loader's
|
|
521
|
+
* children (`defaultLimit: undefined`) can never be served the parent's SQL,
|
|
522
|
+
* and no list of SQL-affecting options exists to fall out of date. Nothing
|
|
523
|
+
* mutates an options object after construction.
|
|
524
|
+
*/
|
|
525
|
+
const sharedTemplateCaches = new WeakMap();
|
|
526
|
+
/**
|
|
527
|
+
* The options a transaction's query interfaces are built with: `options` plus
|
|
528
|
+
* `_txScoped`. ONE object per options object, so every transaction of a client
|
|
529
|
+
* (and every nested write inside one) builds its query interfaces from the same
|
|
530
|
+
* object and shares their template caches (see {@link templateCacheFor}). A
|
|
531
|
+
* fresh copy per transaction gave each `$transaction` an empty cache, so every
|
|
532
|
+
* statement in it rebuilt its SQL. Options that are already transaction-scoped
|
|
533
|
+
* are returned as they are.
|
|
534
|
+
*/
|
|
535
|
+
const txOptionsCache = new WeakMap();
|
|
536
|
+
export function txQueryOptions(options) {
|
|
537
|
+
if (options === undefined)
|
|
538
|
+
return { _txScoped: true };
|
|
539
|
+
if (options._txScoped === true)
|
|
540
|
+
return options;
|
|
541
|
+
let tx = txOptionsCache.get(options);
|
|
542
|
+
if (tx === undefined) {
|
|
543
|
+
tx = { ...options, _txScoped: true };
|
|
544
|
+
txOptionsCache.set(options, tx);
|
|
545
|
+
}
|
|
546
|
+
return tx;
|
|
547
|
+
}
|
|
548
|
+
function templateCacheFor(options, schema, table, capacity) {
|
|
549
|
+
if (options === undefined)
|
|
550
|
+
return new LRUCache(capacity);
|
|
551
|
+
let bySchema = sharedTemplateCaches.get(options);
|
|
552
|
+
if (bySchema === undefined) {
|
|
553
|
+
bySchema = new WeakMap();
|
|
554
|
+
sharedTemplateCaches.set(options, bySchema);
|
|
555
|
+
}
|
|
556
|
+
let byTable = bySchema.get(schema);
|
|
557
|
+
if (byTable === undefined) {
|
|
558
|
+
byTable = new Map();
|
|
559
|
+
bySchema.set(schema, byTable);
|
|
560
|
+
}
|
|
561
|
+
let cache = byTable.get(table);
|
|
562
|
+
if (cache === undefined) {
|
|
563
|
+
cache = new LRUCache(capacity);
|
|
564
|
+
byTable.set(table, cache);
|
|
565
|
+
}
|
|
566
|
+
return cache;
|
|
567
|
+
}
|
|
505
568
|
export class QueryInterface {
|
|
506
569
|
pool;
|
|
507
570
|
table;
|
|
@@ -795,7 +858,7 @@ export class QueryInterface {
|
|
|
795
858
|
// treated as the default rather than throwing.
|
|
796
859
|
const sqlCacheSize = options?.sqlCacheSize;
|
|
797
860
|
this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
|
|
798
|
-
this.sqlTemplateCache =
|
|
861
|
+
this.sqlTemplateCache = templateCacheFor(options, schema, table, sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
|
|
799
862
|
this.dialect = options?.dialect ?? postgresDialect;
|
|
800
863
|
assertDialectCapabilities(this.dialect);
|
|
801
864
|
this.sqlCommenter =
|
|
@@ -2184,8 +2247,11 @@ export class QueryInterface {
|
|
|
2184
2247
|
return { baseArgs, strip: proj.strip };
|
|
2185
2248
|
}
|
|
2186
2249
|
/**
|
|
2187
|
-
*
|
|
2188
|
-
*
|
|
2250
|
+
* SQL template cache statistics. `hits`, `misses` and `hitRate` count this
|
|
2251
|
+
* accessor's own lookups. `size` is the entries in the cache it reads, which
|
|
2252
|
+
* since 0.90.2 is shared with every accessor built from the same client
|
|
2253
|
+
* options for this table: a client's primary and replica accessors share
|
|
2254
|
+
* one, and all of its transactions share another (see `templateCacheFor`).
|
|
2189
2255
|
*/
|
|
2190
2256
|
cacheStats() {
|
|
2191
2257
|
const total = this.cacheHits + this.cacheMisses;
|
|
@@ -2258,15 +2324,33 @@ export class QueryInterface {
|
|
|
2258
2324
|
this.variableArityShape = false;
|
|
2259
2325
|
this.cacheMisses++;
|
|
2260
2326
|
this.lastMissParams = built;
|
|
2327
|
+
return { sql, name: this.preparedNameOf(sql, variableArity), paramCount: built.length };
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* The prepared-statement name for `sql`: empty (sent unnamed) for a
|
|
2331
|
+
* variable-arity shape, else derived from the text.
|
|
2332
|
+
*/
|
|
2333
|
+
preparedNameOf(sql, variableArity) {
|
|
2334
|
+
if (variableArity)
|
|
2335
|
+
return '';
|
|
2261
2336
|
// With `sqlComment` putting a comment on every statement, the template's own
|
|
2262
2337
|
// name is never sent (the execute seam sends a `c_` name derived from it),
|
|
2263
2338
|
// so it is not claimed from the budget: only names that reach the wire do.
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2339
|
+
return this.sqlCommenter?.alwaysComments ? sqlToPreparedName(sql) : boundedPreparedName(sql);
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Name a statement the SQL template cache does not hold (`aggregate`,
|
|
2343
|
+
* `groupBy`): the build runs between a reset and a read of the
|
|
2344
|
+
* variable-arity flag, exactly as in {@link buildCacheEntry}, so the same
|
|
2345
|
+
* shapes stay unnamed. These used to be sent unnamed always, so with
|
|
2346
|
+
* prepared statements on the server re-parsed every `aggregate`.
|
|
2347
|
+
*/
|
|
2348
|
+
withPreparedName(build) {
|
|
2349
|
+
this.variableArityShape = false;
|
|
2350
|
+
const deferred = build();
|
|
2351
|
+
const variableArity = this.variableArityShape;
|
|
2352
|
+
this.variableArityShape = false;
|
|
2353
|
+
return { ...deferred, preparedName: this.preparedNameOf(deferred.sql, variableArity) };
|
|
2270
2354
|
}
|
|
2271
2355
|
/**
|
|
2272
2356
|
* The SQL-cache lockstep checks (see the comment above `CACHE_MISMATCH_WARN_NS`).
|
|
@@ -4227,6 +4311,8 @@ export class QueryInterface {
|
|
|
4227
4311
|
return rows.length > 0 ? rows[0] : null;
|
|
4228
4312
|
},
|
|
4229
4313
|
tag: `${this.table}.findFirst`,
|
|
4314
|
+
// findMany's name: the same statement, so the same prepared statement.
|
|
4315
|
+
...(deferred.preparedName !== undefined ? { preparedName: deferred.preparedName } : {}),
|
|
4230
4316
|
...(deferred.rowLock ? { rowLock: true } : {}),
|
|
4231
4317
|
};
|
|
4232
4318
|
}
|
|
@@ -4260,6 +4346,7 @@ export class QueryInterface {
|
|
|
4260
4346
|
return row;
|
|
4261
4347
|
},
|
|
4262
4348
|
tag: `${this.table}.findFirstOrThrow`,
|
|
4349
|
+
...(inner.preparedName !== undefined ? { preparedName: inner.preparedName } : {}),
|
|
4263
4350
|
...(inner.rowLock ? { rowLock: true } : {}),
|
|
4264
4351
|
};
|
|
4265
4352
|
}
|
|
@@ -4293,6 +4380,7 @@ export class QueryInterface {
|
|
|
4293
4380
|
return row;
|
|
4294
4381
|
},
|
|
4295
4382
|
tag: `${this.table}.findUniqueOrThrow`,
|
|
4383
|
+
...(inner.preparedName !== undefined ? { preparedName: inner.preparedName } : {}),
|
|
4296
4384
|
...(inner.rowLock ? { rowLock: true } : {}),
|
|
4297
4385
|
};
|
|
4298
4386
|
}
|
|
@@ -4462,7 +4550,10 @@ export class QueryInterface {
|
|
|
4462
4550
|
const pool = this.pool;
|
|
4463
4551
|
const schema = this.schema;
|
|
4464
4552
|
const middlewares = this.middlewares;
|
|
4465
|
-
|
|
4553
|
+
// The client's transaction options object, not a copy per call: the SQL
|
|
4554
|
+
// template cache is shared by options-object identity, so a fresh copy
|
|
4555
|
+
// rebuilt the SQL of every child statement of every nested write.
|
|
4556
|
+
const opts = txQueryOptions(this.options);
|
|
4466
4557
|
return {
|
|
4467
4558
|
schema,
|
|
4468
4559
|
tx: unlockNestedWriteTx(this.makeTxProxy(pool, schema, middlewares, opts)),
|
|
@@ -4653,10 +4744,12 @@ export class QueryInterface {
|
|
|
4653
4744
|
// Aggregate / groupBy compilation (extracted to aggregates.ts).
|
|
4654
4745
|
// ---------------------------------------------------------------------------
|
|
4655
4746
|
buildGroupBy(args) {
|
|
4656
|
-
|
|
4747
|
+
const normalized = this.normalizeArgs(args);
|
|
4748
|
+
return this.withPreparedName(() => aggMod.buildGroupBy(this.ctx, normalized));
|
|
4657
4749
|
}
|
|
4658
4750
|
buildAggregate(args) {
|
|
4659
|
-
|
|
4751
|
+
const normalized = this.normalizeArgs(args);
|
|
4752
|
+
return this.withPreparedName(() => aggMod.buildAggregate(this.ctx, normalized));
|
|
4660
4753
|
}
|
|
4661
4754
|
// -------------------------------------------------------------------------
|
|
4662
4755
|
// aggregate, standalone aggregation without groupBy
|
package/dist/query/writes.js
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { maxBindParameters, } from '../dialect.js';
|
|
14
14
|
import { NotFoundError, OptimisticLockError, UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
15
|
-
import { camelToSnake } from '../schema.js';
|
|
16
15
|
import { isColumnRef } from './column-ref.js';
|
|
17
16
|
import { assertMutationWhereIdentifiesOneRow, expandCompoundUniqueWhere, otherCollidingUniqueKey, uniqueKeyNames, upsertCreateCollidingKey, upsertWhereMatchesCreate, } from './compound-unique.js';
|
|
18
17
|
import { compileSetExpr, expr as exprBuilders, exprNotAllowedMessage, isPlainExprShape, isSqlExpr, plainExprShapeMessage, resolveExcludedFromCreate, } from './expr.js';
|
|
@@ -755,6 +754,10 @@ function definedKeys(qi, row) {
|
|
|
755
754
|
function assertUniformCreateManyRows(qi, rows, firstKeys) {
|
|
756
755
|
const expected = new Set(firstKeys);
|
|
757
756
|
for (let i = 1; i < rows.length; i++) {
|
|
757
|
+
// The common case, decided without ordering the row's keys: as many
|
|
758
|
+
// defined keys as the first row, each one of its keys.
|
|
759
|
+
if (namesExactly(rows[i], expected))
|
|
760
|
+
continue;
|
|
758
761
|
const rowKeys = definedKeys(qi, rows[i]);
|
|
759
762
|
const unexpected = rowKeys.filter((k) => !expected.has(k));
|
|
760
763
|
// No stranger and the same count means the same set (object keys are unique).
|
|
@@ -775,6 +778,18 @@ function assertUniformCreateManyRows(qi, rows, firstKeys) {
|
|
|
775
778
|
'row shape.');
|
|
776
779
|
}
|
|
777
780
|
}
|
|
781
|
+
/** Whether `row`'s defined keys are exactly `keys` (object keys are unique, so count plus membership decides it). */
|
|
782
|
+
function namesExactly(row, keys) {
|
|
783
|
+
let count = 0;
|
|
784
|
+
for (const key in row) {
|
|
785
|
+
if (!Object.hasOwn(row, key) || row[key] === undefined)
|
|
786
|
+
continue;
|
|
787
|
+
if (!keys.has(key))
|
|
788
|
+
return false;
|
|
789
|
+
count++;
|
|
790
|
+
}
|
|
791
|
+
return count === keys.size;
|
|
792
|
+
}
|
|
778
793
|
/** `a, b` → `"a", "b"`, for the field lists in {@link assertUniformCreateManyRows}. */
|
|
779
794
|
function quoteList(fields) {
|
|
780
795
|
return fields.map((f) => `"${f}"`).join(', ');
|
|
@@ -857,8 +872,15 @@ function prepareCreateMany(qi, args, projection) {
|
|
|
857
872
|
};
|
|
858
873
|
}
|
|
859
874
|
assertWritable(qi, 'createMany');
|
|
860
|
-
|
|
861
|
-
|
|
875
|
+
args.data.forEach((row, i) => {
|
|
876
|
+
if (row === null || typeof row !== 'object') {
|
|
877
|
+
throw new ValidationError(`createMany on "${qi.table}": row ${i} is ${row === null ? 'null' : `a ${typeof row}`}, not an object of fields.`);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
const generated = generatedStoredColumns(qi);
|
|
881
|
+
if (generated !== null) {
|
|
882
|
+
for (const row of args.data)
|
|
883
|
+
assertNoGeneratedKeys(qi, generated, row, 'createMany');
|
|
862
884
|
}
|
|
863
885
|
const keys = definedKeys(qi, args.data[0]);
|
|
864
886
|
assertUniformCreateManyRows(qi, args.data, keys);
|
|
@@ -1054,11 +1076,13 @@ function describeConflictTargets(qi) {
|
|
|
1054
1076
|
}
|
|
1055
1077
|
/** The conflict-UPDATE's data: the field-list shorthand becomes `{ f: expr.excluded(f) }`. */
|
|
1056
1078
|
function resolveConflictUpdate(qi, raw) {
|
|
1057
|
-
|
|
1079
|
+
// A function, not an error built up front: constructing one captures a
|
|
1080
|
+
// stack trace, and this runs on every onConflict call.
|
|
1081
|
+
const empty = () => new ValidationError(`createMany on "${qi.table}": onConflict.update is empty; use onConflict: { target, ignore: true } to skip ` +
|
|
1058
1082
|
'conflicting rows.');
|
|
1059
1083
|
if (Array.isArray(raw)) {
|
|
1060
1084
|
if (raw.length === 0)
|
|
1061
|
-
throw empty;
|
|
1085
|
+
throw empty();
|
|
1062
1086
|
const update = {};
|
|
1063
1087
|
for (const field of raw) {
|
|
1064
1088
|
if (typeof field !== 'string') {
|
|
@@ -1075,7 +1099,7 @@ function resolveConflictUpdate(qi, raw) {
|
|
|
1075
1099
|
throw new ValidationError(`createMany on "${qi.table}": onConflict.update takes a list of field names or a { field: value } object.`);
|
|
1076
1100
|
}
|
|
1077
1101
|
if (!Object.values(raw).some((v) => v !== undefined))
|
|
1078
|
-
throw empty;
|
|
1102
|
+
throw empty();
|
|
1079
1103
|
assertNoGeneratedColumns(qi, raw, 'createMany');
|
|
1080
1104
|
return raw;
|
|
1081
1105
|
}
|
|
@@ -1131,12 +1155,15 @@ function keyToken(value) {
|
|
|
1131
1155
|
function assertNoDuplicateKeys(qi, conflict, columns, rowValues) {
|
|
1132
1156
|
const at = conflict.target.map((c) => columns.indexOf(c));
|
|
1133
1157
|
const seen = new Map();
|
|
1134
|
-
for (let i = 0; i < rowValues.length; i++) {
|
|
1158
|
+
rows: for (let i = 0; i < rowValues.length; i++) {
|
|
1135
1159
|
const row = rowValues[i];
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1160
|
+
let key = '';
|
|
1161
|
+
for (let k = 0; k < at.length; k++) {
|
|
1162
|
+
const value = row[at[k]];
|
|
1163
|
+
if (value === null || value === undefined)
|
|
1164
|
+
continue rows;
|
|
1165
|
+
key += k === 0 ? keyToken(value) : `|${keyToken(value)}`;
|
|
1166
|
+
}
|
|
1140
1167
|
const first = seen.get(key);
|
|
1141
1168
|
if (first !== undefined) {
|
|
1142
1169
|
throw new ValidationError(`createMany onConflict on "${qi.table}": rows ${first} and ${i} share the conflict key ` +
|
|
@@ -2101,11 +2128,34 @@ export function assertWritable(qi, operation) {
|
|
|
2101
2128
|
* (they're stripped from the statement anyway).
|
|
2102
2129
|
*/
|
|
2103
2130
|
export function assertNoGeneratedColumns(qi, data, operation) {
|
|
2104
|
-
|
|
2105
|
-
|
|
2131
|
+
const generated = generatedStoredColumns(qi);
|
|
2132
|
+
if (generated !== null)
|
|
2133
|
+
assertNoGeneratedKeys(qi, generated, data, operation);
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* The table's generated stored columns, or `null` when it has none (most
|
|
2137
|
+
* tables, and then a write checks nothing). Read once per call, so
|
|
2138
|
+
* `createMany` checks every row against one answer: it used to search the
|
|
2139
|
+
* column list, with a `camelToSnake` per comparison, for every field of every
|
|
2140
|
+
* row, which was most of a 150-row upsert's build time.
|
|
2141
|
+
*/
|
|
2142
|
+
function generatedStoredColumns(qi) {
|
|
2143
|
+
let columns = null;
|
|
2144
|
+
for (const c of qi.tableMeta.columns) {
|
|
2145
|
+
if (!c.isGeneratedStored)
|
|
2106
2146
|
continue;
|
|
2107
|
-
|
|
2108
|
-
|
|
2147
|
+
columns ??= new Set();
|
|
2148
|
+
columns.add(c.name);
|
|
2149
|
+
}
|
|
2150
|
+
return columns;
|
|
2151
|
+
}
|
|
2152
|
+
/** Refuse a key of `data` that names one of `generated` (by THE key rule, {@link resolveColumnName}). */
|
|
2153
|
+
function assertNoGeneratedKeys(qi, generated, data, operation) {
|
|
2154
|
+
for (const key in data) {
|
|
2155
|
+
if (!Object.hasOwn(data, key) || data[key] === undefined)
|
|
2156
|
+
continue;
|
|
2157
|
+
const column = resolveColumnName(qi.tableMeta, key);
|
|
2158
|
+
if (column !== undefined && generated.has(column)) {
|
|
2109
2159
|
throw new ValidationError(`Cannot ${operation} "${qi.table}": column "${key}" is a GENERATED ALWAYS AS (…) STORED ` +
|
|
2110
2160
|
'column whose value the database computes, remove it from your data.');
|
|
2111
2161
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.90.
|
|
3
|
+
"version": "0.90.2-next.91484a2",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
|