turbine-orm 0.81.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
|
@@ -46,109 +46,78 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
46
46
|
})();
|
|
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
|
-
exports.resetCacheCrossCheckEnv = resetCacheCrossCheckEnv;
|
|
50
49
|
exports.unlockNestedWriteTx = unlockNestedWriteTx;
|
|
51
50
|
const checkout_js_1 = require("../checkout.js");
|
|
52
51
|
const connection_guard_js_1 = require("../connection-guard.js");
|
|
52
|
+
const diagnostics_js_1 = require("../diagnostics.js");
|
|
53
53
|
const dialect_js_1 = require("../dialect.js");
|
|
54
54
|
const errors_js_1 = require("../errors.js");
|
|
55
55
|
const index_advisor_js_1 = require("../index-advisor.js");
|
|
56
|
+
const logger_js_1 = require("../logger.js");
|
|
56
57
|
const nested_write_js_1 = require("../nested-write.js");
|
|
57
58
|
const schema_js_1 = require("../schema.js");
|
|
59
|
+
const transaction_state_js_1 = require("../transaction-state.js");
|
|
58
60
|
const aggMod = __importStar(require("./aggregates.js"));
|
|
59
61
|
const batched_loader_js_1 = require("./batched-loader.js");
|
|
60
62
|
const compound_unique_js_1 = require("./compound-unique.js");
|
|
63
|
+
const expr_js_1 = require("./expr.js");
|
|
61
64
|
const filters_js_1 = require("./filters.js");
|
|
65
|
+
const normalize_args_js_1 = require("./normalize-args.js");
|
|
62
66
|
const option_surface_js_1 = require("./option-surface.js");
|
|
63
67
|
const relation_names_js_1 = require("./relation-names.js");
|
|
64
68
|
const relationsMod = __importStar(require("./relations.js"));
|
|
69
|
+
const row_lock_js_1 = require("./row-lock.js");
|
|
70
|
+
const sql_comment_js_1 = require("./sql-comment.js");
|
|
71
|
+
const stream_idle_js_1 = require("./stream-idle.js");
|
|
65
72
|
const types_js_1 = require("./types.js");
|
|
66
73
|
const utils_js_1 = require("./utils.js");
|
|
67
74
|
const warn_registry_js_1 = require("./warn-registry.js");
|
|
68
75
|
const whereMod = __importStar(require("./where.js"));
|
|
69
76
|
const writesMod = __importStar(require("./writes.js"));
|
|
70
77
|
/**
|
|
71
|
-
* SQL-cache lockstep cross-check
|
|
78
|
+
* SQL-cache lockstep cross-check.
|
|
72
79
|
*
|
|
73
80
|
* The SQL template cache requires three code paths to enumerate where-clause
|
|
74
81
|
* keys identically: `fingerprintWhere` (builds the cache key),
|
|
75
82
|
* `buildWhereClause` (builds SQL + `$N` params on a MISS), and
|
|
76
83
|
* `collectWhereParams` (re-collects params on a HIT without rebuilding). Since
|
|
77
84
|
* 0.36 all three consume ONE shared where-key walk (see `where-compile.ts`), so
|
|
78
|
-
* enumeration drift is structurally eliminated;
|
|
85
|
+
* enumeration drift is structurally eliminated; these checks remain as a
|
|
79
86
|
* tripwire in case a future leaf builder or its collect mirror falls out of
|
|
80
|
-
* step.
|
|
81
|
-
*
|
|
87
|
+
* step. Every failure is an {@link InvariantError} (E020), thrown before the
|
|
88
|
+
* statement is sent.
|
|
82
89
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* -
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* -
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
* throws, same as dev. `0`, unset, or an unparseable value means never check.
|
|
93
|
-
*/
|
|
94
|
-
function cacheCrossCheckMode() {
|
|
95
|
-
const env = crossCheckEnv ?? readCrossCheckEnv();
|
|
96
|
-
if (env.mode !== 'sampled')
|
|
97
|
-
return env.mode;
|
|
98
|
-
// The SAMPLING decision, and only it, is genuinely per hit.
|
|
99
|
-
return env.rate >= 1 || Math.random() < env.rate ? 'sampled' : 'off';
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* The env-derived half of {@link cacheCrossCheckMode}, resolved ONCE per
|
|
103
|
-
* process and memoized.
|
|
104
|
-
*
|
|
105
|
-
* `process.env` is not a plain object: every property read crosses a C++
|
|
106
|
-
* interceptor. Measured at 112.7 ns per read against 3.6 ns for a module-level
|
|
107
|
-
* constant, and this function ran TWO reads on every cache hit, which put it
|
|
108
|
-
* at the top of the SQL-build CPU profile (12.53% self time, with a further
|
|
109
|
-
* ~12% inside the interceptor) for a value that does not change.
|
|
110
|
-
*
|
|
111
|
-
* It is memoized rather than hoisted to a `const` because the toggle IS
|
|
112
|
-
* observed at runtime, contrary to what a quick look suggests: the
|
|
113
|
-
* cross-check and sampling suites flip `NODE_ENV` /
|
|
114
|
-
* `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE` around a
|
|
115
|
-
* synchronous block in-process, not at spawn. So the read is deferred to first
|
|
116
|
-
* use and {@link resetCacheCrossCheckEnv} re-arms it; that is the entire
|
|
117
|
-
* contract, and the only thing lost versus reading every time is that a
|
|
118
|
-
* process which mutates these variables mid-run must say so.
|
|
119
|
-
*/
|
|
120
|
-
let crossCheckEnv;
|
|
121
|
-
function readCrossCheckEnv() {
|
|
122
|
-
let resolved;
|
|
123
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
124
|
-
resolved = { mode: process.env.TURBINE_DISABLE_CACHE_CHECK === '1' ? 'off' : 'dev' };
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
const raw = process.env.TURBINE_CACHE_CHECK_SAMPLE;
|
|
128
|
-
const rate = raw === undefined ? Number.NaN : Number.parseFloat(raw);
|
|
129
|
-
resolved = !Number.isFinite(rate) || rate <= 0 ? { mode: 'off' } : { mode: 'sampled', rate };
|
|
130
|
-
}
|
|
131
|
-
crossCheckEnv = resolved;
|
|
132
|
-
return resolved;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Discard the memoized cross-check environment so the next cache hit re-reads
|
|
136
|
-
* `process.env`.
|
|
90
|
+
* Two checks always run, whatever the diagnostics mode, because each costs one
|
|
91
|
+
* array compare and the alternative is executing parameters known to be wrong:
|
|
92
|
+
* - On a MISS, the params the build closure pushed are compared with the params
|
|
93
|
+
* the collect path produced (`sql-cache-lockstep`). The miss already paid
|
|
94
|
+
* for both walks, and a shape run once per instance is otherwise never
|
|
95
|
+
* compared at all.
|
|
96
|
+
* - On a HIT, the collected param count must equal the count the entry was
|
|
97
|
+
* built with (`sql-cache-param-count`). Otherwise the server would reject
|
|
98
|
+
* the statement with a bind error that reads like a connection problem.
|
|
137
99
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
100
|
+
* The full rebuild-and-compare on a HIT follows the client's
|
|
101
|
+
* `diagnostics.cacheCheck`, resolved once at construction (see diagnostics.ts;
|
|
102
|
+
* the legacy `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE`
|
|
103
|
+
* variables are read there and nowhere else):
|
|
104
|
+
* - `'every-hit'` (the development default): every hit is compared, and a
|
|
105
|
+
* mismatch throws.
|
|
106
|
+
* - `{ sample }`: that fraction of hits is compared (per-hit `Math.random()`;
|
|
107
|
+
* a sample of 1 compares every hit). A mismatch is logged at `error` once
|
|
108
|
+
* per distinct fingerprint, then throws.
|
|
109
|
+
* - `'off'` (the production default): never rebuilt on a hit.
|
|
141
110
|
*/
|
|
142
|
-
function resetCacheCrossCheckEnv() {
|
|
143
|
-
crossCheckEnv = undefined;
|
|
144
|
-
}
|
|
145
111
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
112
|
+
* Warn-once namespace for the sampled cross-check's mismatch log, so a hot
|
|
113
|
+
* mismatch logs exactly once per table and shape instead of flooding the log.
|
|
114
|
+
* `'every-hit'` does not log (it throws immediately), so only the sampled path
|
|
115
|
+
* records here. Keyed on the TABLE as well as the fingerprint: the fingerprint
|
|
116
|
+
* alone carries no table, so a mismatch on one table used to silence the same
|
|
117
|
+
* shape's mismatch on every other table. The shared registry also bounds the
|
|
118
|
+
* set (`WARN_ONCE_CAP`) and is what `resetWarnOnce` clears.
|
|
150
119
|
*/
|
|
151
|
-
const
|
|
120
|
+
const CACHE_MISMATCH_WARN_NS = 'cacheMismatch';
|
|
152
121
|
/**
|
|
153
122
|
* Marginal cost of keeping a to-one relation on the JOIN plan, per parent row.
|
|
154
123
|
*
|
|
@@ -306,6 +275,29 @@ const ROW_PLAN_CACHE_SIZE = 128;
|
|
|
306
275
|
* probe's own limit.
|
|
307
276
|
*/
|
|
308
277
|
const STREAM_SPECULATION_MAX_BATCH = 1000;
|
|
278
|
+
/** Per-cell decode work in a {@link RowDecodePlan}. */
|
|
279
|
+
const CELL_PLAIN = 0;
|
|
280
|
+
const CELL_TEMPORAL = 1;
|
|
281
|
+
const CELL_FLOAT = 2;
|
|
282
|
+
const CELL_FLOAT_ARRAY = 3;
|
|
283
|
+
const CELL_TEXT_ARRAY = 4;
|
|
284
|
+
/** The dialect's own decode for a type whose storage differs (see `Dialect.decodeColumnValue`). */
|
|
285
|
+
const CELL_DIALECT = 5;
|
|
286
|
+
const FLOAT_TYPES = new Set(['float4', 'float8', 'real', 'double precision']);
|
|
287
|
+
const FLOAT_ARRAY_TYPES = new Set(['_float4', '_float8', 'float4[]', 'float8[]', 'real[]', 'double precision[]']);
|
|
288
|
+
/**
|
|
289
|
+
* The decode a column of `pgType` needs beyond the driver's, other than
|
|
290
|
+
* temporal: a float can arrive from JSON as "NaN" / "Infinity", and an enum
|
|
291
|
+
* array, which the driver has no parser for, as array text.
|
|
292
|
+
*/
|
|
293
|
+
function cellKindOf(pgType, enums) {
|
|
294
|
+
if (FLOAT_TYPES.has(pgType))
|
|
295
|
+
return CELL_FLOAT;
|
|
296
|
+
if (FLOAT_ARRAY_TYPES.has(pgType))
|
|
297
|
+
return CELL_FLOAT_ARRAY;
|
|
298
|
+
const element = pgType.startsWith('_') ? pgType.slice(1) : pgType.endsWith('[]') ? pgType.slice(0, -2) : undefined;
|
|
299
|
+
return element !== undefined && enums !== undefined && Object.hasOwn(enums, element) ? CELL_TEXT_ARRAY : CELL_PLAIN;
|
|
300
|
+
}
|
|
309
301
|
/**
|
|
310
302
|
* Strict structural equality for a single SQL parameter value. Handles the
|
|
311
303
|
* value shapes Turbine binds: primitives (incl. `NaN` and `bigint`), `null`/
|
|
@@ -448,35 +440,95 @@ function unlockNestedWriteTx(tx) {
|
|
|
448
440
|
*/
|
|
449
441
|
// Moved to ./filters.ts in 0.74.0 so PowqlInterface consumes the SAME
|
|
450
442
|
// predicate rather than a second copy of it. See applyStableRelationOrderTo.
|
|
443
|
+
/**
|
|
444
|
+
* Marks `findMany` args built for a `findFirst`: one row, not a page, so
|
|
445
|
+
* {@link QueryInterface.completePageOrder} leaves its order as written. A
|
|
446
|
+
* symbol, so it survives the spreads at the args seam and no JSON body can
|
|
447
|
+
* set it. Set through {@link firstRowOnly} only.
|
|
448
|
+
*/
|
|
449
|
+
const FIRST_ROW_ONLY = Symbol('turbine.firstRowOnly');
|
|
450
|
+
/**
|
|
451
|
+
* The {@link FIRST_ROW_ONLY} mark for a read the caller bounded with neither
|
|
452
|
+
* `limit` nor `offset`. An `offset` makes the row depend on which tied rows it
|
|
453
|
+
* skipped, so `findFirst({ orderBy, offset })` is a page of one and gets the
|
|
454
|
+
* page tiebreaker like any other page.
|
|
455
|
+
*/
|
|
456
|
+
function firstRowOnly(args) {
|
|
457
|
+
return args?.limit === undefined && args?.offset === undefined ? { [FIRST_ROW_ONLY]: true } : {};
|
|
458
|
+
}
|
|
459
|
+
/** The largest delay `setTimeout` honours; Node fires anything larger after 1 ms. */
|
|
460
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
461
|
+
/** The accepted `relationLoadStrategy` values. */
|
|
462
|
+
const RELATION_LOAD_STRATEGIES = Object.freeze([
|
|
463
|
+
'join',
|
|
464
|
+
'batched',
|
|
465
|
+
'auto',
|
|
466
|
+
'flatten',
|
|
467
|
+
]);
|
|
468
|
+
/** A per-query `timeout`: milliseconds up to {@link MAX_TIMEOUT_MS}, or 0 for none (the same rule as `$transaction`). */
|
|
469
|
+
function isValidTimeout(value) {
|
|
470
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= MAX_TIMEOUT_MS;
|
|
471
|
+
}
|
|
472
|
+
/** A short, value-safe rendering of a rejected option value. */
|
|
473
|
+
function describeOptionValue(value) {
|
|
474
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
475
|
+
return String(value);
|
|
476
|
+
if (typeof value === 'string')
|
|
477
|
+
return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}...` : value);
|
|
478
|
+
return value === null ? 'null' : `a value of type ${typeof value}`;
|
|
479
|
+
}
|
|
480
|
+
function relationLoadStrategyMessage(value) {
|
|
481
|
+
return (`\`relationLoadStrategy\` must be one of ${RELATION_LOAD_STRATEGIES.map((v) => `'${v}'`).join(', ')} ` +
|
|
482
|
+
`(received ${describeOptionValue(value)}).`);
|
|
483
|
+
}
|
|
484
|
+
/** A value the findUnique simple path may bind as it is: a primitive, Date or Buffer. */
|
|
485
|
+
function isFastPathScalar(value) {
|
|
486
|
+
switch (typeof value) {
|
|
487
|
+
case 'string':
|
|
488
|
+
case 'number':
|
|
489
|
+
case 'bigint':
|
|
490
|
+
case 'boolean':
|
|
491
|
+
return true;
|
|
492
|
+
case 'object':
|
|
493
|
+
return value instanceof Date || (value !== null && ArrayBuffer.isView(value));
|
|
494
|
+
default:
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/** Resolves a race when a deadline passes first. */
|
|
499
|
+
const TIMED_OUT = Symbol('timedOut');
|
|
500
|
+
/**
|
|
501
|
+
* How long a statement that outlived its `timeout` gets, after the cancel
|
|
502
|
+
* request is sent, to answer before its connection is destroyed instead of
|
|
503
|
+
* reused. A cancel normally lands within a round trip or two.
|
|
504
|
+
*/
|
|
505
|
+
const CANCEL_GRACE_MS = 2000;
|
|
506
|
+
/** Whether `err` is PostgreSQL's query_canceled (57014), as the driver raised it or as Turbine wrapped it. */
|
|
507
|
+
function isQueryCanceled(err) {
|
|
508
|
+
const e = err;
|
|
509
|
+
return e?.code === '57014' || e?.cause?.code === '57014';
|
|
510
|
+
}
|
|
511
|
+
/** The {@link DeferredQuery.rowLock} marker for a locking read, or nothing. */
|
|
512
|
+
function rowLockMarker(rowLock) {
|
|
513
|
+
return rowLock ? { rowLock: true } : {};
|
|
514
|
+
}
|
|
451
515
|
/** The two accepted {@link JsonEncoding} values, frozen so the check is total. */
|
|
452
516
|
const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
|
|
453
517
|
/**
|
|
454
518
|
* The relation JSON encoding a client on `dialect` gets when it names none.
|
|
455
519
|
*
|
|
456
|
-
* `'positional'` (`json_build_array`)
|
|
457
|
-
* (`json_build_object`) everywhere else. The positional form drops
|
|
458
|
-
* key names from every relation row: measured on a 50-parent /
|
|
459
|
-
* against local PostgreSQL 17, server time 0.685 ms → 0.350 ms
|
|
460
|
-
* 100 KB on the wire, returning byte-identical parsed rows.
|
|
461
|
-
*
|
|
462
|
-
* ## Why the test is `dialect.name`, not "does the dialect have buildJsonArray"
|
|
463
|
-
*
|
|
464
|
-
* Because a presence test does not distinguish engines HERE. `buildJsonArray`
|
|
465
|
-
* is declared on `postgresDialect`, and sqlite.ts / mysql.ts / mssql.ts /
|
|
466
|
-
* powdb.ts each build their dialect by SPREADING it, so every engine inherits
|
|
467
|
-
* the hook and an "is it absent" fallback never fires. That is the documented
|
|
468
|
-
* inheritance trap (see `buildPartitionLimit` in {@link
|
|
469
|
-
* QueryInterface.batchedContext} and the `distinct` gate in
|
|
470
|
-
* {@link QueryInterface.buildFindMany}), and it is exactly the shape that
|
|
471
|
-
* silently turned the partition-limit window on for SQLite.
|
|
520
|
+
* `'positional'` (`json_build_array`) where the engine has it (PostgreSQL),
|
|
521
|
+
* `'object'` (`json_build_object`) everywhere else. The positional form drops
|
|
522
|
+
* the repeated key names from every relation row: measured on a 50-parent /
|
|
523
|
+
* ~10-child read against local PostgreSQL 17, server time 0.685 ms → 0.350 ms
|
|
524
|
+
* and 152 KB → 100 KB on the wire, returning byte-identical parsed rows.
|
|
472
525
|
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
* cannot.
|
|
526
|
+
* Read from `capabilities.positionalJson`, never from whether the dialect has
|
|
527
|
+
* `buildJsonArray`: that hook is on `postgresDialect` and every engine inherits
|
|
528
|
+
* it by spreading, so a presence test would hand every engine the positional
|
|
529
|
+
* default. `buildSelectWithRelations` (relations.ts) refuses `'positional'` on
|
|
530
|
+
* the SAME flag, so the default can never select an encoding the builder then
|
|
531
|
+
* refuses: one authority, not two.
|
|
480
532
|
*
|
|
481
533
|
* A wire-compatible engine that reaches this on `postgresDialect` itself
|
|
482
534
|
* (CockroachDB, YugabyteDB, AlloyDB, Timescale, all of which are ADAPTERS over
|
|
@@ -484,9 +536,8 @@ const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
|
|
|
484
536
|
* which is correct: they speak `json_build_array`.
|
|
485
537
|
*/
|
|
486
538
|
function defaultJsonEncoding(dialect) {
|
|
487
|
-
return dialect.
|
|
539
|
+
return dialect.capabilities.positionalJson ? 'positional' : 'object';
|
|
488
540
|
}
|
|
489
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no relations known", intentional for untyped table access
|
|
490
541
|
class QueryInterface {
|
|
491
542
|
pool;
|
|
492
543
|
table;
|
|
@@ -507,6 +558,13 @@ class QueryInterface {
|
|
|
507
558
|
* inline, not through the top-level cache).
|
|
508
559
|
*/
|
|
509
560
|
lastCacheHit = false;
|
|
561
|
+
/**
|
|
562
|
+
* The params the build closure pushed on the most recent MISS, held until
|
|
563
|
+
* {@link crossCheckCache} compares them with the collected params and clears
|
|
564
|
+
* them. `undefined` after a hit. Same single-flag reasoning as
|
|
565
|
+
* {@link lastCacheHit}.
|
|
566
|
+
*/
|
|
567
|
+
lastMissParams;
|
|
510
568
|
/**
|
|
511
569
|
* Set while a build closure runs when the statement's WHERE (or HAVING)
|
|
512
570
|
* carries a caller-written `AND`/`OR` combinator array, i.e. when its SQL text
|
|
@@ -518,6 +576,21 @@ class QueryInterface {
|
|
|
518
576
|
*/
|
|
519
577
|
variableArityShape = false;
|
|
520
578
|
middlewares;
|
|
579
|
+
/** The client's resolved `diagnostics` (see diagnostics.ts), fixed for this instance's life. */
|
|
580
|
+
diagnostics;
|
|
581
|
+
/** `streamIdleTimeoutMs`: how long a cursor stream may wait for its consumer. `0` disables. */
|
|
582
|
+
streamIdleTimeoutMs;
|
|
583
|
+
/** `sqlComment`, resolved; `undefined` when statements carry no comment (one falsy check per send). */
|
|
584
|
+
sqlCommenter;
|
|
585
|
+
/** Where this instance's log lines go: the client's `logger`, else the console logger. */
|
|
586
|
+
logger;
|
|
587
|
+
/**
|
|
588
|
+
* {@link logger} when `diagnostics.advice` is on, else `undefined`. Every
|
|
589
|
+
* per-query advice site starts with `const log = this.advice; if (!log)
|
|
590
|
+
* return;`, so with advice off a site costs one property read and builds no
|
|
591
|
+
* message.
|
|
592
|
+
*/
|
|
593
|
+
advice;
|
|
521
594
|
defaultLimit;
|
|
522
595
|
warnOnUnlimited;
|
|
523
596
|
scopedConnect;
|
|
@@ -619,7 +692,6 @@ class QueryInterface {
|
|
|
619
692
|
* the audit's "Set<string>" guidance and leaves room for future
|
|
620
693
|
* cross-table sharing.
|
|
621
694
|
*/
|
|
622
|
-
warnedTables = new Set();
|
|
623
695
|
/** Cache hit/miss counters for diagnostics */
|
|
624
696
|
cacheHits = 0;
|
|
625
697
|
cacheMisses = 0;
|
|
@@ -640,6 +712,10 @@ class QueryInterface {
|
|
|
640
712
|
* to Date as well (otherwise nested dates leak through as strings).
|
|
641
713
|
*/
|
|
642
714
|
camelDateFieldCache = new Map();
|
|
715
|
+
/** Per-table memo of {@link cellKindOf}, keyed by column name and field name. */
|
|
716
|
+
cellKindCache = new Map();
|
|
717
|
+
/** The dialect decode of each {@link CELL_DIALECT} column, by table, then by column and field name. */
|
|
718
|
+
dialectDecodeCache = new Map();
|
|
643
719
|
/**
|
|
644
720
|
* Per-table memo of `Object.entries(meta.relations)`, consumed by the nested
|
|
645
721
|
* row parser (see `getRelationEntries` in relations.ts). Same rationale and
|
|
@@ -713,7 +789,14 @@ class QueryInterface {
|
|
|
713
789
|
}
|
|
714
790
|
this.tableMeta = meta;
|
|
715
791
|
this.middlewares = middlewares ?? [];
|
|
792
|
+
// Resolved by the client once; a QueryInterface built directly (tests,
|
|
793
|
+
// tooling) resolves here, at construction, never per call.
|
|
794
|
+
this.diagnostics = (0, diagnostics_js_1.resolveDiagnostics)(options?.diagnostics);
|
|
795
|
+
this.logger = options?.logger ?? logger_js_1.consoleLogger;
|
|
796
|
+
this.advice = this.diagnostics.advice ? this.logger : undefined;
|
|
716
797
|
this.defaultLimit = options?.defaultLimit;
|
|
798
|
+
this.streamIdleTimeoutMs =
|
|
799
|
+
(0, stream_idle_js_1.resolveStreamIdleTimeout)(options?.streamIdleTimeoutMs, 'streamIdleTimeoutMs') ?? stream_idle_js_1.DEFAULT_STREAM_IDLE_TIMEOUT_MS;
|
|
717
800
|
// Default to ON: surfacing accidental full-table scans is more valuable
|
|
718
801
|
// than the (small) risk of noisy logs. Callers opt out globally with
|
|
719
802
|
// `warnOnUnlimited: false`, per table with `warnOnUnlimited: { users:
|
|
@@ -750,6 +833,17 @@ class QueryInterface {
|
|
|
750
833
|
this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
|
|
751
834
|
this.sqlTemplateCache = new utils_js_1.LRUCache(sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
|
|
752
835
|
this.dialect = options?.dialect ?? dialect_js_1.postgresDialect;
|
|
836
|
+
this.sqlCommenter =
|
|
837
|
+
options?._sqlCommenter ??
|
|
838
|
+
(0, sql_comment_js_1.resolveSqlComment)(options?.sqlComment, this.logger, {
|
|
839
|
+
dialect: this.dialect.name,
|
|
840
|
+
preparesEveryStatement: this.dialect.capabilities.preparesEveryStatement,
|
|
841
|
+
buildsSql: true,
|
|
842
|
+
});
|
|
843
|
+
if (options?.relationLoadStrategy !== undefined &&
|
|
844
|
+
!RELATION_LOAD_STRATEGIES.includes(options.relationLoadStrategy)) {
|
|
845
|
+
throw new errors_js_1.ValidationError(relationLoadStrategyMessage(options.relationLoadStrategy));
|
|
846
|
+
}
|
|
753
847
|
this.relationLoadStrategy = options?.relationLoadStrategy ?? 'auto';
|
|
754
848
|
this.stableRelationOrder = options?.stableRelationOrder === true;
|
|
755
849
|
this.implicitPkOrdering = options?.implicitPkOrdering === true;
|
|
@@ -816,6 +910,8 @@ class QueryInterface {
|
|
|
816
910
|
// per `build*` call, so it is exposed as a LIVE getter (not a copied value).
|
|
817
911
|
const self = this;
|
|
818
912
|
this.ctx = {
|
|
913
|
+
diagnostics: this.diagnostics,
|
|
914
|
+
logger: this.logger,
|
|
819
915
|
dialect: this.dialect,
|
|
820
916
|
table: this.table,
|
|
821
917
|
schema: this.schema,
|
|
@@ -882,7 +978,7 @@ class QueryInterface {
|
|
|
882
978
|
};
|
|
883
979
|
}
|
|
884
980
|
/**
|
|
885
|
-
*
|
|
981
|
+
* Advice (`diagnostics.advice`), once per table: the columns whose database type is absent from
|
|
886
982
|
* BOTH the column entry and the table-level type maps, the residual case
|
|
887
983
|
* after the two-source resolution above.
|
|
888
984
|
*
|
|
@@ -907,23 +1003,24 @@ class QueryInterface {
|
|
|
907
1003
|
* Postgres rejects with `22007 invalid input syntax for type time`.
|
|
908
1004
|
* - `timestamptz` and every non-temporal type: unaffected.
|
|
909
1005
|
*
|
|
910
|
-
* PostgreSQL only: the
|
|
911
|
-
* `
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
1006
|
+
* PostgreSQL only: the zone-less `timestamp` rewrite is Postgres-gated (see
|
|
1007
|
+
* `utcDateTimeRewrite` in where.ts), so on the other engines a missing type
|
|
1008
|
+
* costs only the `date` rewrite. Silent when `diagnostics.advice` is off
|
|
1009
|
+
* (the production default) like the other advice, and deduped through the
|
|
1010
|
+
* shared registry, so a hot table logs one line for the process.
|
|
915
1011
|
*
|
|
916
1012
|
* Cannot throw on odd metadata: it walks `tableMeta.columns`, the array the
|
|
917
1013
|
* constructor loop above has already iterated (and that client.ts validates
|
|
918
1014
|
* as an array), never `dateColumns`, which is a `Set` in every first-party
|
|
919
1015
|
* metadata path but arrives as a plain object from JSON-round-tripped
|
|
920
|
-
* metadata.
|
|
1016
|
+
* metadata. An advisory diagnostic that crashes a shape production would serve
|
|
921
1017
|
* is worse than the bug it reports.
|
|
922
1018
|
*/
|
|
923
1019
|
warnUntypedColumns() {
|
|
924
|
-
|
|
1020
|
+
const log = this.advice;
|
|
1021
|
+
if (!log)
|
|
925
1022
|
return;
|
|
926
|
-
if (this.dialect.
|
|
1023
|
+
if (!this.dialect.capabilities.utcTimestampRewrite)
|
|
927
1024
|
return;
|
|
928
1025
|
const untyped = [];
|
|
929
1026
|
for (const col of this.tableMeta.columns) {
|
|
@@ -939,14 +1036,14 @@ class QueryInterface {
|
|
|
939
1036
|
const MAX_NAMED = 12;
|
|
940
1037
|
const named = untyped.slice(0, MAX_NAMED).join(', ');
|
|
941
1038
|
const rest = untyped.length > MAX_NAMED ? ` (+${untyped.length - MAX_NAMED} more)` : '';
|
|
942
|
-
|
|
1039
|
+
(0, logger_js_1.logTo)(log, 'warn', `table "${this.table}": no database type in metadata for column(s) ${named}${rest} (neither the ` +
|
|
943
1040
|
"column entry's `dialectType`/`pgType` nor the table-level `dialectTypes`/`pgTypes` map). Turbine cannot " +
|
|
944
1041
|
'tell which of them are zone-less, so it skips the UTC bind rewrite and a `Date` goes to the driver ' +
|
|
945
1042
|
"as-is: right for `timestamptz`, but a zone-less `date`/`timestamp` then stores the PROCESS's local " +
|
|
946
1043
|
'calendar fields rather than UTC (silently, since the read path shifts back by the same offset), and a ' +
|
|
947
1044
|
'`time`/`timetz` column rejects the value outright (`22007 invalid input syntax for type time`). ' +
|
|
948
1045
|
'Columns whose type IS resolved, every `timestamptz` among them, are unaffected. Fix: regenerate with ' +
|
|
949
|
-
'`npx turbine generate`, or set the column types in `defineSchema`.');
|
|
1046
|
+
'`npx turbine generate`, or set the column types in `defineSchema`.', { code: 'untyped-columns', table: this.table, columns: untyped });
|
|
950
1047
|
}
|
|
951
1048
|
/** Quote an identifier through the active SQL dialect. */
|
|
952
1049
|
q(name) {
|
|
@@ -1113,24 +1210,63 @@ class QueryInterface {
|
|
|
1113
1210
|
* query/relation-names.ts.
|
|
1114
1211
|
*/
|
|
1115
1212
|
withDeclaredRelationNames(args) {
|
|
1116
|
-
|
|
1213
|
+
const withClause = args?.with;
|
|
1214
|
+
if (!withClause)
|
|
1215
|
+
return args;
|
|
1216
|
+
const normalized = (0, relation_names_js_1.normalizeWithClause)(this.schema, this.table, withClause);
|
|
1217
|
+
return normalized === withClause ? args : { ...args, with: normalized };
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* `args` with `take` / `skip` folded into `limit` / `offset` at every `with`
|
|
1221
|
+
* level (see `normalizeWithPagination` in query/relation-names.ts). Runs after
|
|
1222
|
+
* the relation-name pass, so it resolves the declared spelling.
|
|
1223
|
+
*/
|
|
1224
|
+
withRelationPagination(args) {
|
|
1225
|
+
const withClause = args?.with;
|
|
1226
|
+
if (!withClause)
|
|
1117
1227
|
return args;
|
|
1118
|
-
const normalized = (0, relation_names_js_1.
|
|
1119
|
-
return normalized ===
|
|
1228
|
+
const normalized = (0, relation_names_js_1.normalizeWithPagination)(this.schema, this.table, withClause);
|
|
1229
|
+
return normalized === withClause ? args : { ...args, with: normalized };
|
|
1120
1230
|
}
|
|
1121
1231
|
/**
|
|
1122
|
-
* Caller args in canonical form:
|
|
1123
|
-
* pagination aliases folded into
|
|
1232
|
+
* Caller args in canonical form: `undefined` pruned (query/normalize-args.ts),
|
|
1233
|
+
* declared relation spellings, and the Prisma pagination aliases folded into
|
|
1234
|
+
* `limit` / `offset`, with `take` / `skip` folded inside every `with` level too.
|
|
1124
1235
|
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1236
|
+
* Called at the seam of EVERY public operation and its `build*` twin, so the
|
|
1237
|
+
* guards, the fingerprint, the build and the collect all read one shape. Each
|
|
1238
|
+
* step returns its input by reference when there was nothing to change, so a
|
|
1239
|
+
* second call on already-normalized args is free and the common path
|
|
1240
|
+
* allocates nothing.
|
|
1241
|
+
*
|
|
1242
|
+
* `refuseEmptiedWhere` names the single-row read (`findFirst`,
|
|
1243
|
+
* `findFirstOrThrow`) that refuses a `where` whose every key was `undefined`.
|
|
1244
|
+
*/
|
|
1245
|
+
normalizeArgs(args, refuseEmptiedWhere) {
|
|
1246
|
+
const pruned = (0, normalize_args_js_1.pruneUndefinedArgs)(this.schema, this.table, args);
|
|
1247
|
+
if (pruned !== args) {
|
|
1248
|
+
if (refuseEmptiedWhere)
|
|
1249
|
+
(0, normalize_args_js_1.assertWhereNotEmptiedByPruning)(this.table, refuseEmptiedWhere, args, pruned);
|
|
1250
|
+
(0, normalize_args_js_1.assertCursorNotEmptied)(this.table, args, pruned);
|
|
1251
|
+
}
|
|
1252
|
+
if (pruned)
|
|
1253
|
+
this.assertQueryOptions(pruned);
|
|
1254
|
+
return (0, utils_js_1.normalizePagination)(this.withRelationPagination(this.withDeclaredRelationNames(pruned)));
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Validate the per-query options whose bad values used to be ignored or
|
|
1258
|
+
* misread: `timeout` (Node clamps a non-positive, non-finite or oversized
|
|
1259
|
+
* delay to 1 ms) and `relationLoadStrategy`.
|
|
1131
1260
|
*/
|
|
1132
|
-
|
|
1133
|
-
|
|
1261
|
+
assertQueryOptions(args) {
|
|
1262
|
+
const { timeout, relationLoadStrategy } = args;
|
|
1263
|
+
if (timeout !== undefined && !isValidTimeout(timeout)) {
|
|
1264
|
+
throw new errors_js_1.ValidationError(`\`timeout\` on ${this.table} must be a positive number of milliseconds up to ${MAX_TIMEOUT_MS}, or 0 for ` +
|
|
1265
|
+
`no timeout (received ${describeOptionValue(timeout)}). Leave it out for no timeout.`);
|
|
1266
|
+
}
|
|
1267
|
+
if (relationLoadStrategy !== undefined && !RELATION_LOAD_STRATEGIES.includes(relationLoadStrategy)) {
|
|
1268
|
+
throw new errors_js_1.ValidationError(relationLoadStrategyMessage(relationLoadStrategy));
|
|
1269
|
+
}
|
|
1134
1270
|
}
|
|
1135
1271
|
/**
|
|
1136
1272
|
* Refuse a `findUnique` whose `where` names no unique key. The rule and the
|
|
@@ -1140,6 +1276,30 @@ class QueryInterface {
|
|
|
1140
1276
|
assertFindUniqueKey(where) {
|
|
1141
1277
|
(0, compound_unique_js_1.assertWhereIdentifiesOneRow)(this.tableMeta, this.table, where);
|
|
1142
1278
|
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Refuse a locking read that is not inside a transaction, at the execute
|
|
1281
|
+
* entry of each find method and before any strategy runs. The `build*`
|
|
1282
|
+
* methods accept `lock` (the SQL is right wherever it runs); executing it
|
|
1283
|
+
* in autocommit releases the lock before the caller sees the rows.
|
|
1284
|
+
*
|
|
1285
|
+
* The value is validated first, so an engine with no row locks answers
|
|
1286
|
+
* E017 rather than asking for a transaction it would also refuse.
|
|
1287
|
+
* "Inside a transaction" is `_txScoped`: the accessors a TransactionClient
|
|
1288
|
+
* hands out, at any nesting depth.
|
|
1289
|
+
*/
|
|
1290
|
+
assertLockInTransaction(args, operation) {
|
|
1291
|
+
if (args?.lock === undefined)
|
|
1292
|
+
return;
|
|
1293
|
+
(0, row_lock_js_1.resolveRowLock)(args.lock, this.dialect, this.table);
|
|
1294
|
+
if (!this.txScoped)
|
|
1295
|
+
throw new errors_js_1.ValidationError((0, row_lock_js_1.lockOutsideTransactionMessage)(this.table, operation));
|
|
1296
|
+
}
|
|
1297
|
+
/** Refuse `lock` on a streaming read (a cursor is not a locking read). */
|
|
1298
|
+
assertNoStreamLock(args, operation) {
|
|
1299
|
+
if (args?.lock !== undefined) {
|
|
1300
|
+
throw new errors_js_1.ValidationError((0, row_lock_js_1.lockOnStreamMessage)(this.table, operation));
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1143
1303
|
/**
|
|
1144
1304
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
1145
1305
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
|
@@ -1172,146 +1332,325 @@ class QueryInterface {
|
|
|
1172
1332
|
const fields = pk.map((c) => this.tableMeta.reverseColumnMap[c] ?? c);
|
|
1173
1333
|
return fields.length === 1 ? { [fields[0]]: 'asc' } : fields.map((f) => ({ [f]: 'asc' }));
|
|
1174
1334
|
}
|
|
1335
|
+
/** Memo for {@link totalOrderKeys}; table metadata is immutable. */
|
|
1336
|
+
totalOrderKeysMemo;
|
|
1175
1337
|
/**
|
|
1176
|
-
* The
|
|
1177
|
-
*
|
|
1178
|
-
* ordering view; the seek itself orders the fields by their `orderBy`
|
|
1179
|
-
* precedence (see {@link cursorSeekEntries}). Empty for a missing cursor or
|
|
1180
|
-
* one whose every value is `undefined` (which emits no seek condition at
|
|
1181
|
-
* all, so it does not paginate).
|
|
1338
|
+
* The unique keys whose columns are all NOT NULL, primary key first: the
|
|
1339
|
+
* column sets that make an order total (no two rows can tie on them).
|
|
1182
1340
|
*/
|
|
1183
|
-
|
|
1184
|
-
if (
|
|
1341
|
+
totalOrderKeys() {
|
|
1342
|
+
if (this.totalOrderKeysMemo)
|
|
1343
|
+
return this.totalOrderKeysMemo;
|
|
1344
|
+
const meta = this.tableMeta;
|
|
1345
|
+
const nullable = (column) => !meta.primaryKey.includes(column) && meta.columns.find((c) => c.name === column)?.nullable === true;
|
|
1346
|
+
this.totalOrderKeysMemo = (0, compound_unique_js_1.uniqueKeyNames)(meta).filter((cols) => cols.every((c) => !nullable(c)));
|
|
1347
|
+
return this.totalOrderKeysMemo;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* THE tiebreaker rule, shared by cursor pages ({@link planCursor}) and the
|
|
1351
|
+
* pages before them ({@link completePageOrder}), so every page of one walk
|
|
1352
|
+
* is cut from the same total order: the columns to append to an order over
|
|
1353
|
+
* `ordered` so that no two rows tie. `[]` when the order already contains a
|
|
1354
|
+
* unique NOT NULL key; the first such key's missing columns otherwise;
|
|
1355
|
+
* `undefined` when the table has none.
|
|
1356
|
+
*/
|
|
1357
|
+
tiebreakColumns(ordered) {
|
|
1358
|
+
const keys = this.totalOrderKeys();
|
|
1359
|
+
if (keys.some((cols) => cols.every((c) => ordered.has(c))))
|
|
1185
1360
|
return [];
|
|
1186
|
-
return
|
|
1187
|
-
.filter(([, v]) => v !== undefined)
|
|
1188
|
-
.map(([k]) => k)
|
|
1189
|
-
.sort();
|
|
1361
|
+
return keys[0]?.filter((c) => !ordered.has(c));
|
|
1190
1362
|
}
|
|
1191
1363
|
/**
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
* order
|
|
1199
|
-
*
|
|
1200
|
-
* blindly on the primary key when the two differ.
|
|
1364
|
+
* A paginated `findMany` (`limit` / `offset`) whose `orderBy` can tie gets
|
|
1365
|
+
* the tiebreaker appended, in the last term's direction, so a page boundary
|
|
1366
|
+
* cannot fall between tied rows in a different order on each page (skipping
|
|
1367
|
+
* or repeating one), and the first page of a cursor walk uses the order its
|
|
1368
|
+
* cursor pages use. Returns `args` itself when the order is already total,
|
|
1369
|
+
* the query is not paginated, or it pages by `cursor` or `distinct`, which
|
|
1370
|
+
* order themselves. A `findFirst` with no `offset` is one row, not a page,
|
|
1371
|
+
* and is left alone.
|
|
1201
1372
|
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
1206
|
-
* object's key order is canonicalized away, so `(a asc, b asc)` would be a
|
|
1207
|
-
* guess at which key the caller meant to lead. The caller must state the
|
|
1208
|
-
* order, and the unordered-page warning says so.
|
|
1209
|
-
* - a field that does not resolve to a real column. Column validation belongs
|
|
1210
|
-
* to the normal build path, which raises a precise error; synthesizing an
|
|
1211
|
-
* ORDER BY on it here would only change which error the caller sees.
|
|
1373
|
+
* An order containing a vector distance gets no tiebreaker: a KNN index
|
|
1374
|
+
* serves only `ORDER BY <distance> LIMIT k`, a compound order turns that
|
|
1375
|
+
* into a scan and sort of the whole table, and two equal distances are
|
|
1376
|
+
* rare. A `cursor` still refuses such an order (see {@link planCursor}).
|
|
1212
1377
|
*/
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
return
|
|
1378
|
+
completePageOrder(args) {
|
|
1379
|
+
if (args.orderBy === undefined || (args.limit === undefined && args.offset === undefined))
|
|
1380
|
+
return args;
|
|
1381
|
+
if ((0, normalize_args_js_1.hasCursor)(args.cursor) || (args.distinct && args.distinct.length > 0))
|
|
1382
|
+
return args;
|
|
1383
|
+
if (args[FIRST_ROW_ONLY])
|
|
1384
|
+
return args;
|
|
1385
|
+
const entries = (0, filters_js_1.orderByEntries)(args.orderBy).filter(([, d]) => d !== undefined);
|
|
1386
|
+
if (entries.some(([, d]) => (0, filters_js_1.isVectorOrderBy)(d)))
|
|
1387
|
+
return args;
|
|
1388
|
+
const ordered = new Set();
|
|
1389
|
+
let lastDesc = false;
|
|
1390
|
+
for (const [key, dir] of entries) {
|
|
1391
|
+
lastDesc = false;
|
|
1392
|
+
if ((0, filters_js_1.isVectorOrderBy)(dir) || (0, filters_js_1.isJsonPathOrderBy)(dir) || this.isRelationOrderByValue(dir))
|
|
1393
|
+
continue;
|
|
1394
|
+
const column = (0, utils_js_1.resolveColumnName)(this.tableMeta, key);
|
|
1395
|
+
if (column === undefined)
|
|
1396
|
+
continue;
|
|
1397
|
+
ordered.add(column);
|
|
1398
|
+
lastDesc = (0, filters_js_1.normalizeOrderBy)(dir).dir === 'DESC';
|
|
1223
1399
|
}
|
|
1224
|
-
|
|
1400
|
+
const missing = this.tiebreakColumns(ordered);
|
|
1401
|
+
if (!missing || missing.length === 0)
|
|
1402
|
+
return args;
|
|
1403
|
+
const dir = lastDesc ? 'desc' : 'asc';
|
|
1404
|
+
const field = (column) => this.tableMeta.reverseColumnMap[column] ?? column;
|
|
1405
|
+
return {
|
|
1406
|
+
...args,
|
|
1407
|
+
orderBy: [...entries.map(([k, v]) => ({ [k]: v })), ...missing.map((c) => ({ [field(c)]: dir }))],
|
|
1408
|
+
};
|
|
1225
1409
|
}
|
|
1226
1410
|
/**
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
* cache-hit param collect all consume, so the three cannot disagree about
|
|
1230
|
-
* which value binds to which `$n`.
|
|
1411
|
+
* Plan a `cursor` page: THE single authority the cache key, the SQL build and
|
|
1412
|
+
* the cache-hit param collect all read, so the three cannot disagree.
|
|
1231
1413
|
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
* always had.
|
|
1414
|
+
* The page is ordered by the EFFECTIVE order: the caller's `orderBy` plus a
|
|
1415
|
+
* tiebreaker (the primary key, else a unique key on NOT NULL columns) when
|
|
1416
|
+
* the orderBy does not already end in one, in the last term's direction. A
|
|
1417
|
+
* cursor query is always ordered, because a seek over rows the engine may
|
|
1418
|
+
* return in any order skips and repeats rows. `args.orderBy` is rewritten to
|
|
1419
|
+
* that order (by reference when nothing changes).
|
|
1239
1420
|
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1421
|
+
* Two ways to place the page, picked from what the cursor carries:
|
|
1422
|
+
* - VALUES: the cursor has a value for every column of the effective order,
|
|
1423
|
+
* so the seek compares against them directly (`col > $1`, expanded to a
|
|
1424
|
+
* keyset across several columns). Extra cursor fields are ignored.
|
|
1425
|
+
* - ROW: otherwise the cursor must identify one row by a unique key (the
|
|
1426
|
+
* findUnique rule), and the seek reads that row's ordered values in
|
|
1427
|
+
* scalar subqueries, guarded by `EXISTS` so a deleted cursor row gives an
|
|
1428
|
+
* empty page rather than a wrong one.
|
|
1429
|
+
* Anything else is refused (E003) naming the missing fields.
|
|
1246
1430
|
*
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1431
|
+
* NULL sort keys are placed where the engine sorts them (PostgreSQL: last
|
|
1432
|
+
* ascending, first descending; SQLite / MySQL / SQL Server: NULL is smallest)
|
|
1433
|
+
* or where an explicit `nulls` puts them, and each row is visited exactly
|
|
1434
|
+
* once. On PostgreSQL a nullable ordered column has its placement written
|
|
1435
|
+
* out, so a Postgres-compatible engine with a different default reads the
|
|
1436
|
+
* same order the seek assumes.
|
|
1249
1437
|
*/
|
|
1250
|
-
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1438
|
+
planCursor(args) {
|
|
1439
|
+
const raw = args.cursor;
|
|
1440
|
+
if (!(0, normalize_args_js_1.hasCursor)(raw))
|
|
1441
|
+
return { args, plan: undefined };
|
|
1442
|
+
const meta = this.tableMeta;
|
|
1443
|
+
const field = (column) => meta.reverseColumnMap[column] ?? column;
|
|
1444
|
+
const nullableColumn = (column) => !meta.primaryKey.includes(column) && meta.columns.find((c) => c.name === column)?.nullable === true;
|
|
1445
|
+
// The cursor's own values, by resolved column (either spelling, or a
|
|
1446
|
+
// compound-unique selector, is accepted). Each is a row's value, checked
|
|
1447
|
+
// by the rule every engine shares (cursorFieldValue).
|
|
1448
|
+
const flat = (0, compound_unique_js_1.expandCompoundUniqueWhere)(meta, raw);
|
|
1449
|
+
const cursorValues = new Map();
|
|
1450
|
+
for (const [key, written] of (0, filters_js_1.sortedEntries)(flat)) {
|
|
1451
|
+
if (written === undefined)
|
|
1256
1452
|
continue;
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
.
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
return {
|
|
1268
|
-
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1271
|
-
|
|
1453
|
+
const column = this.toColumn(key);
|
|
1454
|
+
const value = (0, filters_js_1.cursorFieldValue)(key, written, {
|
|
1455
|
+
column,
|
|
1456
|
+
table: this.table,
|
|
1457
|
+
kind: (0, filters_js_1.operandColumnKind)(this.getColumnPgType(column)),
|
|
1458
|
+
nullable: nullableColumn(column),
|
|
1459
|
+
});
|
|
1460
|
+
cursorValues.set(column, value);
|
|
1461
|
+
}
|
|
1462
|
+
if (cursorValues.size === 0)
|
|
1463
|
+
return { args, plan: undefined };
|
|
1464
|
+
// The caller's order, plain columns only: a relation, JSON-path or vector
|
|
1465
|
+
// ordering has no value a seek can compare against.
|
|
1466
|
+
const terms = [];
|
|
1467
|
+
const orderEntries = (0, filters_js_1.orderByEntries)(args.orderBy).filter(([, d]) => d !== undefined);
|
|
1468
|
+
for (const [key, dir] of orderEntries) {
|
|
1469
|
+
if ((0, filters_js_1.isVectorOrderBy)(dir) || (0, filters_js_1.isJsonPathOrderBy)(dir) || this.isRelationOrderByValue(dir)) {
|
|
1470
|
+
throw new errors_js_1.ValidationError(`findMany on "${this.table}" refused: \`cursor\` cannot page past the orderBy entry "${key}", which ` +
|
|
1471
|
+
'orders by a relation, a JSON path or a vector distance rather than a column. Order the cursor ' +
|
|
1472
|
+
'query by columns only, or page it with `offset`.');
|
|
1473
|
+
}
|
|
1474
|
+
const column = this.toColumn(key);
|
|
1475
|
+
if (terms.some((t) => t.column === column))
|
|
1476
|
+
continue;
|
|
1477
|
+
const { dir: d, nulls } = (0, filters_js_1.normalizeOrderBy)(dir);
|
|
1478
|
+
terms.push({ column, key, desc: d === 'DESC', nulls, nullable: nullableColumn(column) });
|
|
1479
|
+
}
|
|
1480
|
+
// Tiebreaker, the same one a first page gets (see tiebreakColumns).
|
|
1481
|
+
const tiebreak = this.tiebreakColumns(new Set(terms.map((t) => t.column)));
|
|
1482
|
+
if (!tiebreak) {
|
|
1483
|
+
throw new errors_js_1.ValidationError(`findMany on "${this.table}" refused: a \`cursor\` page needs an order with no ties, and "${this.table}" ` +
|
|
1484
|
+
'has no primary key or unique key on NOT NULL columns to break them. Add a primary key, or page ' +
|
|
1485
|
+
'this table with `offset`.');
|
|
1486
|
+
}
|
|
1487
|
+
const desc = terms.length > 0 ? terms[terms.length - 1].desc : false;
|
|
1488
|
+
for (const column of tiebreak) {
|
|
1489
|
+
terms.push({ column, key: field(column), desc, nulls: undefined, nullable: false });
|
|
1490
|
+
}
|
|
1491
|
+
const appended = tiebreak.length > 0;
|
|
1492
|
+
// Where NULLs sort, per term. Where the engine sorts them high (PostgreSQL)
|
|
1493
|
+
// a nullable column's placement is written out, so a Postgres-compatible
|
|
1494
|
+
// engine with a different default reads the order the seek assumes.
|
|
1495
|
+
const { nullsSortHigh, nullsOrderingClause } = this.dialect.capabilities;
|
|
1496
|
+
const pinNulls = nullsSortHigh && nullsOrderingClause;
|
|
1497
|
+
let pinned = false;
|
|
1498
|
+
for (const t of terms) {
|
|
1499
|
+
if (t.nulls === undefined && t.nullable && pinNulls) {
|
|
1500
|
+
t.nulls = t.desc ? 'first' : 'last';
|
|
1501
|
+
t.pin = true;
|
|
1502
|
+
pinned = true;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
const nullsFirst = (t) => t.nulls !== undefined ? t.nulls === 'first' : nullsSortHigh ? t.desc : !t.desc;
|
|
1506
|
+
let nextArgs = args;
|
|
1507
|
+
if (appended || pinned) {
|
|
1508
|
+
const orderBy = terms.map((t) => {
|
|
1509
|
+
const dir = t.desc ? 'desc' : 'asc';
|
|
1510
|
+
const original = orderEntries.find(([k]) => k === t.key)?.[1];
|
|
1511
|
+
return { [t.key]: t.pin ? { sort: dir, nulls: t.nulls } : (original ?? dir) };
|
|
1512
|
+
});
|
|
1513
|
+
nextArgs = { ...args, orderBy };
|
|
1514
|
+
}
|
|
1515
|
+
const inclusive = (0, normalize_args_js_1.isCursorInclusive)(args);
|
|
1516
|
+
const coerce = (column, value) => whereMod.coerceWhereOperand(this.ctx, meta, column, value);
|
|
1517
|
+
const keysetTerms = terms.map((t) => ({
|
|
1518
|
+
column: t.column,
|
|
1519
|
+
desc: t.desc,
|
|
1520
|
+
nullsFirst: nullsFirst(t),
|
|
1521
|
+
nullable: t.nullable,
|
|
1522
|
+
}));
|
|
1523
|
+
// An inclusive cursor (prisma-compat) names a ROW, as Prisma's does, so it
|
|
1524
|
+
// always reads that row: a deleted cursor row gives an empty page, never a
|
|
1525
|
+
// page placed where the row used to be.
|
|
1526
|
+
const namesRow = inclusive && (0, compound_unique_js_1.whereIdentifiesOneRow)(meta, flat);
|
|
1527
|
+
if (!namesRow && terms.every((t) => cursorValues.has(t.column))) {
|
|
1528
|
+
const values = terms.map((t) => {
|
|
1529
|
+
const v = cursorValues.get(t.column);
|
|
1530
|
+
return v === null ? null : coerce(t.column, v);
|
|
1531
|
+
});
|
|
1532
|
+
const fp = `v:${keysetTerms.map((t, i) => `${t.column}:${t.desc ? 'd' : 'a'}${t.nullable ? (t.nullsFirst ? 'f' : 'l') : ''}${values[i] === null ? '0' : ''}`).join(',')}` +
|
|
1533
|
+
(inclusive ? ':i' : '');
|
|
1534
|
+
return {
|
|
1535
|
+
args: nextArgs,
|
|
1536
|
+
plan: { mode: 'values', terms: keysetTerms, values, lookup: [], inclusive, fingerprint: fp },
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
if (!(0, compound_unique_js_1.whereIdentifiesOneRow)(meta, flat)) {
|
|
1540
|
+
const missing = terms.filter((t) => !cursorValues.has(t.column)).map((t) => `"${field(t.column)}"`);
|
|
1541
|
+
const order = terms.map((t) => `${field(t.column)} ${t.desc ? 'desc' : 'asc'}`).join(', ');
|
|
1542
|
+
const keys = (0, compound_unique_js_1.uniqueKeyNames)(meta).map((cols) => cols.length === 1 ? `{ ${field(cols[0])} }` : `{ ${cols.map(field).join(', ')} }`);
|
|
1543
|
+
throw new errors_js_1.ValidationError(`findMany on "${this.table}" refused: the \`cursor\` has no value for ${missing.join(', ')}, which the ` +
|
|
1544
|
+
`page order (${order}) needs, and it does not name one row by a unique key. Give the cursor a value ` +
|
|
1545
|
+
`for every ordered field, or name the cursor row by ${keys.length > 0 ? keys.join(' or ') : 'a unique key'}.`);
|
|
1546
|
+
}
|
|
1547
|
+
const lookup = [...cursorValues].map(([column, v]) => ({ column, value: v === null ? null : coerce(column, v) }));
|
|
1548
|
+
const fp = `r:${keysetTerms.map((t) => `${t.column}:${t.desc ? 'd' : 'a'}${t.nullable ? (t.nullsFirst ? 'f' : 'l') : ''}`).join(',')}` +
|
|
1549
|
+
`|k=${lookup.map((l) => `${l.column}${l.value === null ? '0' : ''}`).join(',')}` +
|
|
1550
|
+
(inclusive ? ':i' : '');
|
|
1551
|
+
return {
|
|
1552
|
+
args: nextArgs,
|
|
1553
|
+
plan: { mode: 'row', terms: keysetTerms, values: [], lookup, inclusive, fingerprint: fp },
|
|
1554
|
+
};
|
|
1272
1555
|
}
|
|
1273
1556
|
/**
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
* cursor ever emitted). Two or more expand to
|
|
1278
|
-
* `(c1 > $1 OR (c1 = $1 AND c2 > $2) OR (c1 = $1 AND c2 = $2 AND c3 > $3))`,
|
|
1279
|
-
* with `<` for a `desc` field: the row-value form `(c1, c2) > ($1, $2)` is
|
|
1280
|
-
* shorter, but it cannot express a MIXED direction at all and SQL Server has
|
|
1281
|
-
* no row-value comparison, so the expanded form is the one path every engine
|
|
1282
|
-
* and every direction set share. Each value is bound ONCE and its `$n` is
|
|
1283
|
-
* referenced by every branch that needs it (legal on every supported
|
|
1284
|
-
* placeholder syntax), which is what keeps the param count equal to the field
|
|
1285
|
-
* count on both the build and the collect side.
|
|
1557
|
+
* Render a {@link CursorPlan}'s seek predicate, pushing its params. Run by
|
|
1558
|
+
* the build AND by the cache-hit collect (which discards the SQL), so the two
|
|
1559
|
+
* cannot bind different params.
|
|
1286
1560
|
*
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1289
|
-
*
|
|
1561
|
+
* Each value is bound once and referenced by every branch that needs it. One
|
|
1562
|
+
* non-null column is the plain `col > $1`; several expand to
|
|
1563
|
+
* `(c1 > $1 OR (c1 = $1 AND c2 > $2) OR ...)`, with `<` for a descending
|
|
1564
|
+
* column (a row-value comparison cannot mix directions, and SQL Server has
|
|
1565
|
+
* none). A nullable column gets NULL-aware equality and "after" terms.
|
|
1290
1566
|
*/
|
|
1291
|
-
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1567
|
+
renderCursorPredicate(plan, params) {
|
|
1568
|
+
const qt = this.q(this.table);
|
|
1569
|
+
const col = (t) => `${qt}.${this.q(t.column)}`;
|
|
1570
|
+
let ref;
|
|
1571
|
+
let guard = '';
|
|
1572
|
+
if (plan.mode === 'values') {
|
|
1573
|
+
const refs = plan.values.map((v) => {
|
|
1574
|
+
if (v === null)
|
|
1575
|
+
return null;
|
|
1576
|
+
params.push(v);
|
|
1577
|
+
return this.p(params.length);
|
|
1578
|
+
});
|
|
1579
|
+
ref = (i) => refs[i] ?? null;
|
|
1580
|
+
}
|
|
1581
|
+
else {
|
|
1582
|
+
const alias = 'cursor_row';
|
|
1583
|
+
const conds = plan.lookup.map((l) => {
|
|
1584
|
+
if (l.value === null)
|
|
1585
|
+
return `${alias}.${this.q(l.column)} IS NULL`;
|
|
1586
|
+
params.push(l.value);
|
|
1587
|
+
return `${alias}.${this.q(l.column)} = ${this.p(params.length)}`;
|
|
1588
|
+
});
|
|
1589
|
+
const gf = whereMod.targetGlobalFilterAlias(this.ctx, this.table, alias, params);
|
|
1590
|
+
if (gf)
|
|
1591
|
+
conds.push(gf);
|
|
1592
|
+
const from = `FROM ${qt} ${alias} WHERE ${conds.join(' AND ')}`;
|
|
1593
|
+
ref = (i) => `(SELECT ${alias}.${this.q(plan.terms[i].column)} ${from})`;
|
|
1594
|
+
guard = `EXISTS (SELECT 1 ${from})`;
|
|
1595
|
+
}
|
|
1596
|
+
// Value mode knows each value; row mode knows only whether the column can
|
|
1597
|
+
// hold NULL, so its comparisons stay NULL-aware for nullable columns.
|
|
1598
|
+
const rowMode = plan.mode === 'row';
|
|
1599
|
+
const eq = (i) => {
|
|
1600
|
+
const t = plan.terms[i];
|
|
1601
|
+
const r = ref(i);
|
|
1602
|
+
if (r === null)
|
|
1603
|
+
return `${col(t)} IS NULL`;
|
|
1604
|
+
if (rowMode && t.nullable)
|
|
1605
|
+
return `(${col(t)} = ${r} OR (${col(t)} IS NULL AND ${r} IS NULL))`;
|
|
1606
|
+
return `${col(t)} = ${r}`;
|
|
1607
|
+
};
|
|
1608
|
+
const after = (i, orEqual) => {
|
|
1609
|
+
const t = plan.terms[i];
|
|
1610
|
+
const r = ref(i);
|
|
1611
|
+
const op = `${t.desc ? '<' : '>'}${orEqual ? '=' : ''}`;
|
|
1612
|
+
if (r === null)
|
|
1613
|
+
return t.nullsFirst ? `${col(t)} IS NOT NULL` : null;
|
|
1614
|
+
if (!t.nullable)
|
|
1615
|
+
return `${col(t)} ${op} ${r}`;
|
|
1616
|
+
if (rowMode) {
|
|
1617
|
+
return t.nullsFirst
|
|
1618
|
+
? `((${r} IS NULL AND ${col(t)} IS NOT NULL) OR ${col(t)} ${op} ${r})`
|
|
1619
|
+
: `(${r} IS NOT NULL AND (${col(t)} ${op} ${r} OR ${col(t)} IS NULL))`;
|
|
1620
|
+
}
|
|
1621
|
+
return t.nullsFirst ? `${col(t)} ${op} ${r}` : `(${col(t)} ${op} ${r} OR ${col(t)} IS NULL)`;
|
|
1622
|
+
};
|
|
1623
|
+
const n = plan.terms.length;
|
|
1624
|
+
const last = n - 1;
|
|
1625
|
+
// An inclusive page folds "equal on every column" into the last branch when
|
|
1626
|
+
// that column is NOT NULL and its value known non-null (`>=`); otherwise it
|
|
1627
|
+
// is its own branch.
|
|
1628
|
+
const foldEqual = plan.inclusive && !plan.terms[last].nullable && ref(last) !== null;
|
|
1629
|
+
const branches = [];
|
|
1630
|
+
for (let i = 0; i < n; i++) {
|
|
1631
|
+
const a = after(i, foldEqual && i === last);
|
|
1632
|
+
if (a === null)
|
|
1633
|
+
continue;
|
|
1296
1634
|
if (i === 0)
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1635
|
+
branches.push(a);
|
|
1636
|
+
else
|
|
1637
|
+
branches.push(`(${[...plan.terms.slice(0, i).map((_, j) => eq(j)), a].join(' AND ')})`);
|
|
1638
|
+
}
|
|
1639
|
+
if (plan.inclusive && !foldEqual) {
|
|
1640
|
+
branches.push(n === 1 ? eq(0) : `(${plan.terms.map((_, j) => eq(j)).join(' AND ')})`);
|
|
1641
|
+
}
|
|
1642
|
+
const keyset = branches.length === 0 ? '1 = 0' : branches.length === 1 ? branches[0] : `(${branches.join(' OR ')})`;
|
|
1643
|
+
return guard ? `(${guard} AND ${keyset})` : keyset;
|
|
1302
1644
|
}
|
|
1303
1645
|
/**
|
|
1304
|
-
* Whether a findMany paginates (`limit` / `take` / `offset`
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
* none.
|
|
1646
|
+
* Whether a findMany paginates (`limit` / `take` / `offset`) but declares no
|
|
1647
|
+
* ordering, which makes the returned page NON-DETERMINISTIC: Postgres is
|
|
1648
|
+
* free to return different rows for the same unordered `LIMIT` once the heap
|
|
1649
|
+
* changes underneath it, so a row can appear on two pages or on none.
|
|
1309
1650
|
*
|
|
1310
|
-
* `cursor` counts
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
* (`[]`, or an object whose every value is `undefined`) counts as absent,
|
|
1314
|
-
* because it emits no ordering.
|
|
1651
|
+
* A `cursor` page never counts: it is always ordered by its effective order
|
|
1652
|
+
* (see {@link planCursor}). An empty orderBy (`[]`, or an object whose every
|
|
1653
|
+
* value is `undefined`) counts as absent, because it emits no ordering.
|
|
1315
1654
|
*
|
|
1316
1655
|
* `distinct` is still excluded: that path re-orders in an outer wrapper around
|
|
1317
1656
|
* a `DISTINCT ON` whose ordering picks the representative row, so an implicit
|
|
@@ -1322,11 +1661,11 @@ class QueryInterface {
|
|
|
1322
1661
|
return false;
|
|
1323
1662
|
if (args.distinct !== undefined)
|
|
1324
1663
|
return false;
|
|
1664
|
+
if ((0, normalize_args_js_1.hasCursor)(args.cursor))
|
|
1665
|
+
return false;
|
|
1325
1666
|
if (!(0, filters_js_1.isEmptyOrderBy)(args.orderBy))
|
|
1326
1667
|
return false;
|
|
1327
|
-
|
|
1328
|
-
return true;
|
|
1329
|
-
return this.cursorFields(args.cursor).length > 0;
|
|
1668
|
+
return args.limit !== undefined || args.offset !== undefined;
|
|
1330
1669
|
}
|
|
1331
1670
|
/**
|
|
1332
1671
|
* Opt-in (`implicitPkOrdering`) primary-key ascending ordering for a paginating
|
|
@@ -1334,47 +1673,42 @@ class QueryInterface {
|
|
|
1334
1673
|
*
|
|
1335
1674
|
* OFF by default in CORE, deliberately: turning it on would add an `ORDER BY`
|
|
1336
1675
|
* to SQL that existing applications already emit, changing both the rows a
|
|
1337
|
-
* given page returns and the plan the engine picks.
|
|
1338
|
-
* in everything but the type signature, so it waits for a major. The
|
|
1676
|
+
* given page returns and the plan the engine picks. The
|
|
1339
1677
|
* `turbine-orm/prisma-compat` layer defaults it ON instead, because reproducing
|
|
1340
1678
|
* Prisma's semantics is that layer's whole contract.
|
|
1341
1679
|
*
|
|
1342
1680
|
* An explicit `orderBy` always wins, a PK-less table is left alone (nothing
|
|
1343
1681
|
* stable to order by), and a composite PK orders on every column in
|
|
1344
|
-
* declaration order. A `cursor`
|
|
1345
|
-
* (see {@link cursorOrderBy}), and is left alone when that shape is ambiguous.
|
|
1682
|
+
* declaration order. A `cursor` page is ordered by its own plan, not here, and
|
|
1346
1683
|
* `distinct` shapes are skipped (see {@link isUnorderedPage}). With the flag
|
|
1347
|
-
* off this returns `undefined` before touching anything
|
|
1348
|
-
* byte-identical to before.
|
|
1684
|
+
* off this returns `undefined` before touching anything.
|
|
1349
1685
|
*/
|
|
1350
1686
|
implicitPkOrderBy(args) {
|
|
1351
1687
|
if (!this.implicitPkOrdering)
|
|
1352
1688
|
return undefined;
|
|
1353
1689
|
if (!this.isUnorderedPage(args))
|
|
1354
1690
|
return undefined;
|
|
1355
|
-
if (this.cursorFields(args?.cursor).length > 0)
|
|
1356
|
-
return this.cursorOrderBy(args?.cursor);
|
|
1357
1691
|
return this.pkOrderBy();
|
|
1358
1692
|
}
|
|
1359
1693
|
/**
|
|
1360
|
-
*
|
|
1694
|
+
* Advice (`diagnostics.advice`), once per query shape: an unordered paginating findMany returns a
|
|
1361
1695
|
* non-deterministic page (see {@link isUnorderedPage}), and on real data it is
|
|
1362
1696
|
* also usually the slower plan (an unordered `LIMIT` can discard tens of
|
|
1363
1697
|
* thousands of heap rows that an index scan on the key would have skipped).
|
|
1364
1698
|
*
|
|
1365
|
-
* Gated exactly like the other
|
|
1366
|
-
*
|
|
1699
|
+
* Gated exactly like the other advice (silent when `diagnostics.advice` is
|
|
1700
|
+
* off) and consistent with `warnOnUnlimited`: a per-call
|
|
1367
1701
|
* `warnOnUnlimited: false` silences it, `true` forces it past a config-level
|
|
1368
1702
|
* opt-out, and a config/per-table `warnOnUnlimited: false` silences it. Deduped
|
|
1369
1703
|
* process-wide through the shared warn registry, so it can never spam.
|
|
1370
1704
|
*
|
|
1371
1705
|
* Suppressed only when `implicitPkOrdering` will ACTUALLY order this query.
|
|
1372
|
-
* The flag being on is not enough: a PK-less table
|
|
1373
|
-
*
|
|
1374
|
-
* the shapes that still need saying out loud.
|
|
1706
|
+
* The flag being on is not enough: a PK-less table gets no injected
|
|
1707
|
+
* ordering, and that is precisely the shape that still needs saying out loud.
|
|
1375
1708
|
*/
|
|
1376
1709
|
maybeWarnUnorderedPage(args) {
|
|
1377
|
-
|
|
1710
|
+
const log = this.advice;
|
|
1711
|
+
if (!log)
|
|
1378
1712
|
return;
|
|
1379
1713
|
if (this.implicitPkOrdering && this.implicitPkOrderBy(args) !== undefined)
|
|
1380
1714
|
return;
|
|
@@ -1385,30 +1719,18 @@ class QueryInterface {
|
|
|
1385
1719
|
return;
|
|
1386
1720
|
if (!this.isUnorderedPage(args))
|
|
1387
1721
|
return;
|
|
1388
|
-
const
|
|
1389
|
-
const shape = [
|
|
1390
|
-
cursorFields.length > 0 ? 'cursor' : '',
|
|
1391
|
-
args?.limit !== undefined ? 'limit' : '',
|
|
1392
|
-
args?.offset !== undefined ? 'offset' : '',
|
|
1393
|
-
]
|
|
1722
|
+
const shape = [args?.limit !== undefined ? 'limit' : '', args?.offset !== undefined ? 'offset' : '']
|
|
1394
1723
|
.filter(Boolean)
|
|
1395
1724
|
.join('+');
|
|
1396
1725
|
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unorderedPage, `${this.table}|${shape}`))
|
|
1397
1726
|
return;
|
|
1398
|
-
const asOrderBy = (fields) => fields.length === 1 ? `{ ${fields[0]}: 'asc' }` : `[${fields.map((f) => `{ ${f}: 'asc' }`).join(', ')}]`;
|
|
1399
1727
|
const pk = (this.tableMeta.primaryKey ?? []).map((c) => this.tableMeta.reverseColumnMap[c] ?? c);
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
const suggestion = orderFields.length > 0 ? `Add \`orderBy: ${asOrderBy(orderFields)}\`` : 'Add an `orderBy` on a unique column';
|
|
1404
|
-
const seek = cursorFields.length > 0
|
|
1405
|
-
? 'a `cursor` seek with no orderBy compares against rows the engine is free to return in any order, so ' +
|
|
1406
|
-
'paging with it skips and repeats rows: '
|
|
1407
|
-
: '';
|
|
1408
|
-
console.warn(`[turbine] findMany on "${this.table}" paginates (${shape}) with no orderBy: ${seek}the page is NOT ` +
|
|
1728
|
+
const asOrderBy = pk.length === 1 ? `{ ${pk[0]}: 'asc' }` : `[${pk.map((f) => `{ ${f}: 'asc' }`).join(', ')}]`;
|
|
1729
|
+
const suggestion = pk.length > 0 ? `Add \`orderBy: ${asOrderBy}\`` : 'Add an `orderBy` on a unique column';
|
|
1730
|
+
(0, logger_js_1.logTo)(log, 'warn', `findMany on "${this.table}" paginates (${shape}) with no orderBy: the page is NOT ` +
|
|
1409
1731
|
'deterministic (the same query can return different rows as the table changes, so a row may appear ' +
|
|
1410
1732
|
`on two pages or on none). ${suggestion}, or set \`implicitPkOrdering: true\` in the client config ` +
|
|
1411
|
-
'to order paginated queries
|
|
1733
|
+
'to order paginated queries by the primary key automatically.', { code: 'unordered-page', table: this.table, operation: 'findMany', paging: shape });
|
|
1412
1734
|
}
|
|
1413
1735
|
// -------------------------------------------------------------------------
|
|
1414
1736
|
// relationLoadStrategy: 'auto', per-relation batched fallback when the
|
|
@@ -1635,7 +1957,7 @@ class QueryInterface {
|
|
|
1635
1957
|
return split;
|
|
1636
1958
|
}
|
|
1637
1959
|
/**
|
|
1638
|
-
*
|
|
1960
|
+
* Advice, once per relation: `'auto'` engaged the batched fallback.
|
|
1639
1961
|
*
|
|
1640
1962
|
* Both lines follow the same four parts: the CONDITION that tripped the rule,
|
|
1641
1963
|
* the MECHANISM (which plan shape was replaced by which, and what that costs),
|
|
@@ -1644,20 +1966,21 @@ class QueryInterface {
|
|
|
1644
1966
|
* as a bug, so the mechanism sentence is not optional.
|
|
1645
1967
|
*/
|
|
1646
1968
|
emitAutoNotes(engaged) {
|
|
1647
|
-
|
|
1969
|
+
const log = this.advice;
|
|
1970
|
+
if (!log)
|
|
1648
1971
|
return;
|
|
1649
1972
|
for (const e of engaged) {
|
|
1650
1973
|
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.autoStrategy, `${this.table}.${e.relation}`))
|
|
1651
1974
|
continue;
|
|
1652
1975
|
if (e.reason === 'to-one-cardinality') {
|
|
1653
|
-
|
|
1976
|
+
(0, logger_js_1.logTo)(log, 'warn', `auto strategy: to-one relation "${e.relation}" on "${this.table}" loads batched ` +
|
|
1654
1977
|
`(the query is unbounded or its limit exceeds ${this.autoToOneThreshold()} rows). On the join plan ` +
|
|
1655
1978
|
'a to-one relation is a correlated subquery the engine re-evaluates once per parent row, a per-row ' +
|
|
1656
1979
|
'cost that stands even on a unique index; batched replaces it with ONE follow-up statement ' +
|
|
1657
1980
|
'(`key = ANY(...)` for the whole page), so it trades that per-row cost for a single extra round ' +
|
|
1658
1981
|
'trip. Bound the query with a smaller `limit`, tune `autoToOneJoinMaxRows` (the break-even is ' +
|
|
1659
1982
|
"round-trip time / per-row cost), or set `relationLoadStrategy: 'join'` to force the " +
|
|
1660
|
-
'single-statement plan.');
|
|
1983
|
+
'single-statement plan.', { code: 'auto-strategy', table: this.table, relation: e.relation, reason: e.reason });
|
|
1661
1984
|
continue;
|
|
1662
1985
|
}
|
|
1663
1986
|
const probe = e.miss
|
|
@@ -1675,9 +1998,9 @@ class QueryInterface {
|
|
|
1675
1998
|
: ' On the join plan that relation is a correlated subquery re-evaluated once per parent row, so an ' +
|
|
1676
1999
|
`unindexed probe is one full scan of "${child}" per parent row; the batched follow-up scans it ` +
|
|
1677
2000
|
'once for the whole page.';
|
|
1678
|
-
|
|
2001
|
+
(0, logger_js_1.logTo)(log, 'warn', `auto strategy: relation "${e.relation}" on "${this.table}" loads batched (${probe}).${why} ` +
|
|
1679
2002
|
"Create the covering index (or set `relationLoadStrategy: 'join'` to force the single-statement " +
|
|
1680
|
-
'plan); run `npx turbine doctor` for the exact CREATE INDEX SQL.');
|
|
2003
|
+
'plan); run `npx turbine doctor` for the exact CREATE INDEX SQL.', { code: 'auto-strategy', table: this.table, relation: e.relation, reason: e.reason });
|
|
1681
2004
|
}
|
|
1682
2005
|
}
|
|
1683
2006
|
/**
|
|
@@ -1768,16 +2091,15 @@ class QueryInterface {
|
|
|
1768
2091
|
// a closure rather than passing `this.dialect` so the loader keeps
|
|
1769
2092
|
// needing nothing else from the dialect.
|
|
1770
2093
|
//
|
|
1771
|
-
// The
|
|
1772
|
-
//
|
|
1773
|
-
//
|
|
1774
|
-
//
|
|
1775
|
-
//
|
|
1776
|
-
//
|
|
1777
|
-
//
|
|
1778
|
-
//
|
|
1779
|
-
|
|
1780
|
-
buildPartitionLimit: this.dialect.name === 'postgresql' && this.dialect.buildPartitionLimit
|
|
2094
|
+
// The capability is NOT redundant with the hook's presence: every engine
|
|
2095
|
+
// dialect is built by SPREADING `postgresDialect`, so the hook is
|
|
2096
|
+
// INHERITED by sqlite / mysql / mssql, whose "absent" fallback would then
|
|
2097
|
+
// never be taken. The wrapper is not portable as written either (T-SQL
|
|
2098
|
+
// rejects an ORDER BY inside a derived table without TOP/OFFSET, and the
|
|
2099
|
+
// compiled child query carries one), so the pushdown stays with the
|
|
2100
|
+
// dialect that owns the implementation until another engine ships its own
|
|
2101
|
+
// and asserts it.
|
|
2102
|
+
buildPartitionLimit: this.dialect.capabilities.partitionLimitPushdown && this.dialect.buildPartitionLimit
|
|
1781
2103
|
? (input) => this.dialect.buildPartitionLimit(input)
|
|
1782
2104
|
: undefined,
|
|
1783
2105
|
skipGlobalFilters: skip,
|
|
@@ -1916,15 +2238,14 @@ class QueryInterface {
|
|
|
1916
2238
|
*
|
|
1917
2239
|
* When `sqlCache` is disabled, always calls `build()` without caching.
|
|
1918
2240
|
*
|
|
1919
|
-
* `build` receives a fresh `$N` param scratch array.
|
|
1920
|
-
*
|
|
1921
|
-
*
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
1924
|
-
* verify the collect path stayed in lockstep with the build path.
|
|
2241
|
+
* `build` receives a fresh `$N` param scratch array. The executed params
|
|
2242
|
+
* always come from each call site's dedicated collect path; on a miss the
|
|
2243
|
+
* scratch params are kept in {@link lastMissParams} so `crossCheckCache` can
|
|
2244
|
+
* compare the two. On a HIT, `build` is skipped here and re-run by
|
|
2245
|
+
* {@link crossCheckCache} only when `diagnostics.cacheCheck` asks for it.
|
|
1925
2246
|
*
|
|
1926
|
-
* Sets {@link lastCacheHit} so the caller's `crossCheckCache` knows
|
|
1927
|
-
*
|
|
2247
|
+
* Sets {@link lastCacheHit} so the caller's `crossCheckCache` knows which
|
|
2248
|
+
* comparison applies.
|
|
1928
2249
|
*/
|
|
1929
2250
|
acquireSql(cacheKey, build) {
|
|
1930
2251
|
if (!this.sqlCacheEnabled) {
|
|
@@ -1935,6 +2256,7 @@ class QueryInterface {
|
|
|
1935
2256
|
if (cached) {
|
|
1936
2257
|
this.cacheHits++;
|
|
1937
2258
|
this.lastCacheHit = true;
|
|
2259
|
+
this.lastMissParams = undefined;
|
|
1938
2260
|
return cached;
|
|
1939
2261
|
}
|
|
1940
2262
|
this.lastCacheHit = false;
|
|
@@ -1964,40 +2286,68 @@ class QueryInterface {
|
|
|
1964
2286
|
*/
|
|
1965
2287
|
buildCacheEntry(build) {
|
|
1966
2288
|
this.variableArityShape = false;
|
|
1967
|
-
|
|
2289
|
+
this.lastMissParams = undefined;
|
|
2290
|
+
const built = [];
|
|
2291
|
+
const sql = build(built);
|
|
1968
2292
|
const variableArity = this.variableArityShape;
|
|
1969
2293
|
this.variableArityShape = false;
|
|
1970
2294
|
this.cacheMisses++;
|
|
1971
|
-
|
|
2295
|
+
this.lastMissParams = built;
|
|
2296
|
+
// With `sqlComment` putting a comment on every statement, the template's own
|
|
2297
|
+
// name is never sent (the execute seam sends a `c_` name derived from it),
|
|
2298
|
+
// so it is not claimed from the budget: only names that reach the wire do.
|
|
2299
|
+
const name = variableArity
|
|
2300
|
+
? ''
|
|
2301
|
+
: this.sqlCommenter?.alwaysComments
|
|
2302
|
+
? (0, utils_js_1.sqlToPreparedName)(sql)
|
|
2303
|
+
: (0, utils_js_1.boundedPreparedName)(sql);
|
|
2304
|
+
return { sql, name, paramCount: built.length };
|
|
1972
2305
|
}
|
|
1973
2306
|
/**
|
|
1974
|
-
*
|
|
2307
|
+
* The SQL-cache lockstep checks (see the comment above `CACHE_MISMATCH_WARN_NS`).
|
|
1975
2308
|
*
|
|
1976
|
-
*
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
2309
|
+
* On a MISS: the collected params must equal the params the build pushed.
|
|
2310
|
+
* On a HIT: the collected param count must equal the entry's, always; then,
|
|
2311
|
+
* when `diagnostics.cacheCheck` selects this hit, the SQL and params are
|
|
2312
|
+
* rebuilt fresh through the same closure and compared:
|
|
1979
2313
|
* (a) the cached SQL string byte-for-byte against the fresh SQL, and
|
|
1980
|
-
* (b) the params the
|
|
1981
|
-
*
|
|
2314
|
+
* (b) the collected params against the fresh build-path params (length
|
|
2315
|
+
* and element-wise strict deep-equal).
|
|
1982
2316
|
*
|
|
1983
2317
|
* A mismatch means the fingerprint / build / collect paths have drifted out
|
|
1984
2318
|
* of lockstep (the exact class of bug that has silently corrupted results
|
|
1985
|
-
* before), so it throws
|
|
1986
|
-
* fingerprint, the operation, and both SQL strings (truncated)
|
|
1987
|
-
*
|
|
2319
|
+
* before), so it throws an {@link InvariantError} (E020) naming the
|
|
2320
|
+
* fingerprint, the operation, and both SQL strings (truncated), never a
|
|
2321
|
+
* parameter value.
|
|
1988
2322
|
*
|
|
1989
2323
|
* @param op human label of the calling build method (for the error message).
|
|
1990
|
-
* @param cacheKey the cache fingerprint
|
|
1991
|
-
* @param entry the
|
|
2324
|
+
* @param cacheKey the cache fingerprint.
|
|
2325
|
+
* @param entry the SQL entry that will be executed.
|
|
1992
2326
|
* @param build the same closure passed to `acquireSql`; re-run here to
|
|
1993
2327
|
* capture the fresh build-path SQL + params.
|
|
1994
2328
|
* @param collectedParams the params the caller's collect path produced.
|
|
1995
2329
|
*/
|
|
1996
2330
|
crossCheckCache(op, cacheKey, entry, build, collectedParams) {
|
|
1997
|
-
if (!this.lastCacheHit)
|
|
2331
|
+
if (!this.lastCacheHit) {
|
|
2332
|
+
const built = this.lastMissParams;
|
|
2333
|
+
this.lastMissParams = undefined;
|
|
2334
|
+
if (built === undefined || cacheParamsEqual(collectedParams, built))
|
|
2335
|
+
return;
|
|
2336
|
+
throw this.lockstepError(op, cacheKey, [
|
|
2337
|
+
`built params and collected params diverge on a cache miss (collected ${collectedParams.length}, built ${built.length})`,
|
|
2338
|
+
], false);
|
|
2339
|
+
}
|
|
2340
|
+
if (entry.paramCount !== undefined && collectedParams.length !== entry.paramCount) {
|
|
2341
|
+
throw new errors_js_1.InvariantError('sql-cache-param-count', `SQL cache param count mismatch on ${op} (fingerprint "${cacheKey}"): the cached statement has ` +
|
|
2342
|
+
`${entry.paramCount} placeholders but the cache hit collected ${collectedParams.length} params, so ` +
|
|
2343
|
+
'the statement was not sent.');
|
|
2344
|
+
}
|
|
2345
|
+
const check = this.diagnostics.cacheCheck;
|
|
2346
|
+
if (check === 'off')
|
|
1998
2347
|
return;
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2348
|
+
const sampled = typeof check === 'object';
|
|
2349
|
+
// The sampling decision, and only it, is per hit.
|
|
2350
|
+
if (sampled && check.sample < 1 && Math.random() >= check.sample)
|
|
2001
2351
|
return;
|
|
2002
2352
|
const freshParams = [];
|
|
2003
2353
|
const freshSql = build(freshParams);
|
|
@@ -2013,34 +2363,39 @@ class QueryInterface {
|
|
|
2013
2363
|
if (!paramsOk) {
|
|
2014
2364
|
details.push(`cache-hit params and freshly-built params diverge (collected ${collectedParams.length}, built ${freshParams.length})`);
|
|
2015
2365
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2366
|
+
throw this.lockstepError(op, cacheKey, details, sampled);
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* The `sql-cache-lockstep` {@link InvariantError}. When `log` is set (the
|
|
2370
|
+
* sampled hit path) the failure is also logged once per table and shape, so
|
|
2371
|
+
* it is durably recorded in the server log before the throw unwinds the
|
|
2372
|
+
* query; the other paths throw straight away.
|
|
2373
|
+
*/
|
|
2374
|
+
lockstepError(op, cacheKey, details, log) {
|
|
2375
|
+
const err = new errors_js_1.InvariantError('sql-cache-lockstep', `SQL cache lockstep violation on ${op} (fingerprint "${cacheKey}"). The fingerprint, SQL-build, and ` +
|
|
2376
|
+
`param-collect paths must enumerate where-clause keys identically. ${errors_js_1.INVARIANT_REPORT_HINT}\n` +
|
|
2377
|
+
details.join('\n'));
|
|
2378
|
+
if (log && (0, warn_registry_js_1.shouldWarnOnce)(CACHE_MISMATCH_WARN_NS, `${this.table}|${cacheKey}`)) {
|
|
2379
|
+
(0, logger_js_1.logTo)(this.logger, 'error', err.message, { code: 'cache-mismatch', table: this.table, operation: op });
|
|
2026
2380
|
}
|
|
2027
|
-
|
|
2381
|
+
return err;
|
|
2028
2382
|
}
|
|
2029
2383
|
/**
|
|
2030
|
-
* Reset the
|
|
2031
|
-
* Exposed for tests so a single test process can
|
|
2032
|
-
*
|
|
2384
|
+
* Reset the unlimited-query warning dedupe, which is process-wide (see
|
|
2385
|
+
* {@link maybeWarnUnlimited}). Exposed for tests so a single test process can
|
|
2386
|
+
* verify the warning fires once per table without bleeding state between
|
|
2387
|
+
* assertions.
|
|
2033
2388
|
*/
|
|
2034
2389
|
resetUnlimitedWarnings() {
|
|
2035
|
-
|
|
2390
|
+
(0, warn_registry_js_1.resetWarnOnce)(warn_registry_js_1.WARN_NS.unlimitedFindMany);
|
|
2036
2391
|
}
|
|
2037
|
-
emitQueryEvent(
|
|
2392
|
+
emitQueryEvent(out, params, duration, action, rows, error, retried) {
|
|
2038
2393
|
const onQuery = this.options?._onQuery;
|
|
2039
2394
|
if (!onQuery)
|
|
2040
2395
|
return;
|
|
2041
2396
|
try {
|
|
2042
2397
|
onQuery({
|
|
2043
|
-
sql,
|
|
2398
|
+
sql: out.sql,
|
|
2044
2399
|
params,
|
|
2045
2400
|
duration,
|
|
2046
2401
|
model: this.table,
|
|
@@ -2050,12 +2405,28 @@ class QueryInterface {
|
|
|
2050
2405
|
error,
|
|
2051
2406
|
strategy: this.currentStrategyTag,
|
|
2052
2407
|
...(retried ? { retried: true } : {}),
|
|
2408
|
+
...(out.comment === undefined ? {} : { comment: out.comment }),
|
|
2053
2409
|
});
|
|
2054
2410
|
}
|
|
2055
2411
|
catch {
|
|
2056
2412
|
// Listener errors must never crash a query
|
|
2057
2413
|
}
|
|
2058
2414
|
}
|
|
2415
|
+
/**
|
|
2416
|
+
* The statement `sql` as it goes out under `preparedName`: with the
|
|
2417
|
+
* `sqlComment` appended, and under the name that comment calls for (see
|
|
2418
|
+
* query/sql-comment.ts). This is the one place a comment is added, at the
|
|
2419
|
+
* execute seam and after the SQL-template cache, so the cache, its keys and
|
|
2420
|
+
* its lockstep checks only ever see commentless text. Without `sqlComment`
|
|
2421
|
+
* it is the statement unchanged.
|
|
2422
|
+
*/
|
|
2423
|
+
outgoing(sql, preparedName, action) {
|
|
2424
|
+
const commenter = this.sqlCommenter;
|
|
2425
|
+
const c = commenter?.commentFor(this.table, action);
|
|
2426
|
+
if (commenter === undefined || c === undefined)
|
|
2427
|
+
return { sql, text: sql, name: preparedName, comment: undefined };
|
|
2428
|
+
return { sql, text: `${sql} ${c.comment}`, name: commenter.preparedName(preparedName, c), comment: c.comment };
|
|
2429
|
+
}
|
|
2059
2430
|
/**
|
|
2060
2431
|
* Resolve the prepared-statement name a read should execute under, honouring
|
|
2061
2432
|
* the per-query {@link FindManyArgs.forceCustomPlan} opt-in.
|
|
@@ -2146,13 +2517,31 @@ class QueryInterface {
|
|
|
2146
2517
|
}
|
|
2147
2518
|
/**
|
|
2148
2519
|
* Execute a pool.query with an optional timeout.
|
|
2149
|
-
*
|
|
2520
|
+
*
|
|
2521
|
+
* On a node-postgres pool outside a transaction the timeout is enforced on
|
|
2522
|
+
* the SERVER ({@link queryCancellable}). Anywhere else (a transaction's
|
|
2523
|
+
* connection, an engine shim, an HTTP pool) it races the query against a
|
|
2524
|
+
* timer: the call rejects on time but the statement keeps running, so the
|
|
2525
|
+
* error's `outcome` is `'unknown'`.
|
|
2526
|
+
*
|
|
2150
2527
|
* pg driver errors are translated to typed Turbine errors via wrapPgError.
|
|
2151
2528
|
* `retryIfStale` is {@link readWithTimeout}'s; nothing else sets it.
|
|
2152
2529
|
*/
|
|
2153
|
-
async queryWithTimeout(sql, params,
|
|
2154
|
-
let start = performance.now();
|
|
2530
|
+
async queryWithTimeout(sql, params, requestedTimeout, preparedName, retryIfStale = false, pool = this.pool) {
|
|
2155
2531
|
const action = this.currentAction;
|
|
2532
|
+
const out = this.outgoing(sql, preparedName, action);
|
|
2533
|
+
// 0 is no timeout, as it is for `$transaction`'s option and the server's statement_timeout.
|
|
2534
|
+
const timeout = requestedTimeout === 0 ? undefined : requestedTimeout;
|
|
2535
|
+
if (timeout !== undefined && pool === this.pool && this.cancelsOnTimeout()) {
|
|
2536
|
+
return this.queryCancellable(out, params, timeout, retryIfStale, action);
|
|
2537
|
+
}
|
|
2538
|
+
if (timeout !== undefined && pool === this.pool && this.txScoped && this.dialect.capabilities.backendCancel) {
|
|
2539
|
+
const state = (0, transaction_state_js_1.transactionStateOf)(this.pool);
|
|
2540
|
+
if (state !== undefined && (0, utils_js_1.canCancelBackend)(state.client)) {
|
|
2541
|
+
return this.queryCancellableInTransaction(out, params, timeout, state, action);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
let start = performance.now();
|
|
2156
2545
|
// Build the query argument, use object form with `name` for prepared
|
|
2157
2546
|
// statements, or the plain (text, values) form otherwise.
|
|
2158
2547
|
//
|
|
@@ -2160,19 +2549,19 @@ class QueryInterface {
|
|
|
2160
2549
|
// pool shims only speak `(text, values)`), so it is reached through a
|
|
2161
2550
|
// cast. Guarded at runtime by `preparedStatementsEnabled`, which only the
|
|
2162
2551
|
// drivers that accept it turn on.
|
|
2163
|
-
const usePrepared =
|
|
2552
|
+
const usePrepared = out.name && this.preparedStatementsEnabled;
|
|
2164
2553
|
const send = () => usePrepared
|
|
2165
|
-
?
|
|
2166
|
-
name:
|
|
2167
|
-
text:
|
|
2554
|
+
? pool.query({
|
|
2555
|
+
name: out.name,
|
|
2556
|
+
text: out.text,
|
|
2168
2557
|
values: params,
|
|
2169
2558
|
})
|
|
2170
|
-
:
|
|
2559
|
+
: pool.query(out.text, params);
|
|
2171
2560
|
let exec = send();
|
|
2172
2561
|
// Set when the caller's timeout has already answered, so a first attempt
|
|
2173
2562
|
// that fails AFTER that does not send a retry nobody is waiting for.
|
|
2174
2563
|
let abandoned = false;
|
|
2175
|
-
if (retryIfStale && !this.txScoped) {
|
|
2564
|
+
if (retryIfStale && !this.txScoped && pool === this.pool) {
|
|
2176
2565
|
exec = exec.catch(async (err) => {
|
|
2177
2566
|
if (abandoned || !(0, errors_js_1.isStaleConnectionError)(err))
|
|
2178
2567
|
throw err;
|
|
@@ -2180,7 +2569,7 @@ class QueryInterface {
|
|
|
2180
2569
|
// so a listener counting errors sees it and one alerting on failed
|
|
2181
2570
|
// CALLS can skip it. The final event below times the retry alone.
|
|
2182
2571
|
const wrapped = (0, errors_js_1.wrapPgError)(err);
|
|
2183
|
-
this.emitQueryEvent(
|
|
2572
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined, true);
|
|
2184
2573
|
await (0, connection_guard_js_1.settleEventLoop)();
|
|
2185
2574
|
if (abandoned)
|
|
2186
2575
|
throw err;
|
|
@@ -2191,12 +2580,12 @@ class QueryInterface {
|
|
|
2191
2580
|
if (!timeout) {
|
|
2192
2581
|
try {
|
|
2193
2582
|
const result = await exec;
|
|
2194
|
-
this.emitQueryEvent(
|
|
2583
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, result.rowCount ?? 0);
|
|
2195
2584
|
return result;
|
|
2196
2585
|
}
|
|
2197
2586
|
catch (err) {
|
|
2198
2587
|
const wrapped = (0, errors_js_1.wrapPgError)(err);
|
|
2199
|
-
this.emitQueryEvent(
|
|
2588
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
|
|
2200
2589
|
throw wrapped;
|
|
2201
2590
|
}
|
|
2202
2591
|
}
|
|
@@ -2204,23 +2593,240 @@ class QueryInterface {
|
|
|
2204
2593
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2205
2594
|
timer = setTimeout(() => {
|
|
2206
2595
|
abandoned = true;
|
|
2207
|
-
reject(new errors_js_1.TimeoutError(timeout
|
|
2596
|
+
reject(new errors_js_1.TimeoutError(timeout, 'Query', {
|
|
2597
|
+
outcome: 'unknown',
|
|
2598
|
+
message: `Query timed out after ${timeout}ms. It could not be cancelled on the server and may still ` +
|
|
2599
|
+
'complete, so a write may have been applied: check before retrying it.',
|
|
2600
|
+
}));
|
|
2208
2601
|
}, timeout);
|
|
2209
2602
|
});
|
|
2210
2603
|
try {
|
|
2211
2604
|
const result = await Promise.race([exec, timeoutPromise]);
|
|
2212
|
-
this.emitQueryEvent(
|
|
2605
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, result.rowCount ?? 0);
|
|
2213
2606
|
return result;
|
|
2214
2607
|
}
|
|
2215
2608
|
catch (err) {
|
|
2216
2609
|
const wrapped = (0, errors_js_1.wrapPgError)(err);
|
|
2217
|
-
this.emitQueryEvent(
|
|
2610
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
|
|
2218
2611
|
throw wrapped;
|
|
2219
2612
|
}
|
|
2220
2613
|
finally {
|
|
2221
2614
|
clearTimeout(timer);
|
|
2222
2615
|
}
|
|
2223
2616
|
}
|
|
2617
|
+
/**
|
|
2618
|
+
* Whether a timed statement can be stopped on the server: an engine with a
|
|
2619
|
+
* protocol cancel (PostgreSQL) on a node-postgres pool, outside a transaction
|
|
2620
|
+
* (inside one, the connection belongs to the transaction and is not this
|
|
2621
|
+
* method's to cancel or destroy).
|
|
2622
|
+
*/
|
|
2623
|
+
cancelsOnTimeout() {
|
|
2624
|
+
return !this.txScoped && this.dialect.capabilities.backendCancel && (0, utils_js_1.isNodePostgresPool)(this.pool);
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* {@link queryWithTimeout} with a timeout the server enforces. The statement
|
|
2628
|
+
* runs on a connection checked out for it, so when the deadline passes the
|
|
2629
|
+
* backend running it is known and gets a protocol cancel request.
|
|
2630
|
+
*
|
|
2631
|
+
* The call then waits, at most {@link CANCEL_GRACE_MS}, for the statement to
|
|
2632
|
+
* answer, and the answer decides the result:
|
|
2633
|
+
* - cancelled (57014): TimeoutError, `outcome: 'not-executed'`;
|
|
2634
|
+
* - finished first: the result, returned late, because reporting a write
|
|
2635
|
+
* that committed as a timeout invites a retry that applies it twice;
|
|
2636
|
+
* - failed: that error;
|
|
2637
|
+
* - no answer: TimeoutError, `outcome: 'unknown'`, and the connection is
|
|
2638
|
+
* destroyed rather than handed to the next caller still busy.
|
|
2639
|
+
* A deadline that passes while still waiting for a connection sends nothing
|
|
2640
|
+
* (`'not-executed'`). Either way the connection goes back to the pool only
|
|
2641
|
+
* once its backend is idle, so timed-out statements cannot hold the pool.
|
|
2642
|
+
*/
|
|
2643
|
+
async queryCancellable(out, params, timeout, retryIfStale, action) {
|
|
2644
|
+
let start = performance.now();
|
|
2645
|
+
const config = out.name && this.preparedStatementsEnabled ? { name: out.name, text: out.text, values: params } : null;
|
|
2646
|
+
const report = (err, retried) => {
|
|
2647
|
+
const wrapped = err instanceof errors_js_1.TimeoutError ? err : (0, errors_js_1.wrapPgError)(err);
|
|
2648
|
+
const error = wrapped instanceof Error ? wrapped : undefined;
|
|
2649
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, error, retried);
|
|
2650
|
+
return wrapped;
|
|
2651
|
+
};
|
|
2652
|
+
let expired = false;
|
|
2653
|
+
let expire = () => { };
|
|
2654
|
+
const deadline = new Promise((resolve) => {
|
|
2655
|
+
expire = resolve;
|
|
2656
|
+
});
|
|
2657
|
+
const timer = setTimeout(() => {
|
|
2658
|
+
expired = true;
|
|
2659
|
+
expire(TIMED_OUT);
|
|
2660
|
+
}, timeout);
|
|
2661
|
+
const neverSent = () => new errors_js_1.TimeoutError(timeout, 'Query', {
|
|
2662
|
+
outcome: 'not-executed',
|
|
2663
|
+
message: `Query timed out after ${timeout}ms before it reached the server. It made no change.`,
|
|
2664
|
+
});
|
|
2665
|
+
try {
|
|
2666
|
+
for (let attempt = 0;; attempt++) {
|
|
2667
|
+
const checkingOut = (0, checkout_js_1.acquireConnection)(this.pool);
|
|
2668
|
+
let held;
|
|
2669
|
+
try {
|
|
2670
|
+
held = await Promise.race([checkingOut, deadline]);
|
|
2671
|
+
}
|
|
2672
|
+
catch (err) {
|
|
2673
|
+
throw report(err);
|
|
2674
|
+
}
|
|
2675
|
+
if (held === TIMED_OUT) {
|
|
2676
|
+
checkingOut.then((late) => late.checkout.release(), () => { });
|
|
2677
|
+
throw report(neverSent());
|
|
2678
|
+
}
|
|
2679
|
+
const { client, checkout } = held;
|
|
2680
|
+
const running = (config
|
|
2681
|
+
? client.query(config)
|
|
2682
|
+
: client.query(out.text, params)).then((result) => ({ result }), (error) => ({ error }));
|
|
2683
|
+
const first = await Promise.race([running, deadline]);
|
|
2684
|
+
const settled = first === TIMED_OUT ? await this.cancelTimedOut(held, running, timeout) : first;
|
|
2685
|
+
if ('result' in settled) {
|
|
2686
|
+
if (first !== TIMED_OUT)
|
|
2687
|
+
checkout.release();
|
|
2688
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, settled.result.rowCount ?? 0);
|
|
2689
|
+
return settled.result;
|
|
2690
|
+
}
|
|
2691
|
+
if ('timeout' in settled)
|
|
2692
|
+
throw report(settled.timeout);
|
|
2693
|
+
const err = settled.error;
|
|
2694
|
+
if (first === TIMED_OUT)
|
|
2695
|
+
throw report(err);
|
|
2696
|
+
checkout.release(err instanceof Error ? err : true);
|
|
2697
|
+
if (!(retryIfStale && attempt === 0 && (0, errors_js_1.isStaleConnectionError)(err)))
|
|
2698
|
+
throw report(err);
|
|
2699
|
+
report(err, true);
|
|
2700
|
+
await (0, connection_guard_js_1.settleEventLoop)();
|
|
2701
|
+
if (expired)
|
|
2702
|
+
throw report(neverSent());
|
|
2703
|
+
start = performance.now();
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
finally {
|
|
2707
|
+
clearTimeout(timer);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* A timed statement inside a transaction, cancelled on the server when the
|
|
2712
|
+
* deadline passes, on the transaction's own connection.
|
|
2713
|
+
*
|
|
2714
|
+
* A cancelled statement made no change, but PostgreSQL aborts the
|
|
2715
|
+
* transaction it ran in, so the timeout ends the transaction too: it is
|
|
2716
|
+
* marked to roll back ({@link TransactionState.abort}), which also covers a
|
|
2717
|
+
* cancel that arrived after the statement finished. The error is
|
|
2718
|
+
* `outcome: 'not-executed'` either way, because nothing it did can commit.
|
|
2719
|
+
* A statement that finishes before the deadline is returned as usual.
|
|
2720
|
+
*/
|
|
2721
|
+
async queryCancellableInTransaction(out, params, timeout, state, action) {
|
|
2722
|
+
const start = performance.now();
|
|
2723
|
+
const running = (out.name && this.preparedStatementsEnabled
|
|
2724
|
+
? this.pool.query({
|
|
2725
|
+
name: out.name,
|
|
2726
|
+
text: out.text,
|
|
2727
|
+
values: params,
|
|
2728
|
+
})
|
|
2729
|
+
: this.pool.query(out.text, params)).then((result) => ({ result }), (error) => ({ error }));
|
|
2730
|
+
let timer;
|
|
2731
|
+
const deadline = new Promise((resolve) => {
|
|
2732
|
+
timer = setTimeout(() => resolve(TIMED_OUT), timeout);
|
|
2733
|
+
});
|
|
2734
|
+
let settled;
|
|
2735
|
+
try {
|
|
2736
|
+
const first = await Promise.race([running, deadline]);
|
|
2737
|
+
settled = first === TIMED_OUT ? await this.cancelInTransaction(state, running, timeout) : first;
|
|
2738
|
+
}
|
|
2739
|
+
finally {
|
|
2740
|
+
clearTimeout(timer);
|
|
2741
|
+
}
|
|
2742
|
+
if ('result' in settled) {
|
|
2743
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, settled.result.rowCount ?? 0);
|
|
2744
|
+
return settled.result;
|
|
2745
|
+
}
|
|
2746
|
+
const err = 'timeout' in settled ? settled.timeout : (0, errors_js_1.wrapPgError)(settled.error);
|
|
2747
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, err instanceof Error ? err : undefined);
|
|
2748
|
+
throw err;
|
|
2749
|
+
}
|
|
2750
|
+
/** Cancel a timed-out statement in a transaction and end the transaction. See {@link queryCancellableInTransaction}. */
|
|
2751
|
+
async cancelInTransaction(state, running, timeout) {
|
|
2752
|
+
let graceTimer;
|
|
2753
|
+
const graceOver = new Promise((resolve) => {
|
|
2754
|
+
graceTimer = setTimeout(() => resolve(TIMED_OUT), CANCEL_GRACE_MS);
|
|
2755
|
+
});
|
|
2756
|
+
try {
|
|
2757
|
+
await Promise.race([(0, utils_js_1.sendCancelRequest)(state.client, CANCEL_GRACE_MS), graceOver]);
|
|
2758
|
+
const answer = await Promise.race([running, graceOver]);
|
|
2759
|
+
if (answer !== TIMED_OUT && 'result' in answer)
|
|
2760
|
+
return answer;
|
|
2761
|
+
const cancelled = answer !== TIMED_OUT && isQueryCanceled(answer.error);
|
|
2762
|
+
if (answer !== TIMED_OUT && !cancelled)
|
|
2763
|
+
return answer;
|
|
2764
|
+
const error = new errors_js_1.TimeoutError(timeout, 'Query', {
|
|
2765
|
+
outcome: 'not-executed',
|
|
2766
|
+
...(cancelled ? { cause: answer.error } : {}),
|
|
2767
|
+
message: (cancelled
|
|
2768
|
+
? `Query timed out after ${timeout}ms and was cancelled on the server, so it made no change. `
|
|
2769
|
+
: `Query timed out after ${timeout}ms, and the server did not confirm its cancellation within ` +
|
|
2770
|
+
`${CANCEL_GRACE_MS}ms. `) +
|
|
2771
|
+
'The transaction it ran in is over: it rolls back instead of committing, and no statement in it is ' +
|
|
2772
|
+
'kept. Let this error leave the transaction callback and retry the whole transaction, or run a ' +
|
|
2773
|
+
'statement that may time out in a nested `tx.$transaction(...)`, which rolls back only that block.',
|
|
2774
|
+
});
|
|
2775
|
+
state.abort(error);
|
|
2776
|
+
return { timeout: error };
|
|
2777
|
+
}
|
|
2778
|
+
finally {
|
|
2779
|
+
clearTimeout(graceTimer);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* Cancel a statement that outlived its timeout, wait for it to answer, and
|
|
2784
|
+
* release its connection accordingly. See {@link queryCancellable}.
|
|
2785
|
+
*/
|
|
2786
|
+
async cancelTimedOut(held, running, timeout) {
|
|
2787
|
+
let graceTimer;
|
|
2788
|
+
const graceOver = new Promise((resolve) => {
|
|
2789
|
+
graceTimer = setTimeout(() => resolve(TIMED_OUT), CANCEL_GRACE_MS);
|
|
2790
|
+
});
|
|
2791
|
+
try {
|
|
2792
|
+
const delivered = await Promise.race([(0, utils_js_1.sendCancelRequest)(held.client, CANCEL_GRACE_MS), graceOver]);
|
|
2793
|
+
const answer = await Promise.race([running, graceOver]);
|
|
2794
|
+
if (answer === TIMED_OUT) {
|
|
2795
|
+
held.checkout.release(new Error('the statement did not stop after a cancel request'));
|
|
2796
|
+
return {
|
|
2797
|
+
timeout: new errors_js_1.TimeoutError(timeout, 'Query', {
|
|
2798
|
+
outcome: 'unknown',
|
|
2799
|
+
message: `Query timed out after ${timeout}ms and the server did not confirm its cancellation within ` +
|
|
2800
|
+
`${CANCEL_GRACE_MS}ms, so it may still complete and a write may have been applied: check before ` +
|
|
2801
|
+
'retrying it. The connection was closed.',
|
|
2802
|
+
}),
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
// A cancel that was sent but not confirmed could still land on the NEXT
|
|
2806
|
+
// statement this connection runs, so such a connection is not reused.
|
|
2807
|
+
const reusable = delivered === true;
|
|
2808
|
+
if ('result' in answer) {
|
|
2809
|
+
held.checkout.release(reusable ? undefined : true);
|
|
2810
|
+
return answer;
|
|
2811
|
+
}
|
|
2812
|
+
const err = answer.error;
|
|
2813
|
+
if (reusable && err?.code === '57014') {
|
|
2814
|
+
held.checkout.release();
|
|
2815
|
+
return {
|
|
2816
|
+
timeout: new errors_js_1.TimeoutError(timeout, 'Query', {
|
|
2817
|
+
outcome: 'not-executed',
|
|
2818
|
+
cause: err,
|
|
2819
|
+
message: `Query timed out after ${timeout}ms and was cancelled on the server. It made no change.`,
|
|
2820
|
+
}),
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
held.checkout.release(err instanceof Error ? err : true);
|
|
2824
|
+
return answer;
|
|
2825
|
+
}
|
|
2826
|
+
finally {
|
|
2827
|
+
clearTimeout(graceTimer);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2224
2830
|
/**
|
|
2225
2831
|
* Execute a write `DeferredQuery` (create/update/delete/upsert) according to
|
|
2226
2832
|
* the active dialect's {@link Dialect.resultStrategy}, then apply its
|
|
@@ -2235,18 +2841,104 @@ class QueryInterface {
|
|
|
2235
2841
|
* write and a follow-up SELECT; the SELECT's rows feed the transform.
|
|
2236
2842
|
*/
|
|
2237
2843
|
async executeMutation(deferred, timeout) {
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2844
|
+
const reselect = deferred.reselect;
|
|
2845
|
+
if (this.dialect.resultStrategy === 'reselect' && reselect) {
|
|
2846
|
+
// One transaction, so the row returned is the row this call wrote, and
|
|
2847
|
+
// the transform runs before COMMIT so a refusal it raises writes nothing.
|
|
2848
|
+
return this.inOneTransaction(async (exec) => deferred.transform(await reselect(exec)), timeout, `A ${this.dialect.name} write`, 'Pass a pool that can check out a connection.');
|
|
2242
2849
|
}
|
|
2243
2850
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, timeout, deferred.preparedName);
|
|
2244
2851
|
return deferred.transform(result);
|
|
2245
2852
|
}
|
|
2853
|
+
/**
|
|
2854
|
+
* The executor for a write's statements, sharing one deadline: each one gets
|
|
2855
|
+
* what is left of `timeout`, so a chunked `createMany` or a lock, write and
|
|
2856
|
+
* re-read cannot run for N times the timeout. A statement due past the
|
|
2857
|
+
* deadline is not sent. `onClient` routes them to a held connection.
|
|
2858
|
+
*/
|
|
2859
|
+
deadlineExec(timeout, onClient) {
|
|
2860
|
+
const deadline = timeout ? performance.now() + timeout : undefined;
|
|
2861
|
+
return async (sql, params, preparedName) => {
|
|
2862
|
+
let left = timeout;
|
|
2863
|
+
if (deadline !== undefined) {
|
|
2864
|
+
left = Math.ceil(deadline - performance.now());
|
|
2865
|
+
if (left <= 0) {
|
|
2866
|
+
throw new errors_js_1.TimeoutError(timeout, 'Query', {
|
|
2867
|
+
outcome: 'not-executed',
|
|
2868
|
+
message: `Query timed out after ${timeout}ms: the write's statements share one deadline, and it passed ` +
|
|
2869
|
+
'before the next one was sent. Its earlier statements are rolled back, so it makes no change.',
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
return onClient
|
|
2874
|
+
? this.queryWithTimeout(sql, params, left, preparedName, false, onClient)
|
|
2875
|
+
: this.queryWithTimeout(sql, params, left, preparedName);
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Run a write that takes several statements as one transaction on one
|
|
2880
|
+
* connection: the caller's transaction when there is one, else one opened
|
|
2881
|
+
* here. A statement that times out is cancelled where the engine allows it,
|
|
2882
|
+
* and its connection is closed rather than rolled back (a ROLLBACK would wait
|
|
2883
|
+
* behind it); the transaction never commits, so the write made no change.
|
|
2884
|
+
* `timeout` is one deadline for every statement (see {@link deadlineExec}).
|
|
2885
|
+
*/
|
|
2886
|
+
async inOneTransaction(work, timeout, operation, alternative) {
|
|
2887
|
+
if (this.txScoped) {
|
|
2888
|
+
// In the caller's transaction, a savepoint keeps the write whole: a later
|
|
2889
|
+
// statement that fails and is caught cannot leave the earlier ones behind.
|
|
2890
|
+
return (0, transaction_state_js_1.withSavepoint)(this.pool, () => work(this.deadlineExec(timeout)));
|
|
2891
|
+
}
|
|
2892
|
+
if (typeof this.pool.connect !== 'function') {
|
|
2893
|
+
throw new errors_js_1.UnsupportedFeatureError(`${operation} on a pool without connect()`, this.dialect.name, `${operation} runs several statements in one transaction, which needs a connection of its own, and ` +
|
|
2894
|
+
`this pool cannot check one out (a stateless HTTP driver). ${alternative}`);
|
|
2895
|
+
}
|
|
2896
|
+
const { client, checkout } = await (0, checkout_js_1.openCheckout)(this.pool, (c) => c.query(this.dialect.beginStatement()));
|
|
2897
|
+
const onClient = {
|
|
2898
|
+
query: (text, values) => typeof text === 'string'
|
|
2899
|
+
? client.query(text, values)
|
|
2900
|
+
: client.query(text),
|
|
2901
|
+
};
|
|
2902
|
+
try {
|
|
2903
|
+
const value = await work(this.deadlineExec(timeout, onClient));
|
|
2904
|
+
await client.query(this.dialect.commitStatement());
|
|
2905
|
+
checkout.release();
|
|
2906
|
+
return value;
|
|
2907
|
+
}
|
|
2908
|
+
catch (err) {
|
|
2909
|
+
if (err instanceof errors_js_1.TimeoutError) {
|
|
2910
|
+
if (await (0, utils_js_1.sendCancelRequest)(client, CANCEL_GRACE_MS)) {
|
|
2911
|
+
checkout.release(err);
|
|
2912
|
+
}
|
|
2913
|
+
else {
|
|
2914
|
+
// No cancel reached the server (or the engine has none), so the
|
|
2915
|
+
// statement may still be running. The ROLLBACK queues behind it on
|
|
2916
|
+
// this connection, and the connection goes back to the pool only
|
|
2917
|
+
// after it: never with the transaction still open.
|
|
2918
|
+
client.query(this.dialect.rollbackStatement()).then(() => checkout.release(), (e) => checkout.release(e instanceof Error ? e : true));
|
|
2919
|
+
}
|
|
2920
|
+
throw new errors_js_1.TimeoutError(err.timeoutMs, 'Query', {
|
|
2921
|
+
outcome: 'not-executed',
|
|
2922
|
+
cause: err,
|
|
2923
|
+
message: `Query timed out after ${err.timeoutMs}ms. Its transaction is rolled back rather than committed, ` +
|
|
2924
|
+
'so it makes no change.',
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
try {
|
|
2928
|
+
await client.query(this.dialect.rollbackStatement());
|
|
2929
|
+
}
|
|
2930
|
+
catch {
|
|
2931
|
+
// Best effort: the connection may be gone.
|
|
2932
|
+
}
|
|
2933
|
+
checkout.release();
|
|
2934
|
+
throw (0, errors_js_1.explainConnectionLoss)((0, errors_js_1.wrapPgError)(err), checkout.lostWith);
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2246
2937
|
// ---------------------------------------------------------------------------
|
|
2247
2938
|
// Write compilation (extracted to writes.ts).
|
|
2248
2939
|
// ---------------------------------------------------------------------------
|
|
2249
2940
|
buildCreate(args) {
|
|
2941
|
+
args = this.normalizeArgs(args);
|
|
2250
2942
|
return writesMod.buildCreate(this.ctx, args, this.resolveWriteProjection(args));
|
|
2251
2943
|
}
|
|
2252
2944
|
/**
|
|
@@ -2268,22 +2960,26 @@ class QueryInterface {
|
|
|
2268
2960
|
return { columns };
|
|
2269
2961
|
}
|
|
2270
2962
|
buildCreateMany(args) {
|
|
2271
|
-
|
|
2963
|
+
args = this.normalizeArgs(args);
|
|
2964
|
+
return writesMod.buildCreateMany(this.ctx, args, 'refuse', this.resolveWriteProjection(args));
|
|
2272
2965
|
}
|
|
2273
2966
|
buildUpdate(args) {
|
|
2967
|
+
args = this.normalizeArgs(args);
|
|
2274
2968
|
return writesMod.buildUpdate(this.ctx, args, this.resolveWriteProjection(args));
|
|
2275
2969
|
}
|
|
2276
2970
|
buildDelete(args) {
|
|
2971
|
+
args = this.normalizeArgs(args);
|
|
2277
2972
|
return writesMod.buildDelete(this.ctx, args, this.resolveWriteProjection(args));
|
|
2278
2973
|
}
|
|
2279
2974
|
buildUpsert(args) {
|
|
2975
|
+
args = this.normalizeArgs(args);
|
|
2280
2976
|
return writesMod.buildUpsert(this.ctx, args, this.resolveWriteProjection(args));
|
|
2281
2977
|
}
|
|
2282
2978
|
buildUpdateMany(args) {
|
|
2283
|
-
return writesMod.buildUpdateMany(this.ctx, args);
|
|
2979
|
+
return writesMod.buildUpdateMany(this.ctx, this.normalizeArgs(args));
|
|
2284
2980
|
}
|
|
2285
2981
|
buildDeleteMany(args) {
|
|
2286
|
-
return writesMod.buildDeleteMany(this.ctx, args);
|
|
2982
|
+
return writesMod.buildDeleteMany(this.ctx, this.normalizeArgs(args));
|
|
2287
2983
|
}
|
|
2288
2984
|
/**
|
|
2289
2985
|
* Best-effort extraction of an auto-generated primary key from a write
|
|
@@ -2299,11 +2995,10 @@ class QueryInterface {
|
|
|
2299
2995
|
* If no middlewares are registered, executes directly.
|
|
2300
2996
|
*
|
|
2301
2997
|
* Middleware can inspect and log query parameters, measure timing, and
|
|
2302
|
-
* transform the result returned by `next()`.
|
|
2303
|
-
*
|
|
2304
|
-
*
|
|
2305
|
-
* (e.g. soft deletes) belong in the query itself
|
|
2306
|
-
* `where: { deletedAt: null }` or wrap the table accessor in a small helper.
|
|
2998
|
+
* transform the result returned by `next()`. `params.args` is a plain copy
|
|
2999
|
+
* (see `readOnlyMiddlewareParams`): the query is compiled from the caller's
|
|
3000
|
+
* args, so `next()` throws a TypeError when the copy was changed. Cross-cutting
|
|
3001
|
+
* filters (e.g. soft deletes) belong in the query itself, or in `globalFilters`.
|
|
2307
3002
|
*/
|
|
2308
3003
|
async executeWithMiddleware(action, args, executor) {
|
|
2309
3004
|
this.currentAction = action;
|
|
@@ -2312,14 +3007,17 @@ class QueryInterface {
|
|
|
2312
3007
|
// can be complete. Putting it in each method instead would mean fifteen
|
|
2313
3008
|
// sites and a new one every time an operation is added, which is precisely
|
|
2314
3009
|
// how the surface it checks came to need checking.
|
|
2315
|
-
(0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args);
|
|
3010
|
+
(0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args, this.advice ?? null);
|
|
2316
3011
|
if (this.middlewares.length === 0) {
|
|
2317
3012
|
return executor();
|
|
2318
3013
|
}
|
|
2319
|
-
|
|
3014
|
+
// A copy: the query is compiled from `args`, not from what middleware
|
|
3015
|
+
// sees, so `next()` throws on a changed copy rather than doing nothing.
|
|
3016
|
+
const params = (0, utils_js_1.readOnlyMiddlewareParams)(this.table, action, args);
|
|
2320
3017
|
// Build middleware chain
|
|
2321
3018
|
let index = 0;
|
|
2322
3019
|
const next = async (p) => {
|
|
3020
|
+
(0, utils_js_1.assertMiddlewareArgsKept)(params, p);
|
|
2323
3021
|
if (index < this.middlewares.length) {
|
|
2324
3022
|
const mw = this.middlewares[index++];
|
|
2325
3023
|
return mw(p, next);
|
|
@@ -2342,6 +3040,7 @@ class QueryInterface {
|
|
|
2342
3040
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2343
3041
|
// depending on every consumer to re-establish it.
|
|
2344
3042
|
args = this.normalizeArgs(args);
|
|
3043
|
+
this.assertLockInTransaction(args, 'findUnique');
|
|
2345
3044
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
2346
3045
|
if (args.with) {
|
|
2347
3046
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -2394,6 +3093,9 @@ class QueryInterface {
|
|
|
2394
3093
|
return entity;
|
|
2395
3094
|
}
|
|
2396
3095
|
buildFindUnique(args) {
|
|
3096
|
+
// Canonical args FIRST: the empty-where guard and the unique-key rule below
|
|
3097
|
+
// must read the pruned `where`, never one still carrying `undefined` keys.
|
|
3098
|
+
args = this.normalizeArgs(args);
|
|
2397
3099
|
this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
2398
3100
|
// Pinned before the cache key, which carries it (see buildFindMany).
|
|
2399
3101
|
const jsonEncoding = this.resolveJsonEncoding(args.jsonEncoding);
|
|
@@ -2415,9 +3117,10 @@ class QueryInterface {
|
|
|
2415
3117
|
// deliberately so: a tenant filter is not a unique selector, and letting it
|
|
2416
3118
|
// satisfy this would still hand back an arbitrary row from inside the tenant.
|
|
2417
3119
|
//
|
|
2418
|
-
// `findFirst`
|
|
2419
|
-
//
|
|
2420
|
-
//
|
|
3120
|
+
// `findFirst` takes no such guard: "the first row matching an optional
|
|
3121
|
+
// filter" is its contract, and `where: {}` or no `where` asks for any row.
|
|
3122
|
+
// It refuses only a `where` that was emptied by `undefined` values (see
|
|
3123
|
+
// normalizeArgs), which never meant "any row".
|
|
2421
3124
|
if (whereMod.userPredicateIsEmpty(this.ctx, (args.where ?? {}))) {
|
|
2422
3125
|
throw new errors_js_1.ValidationError(`findUnique on "${this.table}" refused: the \`where\` clause has no predicate, ` +
|
|
2423
3126
|
'so this would return an arbitrary row rather than a specific one. ' +
|
|
@@ -2437,9 +3140,9 @@ class QueryInterface {
|
|
|
2437
3140
|
// a global filter is not an identity, and letting one satisfy this would
|
|
2438
3141
|
// hand back an arbitrary row from inside the tenant.
|
|
2439
3142
|
this.assertFindUniqueKey(args.where);
|
|
2440
|
-
//
|
|
2441
|
-
|
|
2442
|
-
|
|
3143
|
+
// The locking clause, read by the cache key and all three SQL paths below.
|
|
3144
|
+
const rowLock = (0, row_lock_js_1.resolveRowLock)(args.lock, this.dialect, this.table);
|
|
3145
|
+
const lockSql = rowLock?.sql ?? '';
|
|
2443
3146
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2444
3147
|
// relations before fingerprinting (see buildFindMany).
|
|
2445
3148
|
if (args.with && this.resolveStableOrder(args.stableRelationOrder)) {
|
|
@@ -2469,7 +3172,9 @@ class QueryInterface {
|
|
|
2469
3172
|
// `|je=`: same rule and same hazard as in buildFindMany. findUnique is never
|
|
2470
3173
|
// flatten-planned, so this is the only plan-shape segment it needs.
|
|
2471
3174
|
const encodingFp = `|je=${jsonEncoding === 'positional' ? 'p' : 'o'}`;
|
|
2472
|
-
|
|
3175
|
+
// Empty without a lock, so every unlocked key is unchanged.
|
|
3176
|
+
const lockFp = rowLock ? `|lk=${rowLock.fp}` : '';
|
|
3177
|
+
const ck = `fu:${whereFingerprint}|c=${colKey}|w=${withFp}|pii=${includePii ? 1 : 0}${encodingFp}${lockFp}${this.globalFilterCacheSegment()}`;
|
|
2473
3178
|
const params = [];
|
|
2474
3179
|
// Check if all where values are simple (plain equality, no operators/null/OR).
|
|
2475
3180
|
// Keys are sorted to match fingerprintWhere, insertion order here would let
|
|
@@ -2477,14 +3182,19 @@ class QueryInterface {
|
|
|
2477
3182
|
const whereKeys = Object.keys(whereObj)
|
|
2478
3183
|
.filter((k) => whereObj[k] !== undefined)
|
|
2479
3184
|
.sort();
|
|
3185
|
+
// Only a scalar the general path would accept as it is (a primitive, Date
|
|
3186
|
+
// or Buffer) on a non-JSON column takes the simple path, so no operand
|
|
3187
|
+
// reaches SQL without the general path's validation. Resolved, not looked
|
|
3188
|
+
// up: a relation filter spelled the snake_case way must not read as a
|
|
3189
|
+
// plain equality.
|
|
2480
3190
|
const isSimpleWhere = !whereObj.OR &&
|
|
2481
3191
|
!whereObj.AND &&
|
|
2482
3192
|
!whereObj.NOT &&
|
|
2483
3193
|
whereKeys.every((k) => {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
return
|
|
3194
|
+
if (!isFastPathScalar(whereObj[k]) || (0, utils_js_1.resolveRelationDef)(this.tableMeta.relations, k))
|
|
3195
|
+
return false;
|
|
3196
|
+
const column = (0, utils_js_1.resolveColumnName)(this.tableMeta, k);
|
|
3197
|
+
return column !== undefined && !this.isJsonColumnType(this.getColumnPgType(column));
|
|
2488
3198
|
});
|
|
2489
3199
|
// Simple path: plain equality, no operators/null/OR.
|
|
2490
3200
|
//
|
|
@@ -2505,7 +3215,7 @@ class QueryInterface {
|
|
|
2505
3215
|
});
|
|
2506
3216
|
const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
2507
3217
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
2508
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
3218
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}${lockSql}`;
|
|
2509
3219
|
};
|
|
2510
3220
|
const entry = this.acquireSql(ck, buildSql);
|
|
2511
3221
|
// Collect params (same order and same coercion as build)
|
|
@@ -2522,6 +3232,7 @@ class QueryInterface {
|
|
|
2522
3232
|
},
|
|
2523
3233
|
tag: `${this.table}.findUnique`,
|
|
2524
3234
|
preparedName: entry.name,
|
|
3235
|
+
...rowLockMarker(rowLock),
|
|
2525
3236
|
};
|
|
2526
3237
|
}
|
|
2527
3238
|
// General path (with operators, null, OR, with clause)
|
|
@@ -2531,7 +3242,7 @@ class QueryInterface {
|
|
|
2531
3242
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
2532
3243
|
const qt = this.q(this.table);
|
|
2533
3244
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
2534
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
3245
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}${lockSql}`;
|
|
2535
3246
|
};
|
|
2536
3247
|
const entry = this.acquireSql(ck, buildSql);
|
|
2537
3248
|
// Collect params
|
|
@@ -2546,6 +3257,7 @@ class QueryInterface {
|
|
|
2546
3257
|
},
|
|
2547
3258
|
tag: `${this.table}.findUnique`,
|
|
2548
3259
|
preparedName: entry.name,
|
|
3260
|
+
...rowLockMarker(rowLock),
|
|
2549
3261
|
};
|
|
2550
3262
|
}
|
|
2551
3263
|
// Nested queries with `with` clause.
|
|
@@ -2557,7 +3269,7 @@ class QueryInterface {
|
|
|
2557
3269
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
2558
3270
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
2559
3271
|
const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList, undefined, undefined, includePii);
|
|
2560
|
-
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}`;
|
|
3272
|
+
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}${lockSql}`;
|
|
2561
3273
|
};
|
|
2562
3274
|
const entry = this.acquireSql(ck, buildSql);
|
|
2563
3275
|
// Collect params in exact build order: where first, then with-clause relations
|
|
@@ -2574,6 +3286,7 @@ class QueryInterface {
|
|
|
2574
3286
|
},
|
|
2575
3287
|
tag: `${this.table}.findUnique`,
|
|
2576
3288
|
preparedName: entry.name,
|
|
3289
|
+
...rowLockMarker(rowLock),
|
|
2577
3290
|
};
|
|
2578
3291
|
}
|
|
2579
3292
|
// -------------------------------------------------------------------------
|
|
@@ -2589,19 +3302,20 @@ class QueryInterface {
|
|
|
2589
3302
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2590
3303
|
// depending on every consumer to re-establish it.
|
|
2591
3304
|
args = this.normalizeArgs(args);
|
|
3305
|
+
this.assertLockInTransaction(args, 'findMany');
|
|
2592
3306
|
this.maybeWarnUnlimited(args);
|
|
2593
3307
|
this.maybeWarnUnorderedPage(args);
|
|
2594
|
-
//
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
3308
|
+
// Advice: warn on deeply nested with clauses.
|
|
3309
|
+
const advice = this.advice;
|
|
3310
|
+
if (advice && args?.with) {
|
|
3311
|
+
const depth = this.measureWithDepth(args.with);
|
|
3312
|
+
if (depth > 5 && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.deepWith, this.table)) {
|
|
3313
|
+
(0, logger_js_1.logTo)(advice, 'warn', `Deep with clause (depth ${depth}) on "${this.tableMeta.name}": every level is a ` +
|
|
3314
|
+
'correlated subquery the engine re-evaluates once per row of the level above, so the work ' +
|
|
3315
|
+
'multiplies down the tree and the whole subtree is built as JSON inside each parent row. Split ' +
|
|
3316
|
+
"into separate queries, or use `relationLoadStrategy: 'batched'` (one flat statement per " +
|
|
3317
|
+
'relation, no per-parent re-evaluation). Advice: silent when `diagnostics.advice` is off, as it ' +
|
|
3318
|
+
'is by default in production.', { code: 'deep-with', table: this.table, operation: 'findMany', depth });
|
|
2605
3319
|
}
|
|
2606
3320
|
}
|
|
2607
3321
|
return this.executeWithMiddleware('findMany', (args ?? {}), async () => {
|
|
@@ -2676,11 +3390,13 @@ class QueryInterface {
|
|
|
2676
3390
|
}
|
|
2677
3391
|
}
|
|
2678
3392
|
/**
|
|
2679
|
-
* Emit a one-time
|
|
3393
|
+
* Emit a one-time warning when {@link findMany} is called without an
|
|
2680
3394
|
* explicit `limit`/`take` and `warnOnUnlimited` has not been disabled.
|
|
2681
3395
|
*
|
|
2682
|
-
*
|
|
2683
|
-
* loop calling `db.users.findMany()` thousands of times
|
|
3396
|
+
* Said once per table per process (`WARN_NS.unlimitedFindMany`), so a busy
|
|
3397
|
+
* loop calling `db.users.findMany()` thousands of times logs once, and so
|
|
3398
|
+
* does the same call inside every `$transaction`, which builds its own
|
|
3399
|
+
* query interfaces.
|
|
2684
3400
|
* Suppressed when `defaultLimit` is configured (the caller has already
|
|
2685
3401
|
* opted in to a bounded query) and when the user passed an explicit
|
|
2686
3402
|
* `limit`, `take`, or `cursor`. A per-call `warnOnUnlimited` overrides the
|
|
@@ -2696,19 +3412,18 @@ class QueryInterface {
|
|
|
2696
3412
|
return;
|
|
2697
3413
|
if (this.defaultLimit !== undefined)
|
|
2698
3414
|
return;
|
|
2699
|
-
const hasExplicitLimit = args?.limit !== undefined || args?.cursor
|
|
3415
|
+
const hasExplicitLimit = args?.limit !== undefined || (0, normalize_args_js_1.hasCursor)(args?.cursor);
|
|
2700
3416
|
if (hasExplicitLimit)
|
|
2701
3417
|
return;
|
|
2702
3418
|
if (this.whereMatchesAtMostOneRow(args?.where))
|
|
2703
3419
|
return;
|
|
2704
|
-
if (
|
|
3420
|
+
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unlimitedFindMany, this.table))
|
|
2705
3421
|
return;
|
|
2706
|
-
|
|
2707
|
-
console.warn(`[turbine] warning: findMany on "${this.table}" has no limit: the statement is emitted with no row limit, ` +
|
|
3422
|
+
(0, logger_js_1.logTo)(this.logger, 'warn', `warning: findMany on "${this.table}" has no limit: the statement is emitted with no row limit, ` +
|
|
2708
3423
|
'so the engine returns every matching row and the driver materializes all of them as objects before ' +
|
|
2709
3424
|
'this call resolves (the cost grows with the table, not with the rows you use). Pass `limit`/`take`, ' +
|
|
2710
3425
|
'or set `defaultLimit` in the client config; silence with `warnOnUnlimited: false` (per call, per ' +
|
|
2711
|
-
'table, or in config).');
|
|
3426
|
+
'table, or in config).', { code: 'unlimited-find-many', table: this.table, operation: 'findMany' });
|
|
2712
3427
|
}
|
|
2713
3428
|
/**
|
|
2714
3429
|
* Whether `where` can match at most one row, because it pins every column of
|
|
@@ -2755,7 +3470,7 @@ class QueryInterface {
|
|
|
2755
3470
|
}
|
|
2756
3471
|
/**
|
|
2757
3472
|
* Recursively measure the maximum depth of a `with` clause tree.
|
|
2758
|
-
* Used by the
|
|
3473
|
+
* Used by the deep-with advice.
|
|
2759
3474
|
*/
|
|
2760
3475
|
measureWithDepth(withClause) {
|
|
2761
3476
|
let maxDepth = 1;
|
|
@@ -2806,7 +3521,7 @@ class QueryInterface {
|
|
|
2806
3521
|
if (args?.orderBy !== undefined) {
|
|
2807
3522
|
const deduped = (0, filters_js_1.dedupeOrderEntries)(this.tableMeta, (0, filters_js_1.orderByEntries)(args.orderBy), this.table);
|
|
2808
3523
|
if (deduped) {
|
|
2809
|
-
(0, utils_js_1.warnRedundantSortTerm)(this.table, 'orderBy', deduped.dropped);
|
|
3524
|
+
(0, utils_js_1.warnRedundantSortTerm)(this.advice, this.table, 'orderBy', deduped.dropped);
|
|
2810
3525
|
args = {
|
|
2811
3526
|
...args,
|
|
2812
3527
|
orderBy: deduped.entries.map(([k, v]) => ({ [k]: v })),
|
|
@@ -2816,7 +3531,7 @@ class QueryInterface {
|
|
|
2816
3531
|
if (args?.distinct && args.distinct.length > 0) {
|
|
2817
3532
|
const deduped = (0, filters_js_1.dedupeColumnList)(this.tableMeta, args.distinct);
|
|
2818
3533
|
if (deduped) {
|
|
2819
|
-
(0, utils_js_1.warnRedundantSortTerm)(this.table, 'distinct', deduped.dropped);
|
|
3534
|
+
(0, utils_js_1.warnRedundantSortTerm)(this.advice, this.table, 'distinct', deduped.dropped);
|
|
2820
3535
|
args = { ...args, distinct: deduped.columns };
|
|
2821
3536
|
}
|
|
2822
3537
|
}
|
|
@@ -2827,29 +3542,40 @@ class QueryInterface {
|
|
|
2827
3542
|
const implicitOrder = this.implicitPkOrderBy(args);
|
|
2828
3543
|
if (implicitOrder)
|
|
2829
3544
|
args = { ...args, orderBy: implicitOrder };
|
|
3545
|
+
// A `cursor` that names no row (`null`, `{}`) is refused before either
|
|
3546
|
+
// reader below asks whether there is one.
|
|
3547
|
+
(0, normalize_args_js_1.assertCursorWritten)(this.table, 'findMany', args?.cursor);
|
|
3548
|
+
// A page whose order can tie gets the cursor tiebreaker (completePageOrder),
|
|
3549
|
+
// before fingerprinting, so the cache key carries it.
|
|
3550
|
+
if (args)
|
|
3551
|
+
args = this.completePageOrder(args);
|
|
3552
|
+
// A cursor page is planned (and its orderBy completed to the effective
|
|
3553
|
+
// order) BEFORE fingerprinting, so the key, the SQL and the param collect
|
|
3554
|
+
// all read one plan. See planCursor.
|
|
3555
|
+
let cursorPlan;
|
|
3556
|
+
if ((0, normalize_args_js_1.hasCursor)(args?.cursor))
|
|
3557
|
+
({ args, plan: cursorPlan } = this.planCursor(args));
|
|
2830
3558
|
// `distinct` compiles to `SELECT DISTINCT ON (...)`, which is PostgreSQL
|
|
2831
3559
|
// syntax and nothing else's. Every other engine parsed it as far as the
|
|
2832
3560
|
// word `ON` and answered with a RAW driver error carrying no TURBINE_ code
|
|
2833
3561
|
// (SQLite: `near "ON": syntax error`; MySQL: ER_PARSE_ERROR), i.e. the one
|
|
2834
3562
|
// shape the typed-error surface exists to prevent. Refused with the same
|
|
2835
|
-
// E017 the OTHER spelling of this
|
|
2836
|
-
// distinctOn })` in aggregates.ts
|
|
2837
|
-
//
|
|
2838
|
-
// `dialect.name` rather than a new capability flag, deliberately, and this
|
|
2839
|
-
// is the exception to the "capability flags, not engine names" rule the
|
|
2840
|
-
// rest of the dialect follows: every engine dialect is built by SPREADING
|
|
2841
|
-
// `postgresDialect`, so a flag defaulting to `true` would be inherited by
|
|
2842
|
-
// all of them (which is the bug) and one defaulting to `false` would have
|
|
2843
|
-
// to be re-declared on Postgres-compatible dialects that already work.
|
|
2844
|
-
// The established precedent for exactly this feature is the `dialect.name`
|
|
2845
|
-
// test in `buildDistinctOnSource`.
|
|
3563
|
+
// E017, on the same `distinctOn` capability, as the OTHER spelling of this
|
|
3564
|
+
// feature, `groupBy({ distinctOn })` in aggregates.ts.
|
|
2846
3565
|
//
|
|
2847
3566
|
// Before the cache, like the two guards below, so a warmed template cannot
|
|
2848
3567
|
// serve the query the cold path refuses.
|
|
2849
|
-
if (args?.distinct && args.distinct.length > 0 && this.dialect.
|
|
3568
|
+
if (args?.distinct && args.distinct.length > 0 && !this.dialect.capabilities.distinctOn) {
|
|
2850
3569
|
throw new errors_js_1.UnsupportedFeatureError('DISTINCT ON (findMany distinct)', this.dialect.name, 'findMany({ distinct }) requires PostgreSQL: SELECT DISTINCT ON is not portable. Group in ' +
|
|
2851
3570
|
'application code, or use groupBy({ by }) for one row per combination.');
|
|
2852
3571
|
}
|
|
3572
|
+
// The locking clause, validated and rendered once: the cache key and the
|
|
3573
|
+
// SQL both read this value. PostgreSQL refuses a lock on a DISTINCT query
|
|
3574
|
+
// (0A000), so that pair is refused here, before the cache.
|
|
3575
|
+
const rowLock = (0, row_lock_js_1.resolveRowLock)(args?.lock, this.dialect, this.table);
|
|
3576
|
+
if (rowLock && args?.distinct && args.distinct.length > 0) {
|
|
3577
|
+
throw new errors_js_1.ValidationError((0, row_lock_js_1.lockWithDistinctMessage)(this.table));
|
|
3578
|
+
}
|
|
2853
3579
|
// `distinct` + relation orderBy is refused up front (E003): the distinct
|
|
2854
3580
|
// path re-orders in an outer wrapper (`... AS "<table>_distinct" ORDER BY
|
|
2855
3581
|
// <userOrder>`) where a correlated relation subquery (pick-row, `_count`,
|
|
@@ -2896,14 +3622,9 @@ class QueryInterface {
|
|
|
2896
3622
|
})
|
|
2897
3623
|
.join(',')
|
|
2898
3624
|
: '';
|
|
2899
|
-
//
|
|
2900
|
-
//
|
|
2901
|
-
|
|
2902
|
-
const cursorFp = args?.cursor
|
|
2903
|
-
? this.cursorSeekEntries(args.cursor, args.orderBy)
|
|
2904
|
-
.map((e) => `${e.column}:${e.desc ? 'd' : 'a'}`)
|
|
2905
|
-
.join(',')
|
|
2906
|
-
: '';
|
|
3625
|
+
// Mode, keyset order, directions, NULL placement and which values are
|
|
3626
|
+
// NULL all change the predicate's SQL text (see planCursor).
|
|
3627
|
+
const cursorFp = cursorPlan?.fingerprint ?? '';
|
|
2907
3628
|
// distinct must fingerprint in USER order: the SQL emits `DISTINCT ON` in
|
|
2908
3629
|
// the caller's column order, so a permuted array rebuilds different SQL and
|
|
2909
3630
|
// must not collapse onto the same cache entry (would trip the cross-check).
|
|
@@ -2944,7 +3665,9 @@ class QueryInterface {
|
|
|
2944
3665
|
// non-default one, so the key never depends on what the client default
|
|
2945
3666
|
// happens to be.
|
|
2946
3667
|
const encodingFp = `|je=${jsonEncoding === 'positional' ? 'p' : 'o'}`;
|
|
2947
|
-
|
|
3668
|
+
// Empty without a lock, so every unlocked key is unchanged.
|
|
3669
|
+
const lockFp = rowLock ? `|lk=${rowLock.fp}` : '';
|
|
3670
|
+
const ck = `fm:${whereFp}|c=${colKey}|o=${orderFp}|l=${limitFp}|off=${offsetFp}|cur=${cursorFp}|d=${distinctFp}|w=${withFp}|pii=${includePii ? 1 : 0}${flattenFp}${encodingFp}${lockFp}${this.globalFilterCacheSegment()}`;
|
|
2948
3671
|
const params = [];
|
|
2949
3672
|
const buildSql = (freshParams) => {
|
|
2950
3673
|
// Fresh build: generates SQL and populates freshParams
|
|
@@ -3010,18 +3733,9 @@ class QueryInterface {
|
|
|
3010
3733
|
// WHERE + cursor conditions accumulate into `tail`, pushing params in
|
|
3011
3734
|
// where → cursor order (the collect path mirrors this exactly).
|
|
3012
3735
|
let tail = freshWhereSql;
|
|
3013
|
-
if (
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
const seek = this.cursorSeekEntries(args.cursor, args.orderBy);
|
|
3017
|
-
if (seek.length > 0) {
|
|
3018
|
-
const terms = seek.map((e) => {
|
|
3019
|
-
freshParams.push(e.value);
|
|
3020
|
-
return { column: `${qt}.${this.q(e.column)}`, placeholder: this.p(freshParams.length), desc: e.desc };
|
|
3021
|
-
});
|
|
3022
|
-
const predicate = this.keysetPredicate(terms);
|
|
3023
|
-
tail += freshWhereSql ? ` AND ${predicate}` : ` WHERE ${predicate}`;
|
|
3024
|
-
}
|
|
3736
|
+
if (cursorPlan) {
|
|
3737
|
+
const predicate = this.renderCursorPredicate(cursorPlan, freshParams);
|
|
3738
|
+
tail += freshWhereSql ? ` AND ${predicate}` : ` WHERE ${predicate}`;
|
|
3025
3739
|
}
|
|
3026
3740
|
// ORDER BY is built AFTER the cursor pushes (param order
|
|
3027
3741
|
// where → with → cursor → orderBy → limit → offset) and BEFORE final
|
|
@@ -3095,6 +3809,9 @@ class QueryInterface {
|
|
|
3095
3809
|
offsetPh = this.paginationRef(args.offset, freshParams, 'skip/offset');
|
|
3096
3810
|
}
|
|
3097
3811
|
sql += this.buildPagination(limitPh, offsetPh, !!args?.orderBy);
|
|
3812
|
+
// The locking clause follows pagination and binds nothing.
|
|
3813
|
+
if (rowLock)
|
|
3814
|
+
sql += rowLock.sql;
|
|
3098
3815
|
return sql;
|
|
3099
3816
|
};
|
|
3100
3817
|
const entry = this.acquireSql(ck, buildSql);
|
|
@@ -3107,13 +3824,9 @@ class QueryInterface {
|
|
|
3107
3824
|
if (args?.with) {
|
|
3108
3825
|
this.collectWithParams(args.with, params, undefined, flattenPlan);
|
|
3109
3826
|
}
|
|
3110
|
-
// 3. Cursor params
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
for (const e of this.cursorSeekEntries(args.cursor, args.orderBy)) {
|
|
3114
|
-
params.push(e.value);
|
|
3115
|
-
}
|
|
3116
|
-
}
|
|
3827
|
+
// 3. Cursor params: the build's own renderer, SQL discarded.
|
|
3828
|
+
if (cursorPlan)
|
|
3829
|
+
this.renderCursorPredicate(cursorPlan, params);
|
|
3117
3830
|
// 4. ORDER BY params (vector KNN ordering binds a `$n::vector` query vector).
|
|
3118
3831
|
// Mirrors buildOrderBy's push order, between cursor and LIMIT.
|
|
3119
3832
|
if (args?.orderBy) {
|
|
@@ -3140,6 +3853,7 @@ class QueryInterface {
|
|
|
3140
3853
|
transform: (result) => result.rows.map((row) => (parseWith ? parseWith(row) : this.parseRow(row, this.table))),
|
|
3141
3854
|
tag: `${this.table}.findMany`,
|
|
3142
3855
|
preparedName: entry.name,
|
|
3856
|
+
...rowLockMarker(rowLock),
|
|
3143
3857
|
};
|
|
3144
3858
|
}
|
|
3145
3859
|
// -------------------------------------------------------------------------
|
|
@@ -3200,20 +3914,21 @@ class QueryInterface {
|
|
|
3200
3914
|
* the result set is empty. Callers may therefore treat a yielded batch as
|
|
3201
3915
|
* non-empty.
|
|
3202
3916
|
*/
|
|
3203
|
-
async *streamRaw(args, action) {
|
|
3204
|
-
//
|
|
3205
|
-
//
|
|
3206
|
-
//
|
|
3207
|
-
|
|
3208
|
-
// through.
|
|
3209
|
-
args = (0, utils_js_1.normalizePagination)(args);
|
|
3917
|
+
async *streamRaw(args, action, idle, unit) {
|
|
3918
|
+
// Canonical args BEFORE the speculative build below, which spreads `args`
|
|
3919
|
+
// and sets its own `limit`: a surviving `take` would reach that spread
|
|
3920
|
+
// beside it and be read as a second value for one bound.
|
|
3921
|
+
args = this.normalizeArgs(args);
|
|
3210
3922
|
const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
|
|
3923
|
+
// Validated on every path, so a bad value is refused whether or not this
|
|
3924
|
+
// stream turns out to need a cursor.
|
|
3925
|
+
const idleTimeoutMs = (0, stream_idle_js_1.resolveStreamIdleTimeout)(args?.idleTimeoutMs, 'idleTimeoutMs') ?? this.streamIdleTimeoutMs;
|
|
3211
3926
|
this.currentAction = action;
|
|
3212
3927
|
// The two streaming methods do NOT go through `executeWithMiddleware`, so
|
|
3213
3928
|
// the unknown-key diagnostic has to be hung here as well or a stream would
|
|
3214
3929
|
// be the one read that silently drops an `include`. Both public methods
|
|
3215
3930
|
// reach this, and each passes its own name.
|
|
3216
|
-
(0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args);
|
|
3931
|
+
(0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args, this.advice ?? null);
|
|
3217
3932
|
// Streaming is ALREADY immune to the generic-plan cliff: the speculative
|
|
3218
3933
|
// fetch has never passed a prepared name, and the cursor path runs through
|
|
3219
3934
|
// DECLARE, so neither statement enters the plan cache. `preparedNameFor` is
|
|
@@ -3250,9 +3965,18 @@ class QueryInterface {
|
|
|
3250
3965
|
// four round trips on a drain that would have fit, never any transferred
|
|
3251
3966
|
// rows.
|
|
3252
3967
|
if (batchSize <= STREAM_SPECULATION_MAX_BATCH) {
|
|
3968
|
+
// The caller's bound (or the client's `defaultLimit`, exactly as findMany
|
|
3969
|
+
// applies it) caps the speculative fetch. When the bound fits in a batch
|
|
3970
|
+
// the speculative fetch IS the whole answer; otherwise `rows <= batchSize`
|
|
3971
|
+
// still means fewer rows exist than either bound, and an overflow goes to
|
|
3972
|
+
// the cursor, whose statement carries the caller's own LIMIT.
|
|
3973
|
+
const callerLimit = args?.limit ?? this.defaultLimit;
|
|
3974
|
+
const bound = callerLimit === undefined ? undefined : this.paginationValue(callerLimit, 'limit');
|
|
3253
3975
|
const speculativeDeferred = this.buildFindMany({
|
|
3254
3976
|
...args,
|
|
3255
|
-
limit: batchSize + 1,
|
|
3977
|
+
limit: bound !== undefined && bound <= batchSize ? bound : batchSize + 1,
|
|
3978
|
+
// Only the caller's own bound makes this a page (completePageOrder).
|
|
3979
|
+
...firstRowOnly(args),
|
|
3256
3980
|
});
|
|
3257
3981
|
const speculativeResult = await this.readWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout, streamPreparedName);
|
|
3258
3982
|
if (speculativeResult.rows.length <= batchSize) {
|
|
@@ -3276,22 +4000,100 @@ class QueryInterface {
|
|
|
3276
4000
|
// transaction control of its own (`ambientTransaction`).
|
|
3277
4001
|
const held = this.txScoped ? null : await this.acquireConnection();
|
|
3278
4002
|
const client = held?.client ?? null;
|
|
3279
|
-
const
|
|
4003
|
+
const base = client ?? {
|
|
3280
4004
|
query: async (text, values) => (await this.pool.query(text, values)),
|
|
3281
4005
|
};
|
|
4006
|
+
// The idle guard watches only a stream that holds a connection of its own
|
|
4007
|
+
// between pulls. Inside a caller's transaction the connection is theirs and
|
|
4008
|
+
// their transaction's timeout bounds it. On PostgreSQL the stream's BEGIN
|
|
4009
|
+
// also carries the server-side limit (see streamBackstopStatement), and the
|
|
4010
|
+
// guard keeps a live consumer's transaction from looking idle to it.
|
|
4011
|
+
const watched = held !== null && idleTimeoutMs > 0;
|
|
4012
|
+
const backstop = watched && this.dialect.capabilities.idleTransactionTimeout;
|
|
4013
|
+
let guard;
|
|
4014
|
+
const conn = watched
|
|
4015
|
+
? (0, stream_idle_js_1.watchStreamConnection)(base, () => guard, backstop
|
|
4016
|
+
? { begin: this.dialect.beginStatement(), statement: (0, stream_idle_js_1.streamBackstopStatement)(idleTimeoutMs) }
|
|
4017
|
+
: undefined)
|
|
4018
|
+
: base;
|
|
4019
|
+
// The cursor's statement carries the `sqlComment` like any other (the
|
|
4020
|
+
// speculative fetch above got it at the execute seam). Never named: a
|
|
4021
|
+
// cursor is declared by text.
|
|
4022
|
+
const streamSql = this.outgoing(deferred.sql, undefined, action).text;
|
|
4023
|
+
const stream = this.dialect.openStream(conn, streamSql, deferred.params, batchSize, {
|
|
4024
|
+
ambientTransaction: this.txScoped,
|
|
4025
|
+
});
|
|
4026
|
+
if (watched) {
|
|
4027
|
+
guard = new stream_idle_js_1.StreamIdleGuard(idle, idleTimeoutMs, () => (0, stream_idle_js_1.streamIdleExpiredError)(action, this.table, idleTimeoutMs, unit), () => this.expireIdleStream(stream, held, action, idleTimeoutMs, idle, unit), backstop ? () => base.query('SELECT 1') : undefined);
|
|
4028
|
+
}
|
|
3282
4029
|
try {
|
|
3283
|
-
yield
|
|
3284
|
-
|
|
3285
|
-
|
|
4030
|
+
// Driven by hand rather than `yield*`, so an expired stream's cleanup can
|
|
4031
|
+
// run from the timer while this generator is suspended.
|
|
4032
|
+
while (true) {
|
|
4033
|
+
const step = await stream.next();
|
|
4034
|
+
if (step.done)
|
|
4035
|
+
break;
|
|
4036
|
+
yield step.value;
|
|
4037
|
+
if (idle.expired)
|
|
4038
|
+
throw idle.expired;
|
|
4039
|
+
}
|
|
3286
4040
|
}
|
|
3287
4041
|
catch (err) {
|
|
4042
|
+
if (err === idle.expired)
|
|
4043
|
+
throw err;
|
|
3288
4044
|
// Wrap pg constraint errors so streaming surfaces typed errors like the rest of the API
|
|
3289
4045
|
throw (0, errors_js_1.explainConnectionLoss)((0, errors_js_1.wrapPgError)(err), held?.checkout.lostWith);
|
|
3290
4046
|
}
|
|
3291
4047
|
finally {
|
|
3292
|
-
|
|
4048
|
+
guard?.stop();
|
|
4049
|
+
if (guard?.expiry) {
|
|
4050
|
+
// The timer already closed the stream and released the connection;
|
|
4051
|
+
// wait for that so the connection is never handed back mid-cleanup.
|
|
4052
|
+
await guard.expiry;
|
|
4053
|
+
}
|
|
4054
|
+
else {
|
|
4055
|
+
try {
|
|
4056
|
+
// An early exit (`break`) closes the cursor here; after a full drain
|
|
4057
|
+
// or an error the dialect's generator has finished and this is a no-op.
|
|
4058
|
+
await stream.return(undefined);
|
|
4059
|
+
}
|
|
4060
|
+
catch (err) {
|
|
4061
|
+
// Only an early exit can reach this throw: after an error or a full
|
|
4062
|
+
// drain the dialect's generator has finished and return() resolves.
|
|
4063
|
+
// A failed CLOSE / COMMIT on an early exit must reach the caller, as
|
|
4064
|
+
// it did when this loop was a `yield*`.
|
|
4065
|
+
// biome-ignore lint/correctness/noUnsafeFinally: it replaces only an early-exit return, see above
|
|
4066
|
+
throw (0, errors_js_1.explainConnectionLoss)((0, errors_js_1.wrapPgError)(err), held?.checkout.lostWith);
|
|
4067
|
+
}
|
|
4068
|
+
finally {
|
|
4069
|
+
held?.checkout.release();
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
3293
4072
|
}
|
|
3294
4073
|
}
|
|
4074
|
+
/**
|
|
4075
|
+
* Close a stream whose consumer asked for nothing within its idle timeout:
|
|
4076
|
+
* run the dialect's own cleanup (PostgreSQL: CLOSE, COMMIT), release the
|
|
4077
|
+
* connection (with the cleanup's error, if any, so the pool discards it), and
|
|
4078
|
+
* log `stream-idle-timeout`. The log line matters: an abandoned consumer
|
|
4079
|
+
* never pulls again, so it never sees the E002 waiting for it.
|
|
4080
|
+
*/
|
|
4081
|
+
async expireIdleStream(stream, held, action, timeoutMs, idle, unit) {
|
|
4082
|
+
let failure;
|
|
4083
|
+
try {
|
|
4084
|
+
await stream.return(undefined);
|
|
4085
|
+
}
|
|
4086
|
+
catch (err) {
|
|
4087
|
+
failure = err;
|
|
4088
|
+
}
|
|
4089
|
+
finally {
|
|
4090
|
+
held.checkout.release(failure === undefined ? undefined : failure instanceof Error ? failure : true);
|
|
4091
|
+
}
|
|
4092
|
+
(0, logger_js_1.logTo)(this.logger, 'warn', `${action} on "${this.table}" had no ${unit} requested for ${timeoutMs} ms, so Turbine closed its cursor ` +
|
|
4093
|
+
`and released its connection (${idle.rows} ${idle.rows === 1 ? 'row' : 'rows'} handed out before that). ` +
|
|
4094
|
+
'An iterator that is kept and never finished, or a response stream torn down without calling return(), ' +
|
|
4095
|
+
'leaves a stream like this. The next request on it throws TimeoutError (E002).', { code: 'stream-idle-timeout', table: this.table, operation: action, timeoutMs, rows: idle.rows });
|
|
4096
|
+
}
|
|
3295
4097
|
/**
|
|
3296
4098
|
* Stream rows from a findMany query using PostgreSQL cursors, one BATCH of
|
|
3297
4099
|
* rows at a time.
|
|
@@ -3327,13 +4129,24 @@ class QueryInterface {
|
|
|
3327
4129
|
* ```
|
|
3328
4130
|
*/
|
|
3329
4131
|
async *findManyStreamBatches(args) {
|
|
4132
|
+
args = this.normalizeArgs(args);
|
|
4133
|
+
this.assertNoStreamLock(args, 'findManyStreamBatches');
|
|
3330
4134
|
const parse = this.makeStreamRowParser(args);
|
|
3331
|
-
|
|
4135
|
+
const idle = (0, stream_idle_js_1.newStreamIdleState)();
|
|
4136
|
+
for await (const batch of this.streamRaw(args, 'findManyStreamBatches', idle, 'batch')) {
|
|
3332
4137
|
const parsed = new Array(batch.length);
|
|
3333
4138
|
for (let i = 0; i < batch.length; i++) {
|
|
3334
4139
|
parsed[i] = parse(batch[i]);
|
|
3335
4140
|
}
|
|
4141
|
+
idle.rows += parsed.length;
|
|
4142
|
+
// `suspended` and `pulls` are the idle guard's whole view of the
|
|
4143
|
+
// consumer: two writes per yield, no timer work.
|
|
4144
|
+
idle.suspended = true;
|
|
3336
4145
|
yield parsed;
|
|
4146
|
+
idle.suspended = false;
|
|
4147
|
+
idle.pulls++;
|
|
4148
|
+
if (idle.expired)
|
|
4149
|
+
throw idle.expired;
|
|
3337
4150
|
}
|
|
3338
4151
|
}
|
|
3339
4152
|
/**
|
|
@@ -3372,14 +4185,25 @@ class QueryInterface {
|
|
|
3372
4185
|
* ```
|
|
3373
4186
|
*/
|
|
3374
4187
|
async *findManyStream(args) {
|
|
4188
|
+
args = this.normalizeArgs(args);
|
|
4189
|
+
this.assertNoStreamLock(args, 'findManyStream');
|
|
3375
4190
|
const parse = this.makeStreamRowParser(args);
|
|
3376
4191
|
// Parsed one row at a time, INSIDE the yield loop rather than a batch ahead
|
|
3377
4192
|
// of it, so a consumer that breaks early still pays only for the rows it
|
|
3378
4193
|
// took. That laziness is the reason this loop is not written over
|
|
3379
4194
|
// `findManyStreamBatches`.
|
|
3380
|
-
|
|
4195
|
+
const idle = (0, stream_idle_js_1.newStreamIdleState)();
|
|
4196
|
+
for await (const batch of this.streamRaw(args, 'findManyStream', idle, 'row')) {
|
|
3381
4197
|
for (let i = 0; i < batch.length; i++) {
|
|
3382
|
-
|
|
4198
|
+
const row = parse(batch[i]);
|
|
4199
|
+
idle.rows++;
|
|
4200
|
+
// See findManyStreamBatches: the idle guard's view of the consumer.
|
|
4201
|
+
idle.suspended = true;
|
|
4202
|
+
yield row;
|
|
4203
|
+
idle.suspended = false;
|
|
4204
|
+
idle.pulls++;
|
|
4205
|
+
if (idle.expired)
|
|
4206
|
+
throw idle.expired;
|
|
3383
4207
|
}
|
|
3384
4208
|
}
|
|
3385
4209
|
}
|
|
@@ -3387,28 +4211,28 @@ class QueryInterface {
|
|
|
3387
4211
|
// findFirst, like findMany but returns a single row or null
|
|
3388
4212
|
// -------------------------------------------------------------------------
|
|
3389
4213
|
async findFirst(args) {
|
|
3390
|
-
// BEFORE the strategy branch,
|
|
3391
|
-
//
|
|
3392
|
-
//
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
// changes the warning text from the declared name back to the caller's and
|
|
3396
|
-
// nothing else). It is here so the invariant holds at the seam rather than
|
|
3397
|
-
// depending on every consumer to re-establish it.
|
|
3398
|
-
args = this.normalizeArgs(args);
|
|
4214
|
+
// BEFORE the strategy branch, so the batched and auto paths below read the
|
|
4215
|
+
// same canonical args the builders do, and so an all-undefined `where` is
|
|
4216
|
+
// refused before anything runs (see normalizeArgs).
|
|
4217
|
+
args = this.normalizeArgs(args, 'findFirst');
|
|
4218
|
+
this.assertLockInTransaction(args, 'findFirst');
|
|
3399
4219
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
3400
4220
|
if (args?.with) {
|
|
3401
4221
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
3402
4222
|
// findFirst is findMany + LIMIT 1: batch the single base row, then load.
|
|
3403
4223
|
if (strategy === 'batched') {
|
|
3404
|
-
const rows = await this.runFindManyBatched({
|
|
4224
|
+
const rows = await this.runFindManyBatched({
|
|
4225
|
+
...args,
|
|
4226
|
+
limit: 1,
|
|
4227
|
+
...firstRowOnly(args),
|
|
4228
|
+
});
|
|
3405
4229
|
return (rows[0] ?? null);
|
|
3406
4230
|
}
|
|
3407
4231
|
if (strategy === 'auto') {
|
|
3408
4232
|
// findFirst is findMany + LIMIT 1: a one-row parent set.
|
|
3409
4233
|
const split = this.planAuto(args.with, args.stableRelationOrder, false, 1);
|
|
3410
4234
|
if (split) {
|
|
3411
|
-
const rows = (await this.runAutoSplit({ ...args, limit: 1 }, split, false));
|
|
4235
|
+
const rows = (await this.runAutoSplit({ ...args, limit: 1, ...firstRowOnly(args) }, split, false));
|
|
3412
4236
|
return (rows[0] ?? null);
|
|
3413
4237
|
}
|
|
3414
4238
|
}
|
|
@@ -3419,8 +4243,16 @@ class QueryInterface {
|
|
|
3419
4243
|
});
|
|
3420
4244
|
}
|
|
3421
4245
|
buildFindFirst(args) {
|
|
4246
|
+
return this.buildFindFirstAs(args, 'findFirst');
|
|
4247
|
+
}
|
|
4248
|
+
/**
|
|
4249
|
+
* findFirst / findFirstOrThrow over findMany's builder with `LIMIT 1`.
|
|
4250
|
+
* `operation` names the method in the refusal of an all-undefined `where`.
|
|
4251
|
+
*/
|
|
4252
|
+
buildFindFirstAs(args, operation) {
|
|
4253
|
+
args = this.normalizeArgs(args, operation);
|
|
3422
4254
|
// Reuse findMany's SQL builder but force LIMIT 1
|
|
3423
|
-
const findManyArgs = { ...args, limit: 1 };
|
|
4255
|
+
const findManyArgs = { ...args, limit: 1, ...firstRowOnly(args) };
|
|
3424
4256
|
const deferred = this.buildFindMany(findManyArgs);
|
|
3425
4257
|
return {
|
|
3426
4258
|
sql: deferred.sql,
|
|
@@ -3430,12 +4262,15 @@ class QueryInterface {
|
|
|
3430
4262
|
return rows.length > 0 ? rows[0] : null;
|
|
3431
4263
|
},
|
|
3432
4264
|
tag: `${this.table}.findFirst`,
|
|
4265
|
+
...(deferred.rowLock ? { rowLock: true } : {}),
|
|
3433
4266
|
};
|
|
3434
4267
|
}
|
|
3435
4268
|
// -------------------------------------------------------------------------
|
|
3436
4269
|
// findFirstOrThrow, like findFirst but throws if no record found
|
|
3437
4270
|
// -------------------------------------------------------------------------
|
|
3438
4271
|
async findFirstOrThrow(args) {
|
|
4272
|
+
args = this.normalizeArgs(args, 'findFirstOrThrow');
|
|
4273
|
+
this.assertLockInTransaction(args, 'findFirstOrThrow');
|
|
3439
4274
|
return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
|
|
3440
4275
|
const deferred = this.buildFindFirstOrThrow(args);
|
|
3441
4276
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3443,7 +4278,8 @@ class QueryInterface {
|
|
|
3443
4278
|
});
|
|
3444
4279
|
}
|
|
3445
4280
|
buildFindFirstOrThrow(args) {
|
|
3446
|
-
|
|
4281
|
+
args = this.normalizeArgs(args, 'findFirstOrThrow');
|
|
4282
|
+
const inner = this.buildFindFirstAs(args, 'findFirstOrThrow');
|
|
3447
4283
|
return {
|
|
3448
4284
|
sql: inner.sql,
|
|
3449
4285
|
params: inner.params,
|
|
@@ -3459,12 +4295,15 @@ class QueryInterface {
|
|
|
3459
4295
|
return row;
|
|
3460
4296
|
},
|
|
3461
4297
|
tag: `${this.table}.findFirstOrThrow`,
|
|
4298
|
+
...(inner.rowLock ? { rowLock: true } : {}),
|
|
3462
4299
|
};
|
|
3463
4300
|
}
|
|
3464
4301
|
// -------------------------------------------------------------------------
|
|
3465
4302
|
// findUniqueOrThrow, like findUnique but throws if no record found
|
|
3466
4303
|
// -------------------------------------------------------------------------
|
|
3467
4304
|
async findUniqueOrThrow(args) {
|
|
4305
|
+
args = this.normalizeArgs(args);
|
|
4306
|
+
this.assertLockInTransaction(args, 'findUniqueOrThrow');
|
|
3468
4307
|
return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
|
|
3469
4308
|
const deferred = this.buildFindUniqueOrThrow(args);
|
|
3470
4309
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3472,6 +4311,7 @@ class QueryInterface {
|
|
|
3472
4311
|
});
|
|
3473
4312
|
}
|
|
3474
4313
|
buildFindUniqueOrThrow(args) {
|
|
4314
|
+
args = this.normalizeArgs(args);
|
|
3475
4315
|
const inner = this.buildFindUnique(args);
|
|
3476
4316
|
return {
|
|
3477
4317
|
sql: inner.sql,
|
|
@@ -3488,12 +4328,14 @@ class QueryInterface {
|
|
|
3488
4328
|
return row;
|
|
3489
4329
|
},
|
|
3490
4330
|
tag: `${this.table}.findUniqueOrThrow`,
|
|
4331
|
+
...(inner.rowLock ? { rowLock: true } : {}),
|
|
3491
4332
|
};
|
|
3492
4333
|
}
|
|
3493
4334
|
// -------------------------------------------------------------------------
|
|
3494
4335
|
// create
|
|
3495
4336
|
// -------------------------------------------------------------------------
|
|
3496
4337
|
async create(args) {
|
|
4338
|
+
args = this.normalizeArgs(args);
|
|
3497
4339
|
return this.executeWithMiddleware('create', args, async () => {
|
|
3498
4340
|
if ((0, nested_write_js_1.hasRelationFields)(args.data, this.tableMeta)) {
|
|
3499
4341
|
return this.nestedCreate(args);
|
|
@@ -3506,16 +4348,55 @@ class QueryInterface {
|
|
|
3506
4348
|
// createMany, uses UNNEST for performance
|
|
3507
4349
|
// -------------------------------------------------------------------------
|
|
3508
4350
|
async createMany(args) {
|
|
4351
|
+
args = this.normalizeArgs(args);
|
|
3509
4352
|
return this.executeWithMiddleware('createMany', args, async () => {
|
|
3510
|
-
const
|
|
3511
|
-
const
|
|
3512
|
-
|
|
4353
|
+
const projection = this.resolveWriteProjection(args);
|
|
4354
|
+
const base = args;
|
|
4355
|
+
// One statement when the rows fit the engine's per-statement limits,
|
|
4356
|
+
// else chunks that do, all in one transaction so the rows land together
|
|
4357
|
+
// or not at all. (`buildCreateMany`, one statement for a pipeline,
|
|
4358
|
+
// refuses instead.)
|
|
4359
|
+
const { chunks, rowsPerChunk } = writesMod.planCreateMany(this.ctx, base, projection);
|
|
4360
|
+
const run = async () => {
|
|
4361
|
+
// An engine that applies two rows whose onConflict keys are equal only
|
|
4362
|
+
// under the column's collation (SQLite's NOCASE) as an insert and an
|
|
4363
|
+
// update of one row, in one statement, is caught by the transform (the
|
|
4364
|
+
// row comes back twice), so the statement runs in a transaction that
|
|
4365
|
+
// refusal rolls back. A reselect write already runs in one.
|
|
4366
|
+
const appliesRepeatedKeys = base.onConflict !== undefined &&
|
|
4367
|
+
this.dialect.capabilities.bulkUpsertAppliesRepeatedKeys &&
|
|
4368
|
+
this.dialect.resultStrategy !== 'reselect';
|
|
4369
|
+
if (chunks.length === 1 && !appliesRepeatedKeys) {
|
|
4370
|
+
return this.executeMutation(chunks[0], args.timeout);
|
|
4371
|
+
}
|
|
4372
|
+
const reselects = this.dialect.resultStrategy === 'reselect';
|
|
4373
|
+
return this.inOneTransaction(async (exec) => {
|
|
4374
|
+
const rows = [];
|
|
4375
|
+
for (const chunk of chunks) {
|
|
4376
|
+
const result = reselects && chunk.reselect
|
|
4377
|
+
? await chunk.reselect(exec)
|
|
4378
|
+
: await exec(chunk.sql, chunk.params, chunk.preparedName);
|
|
4379
|
+
rows.push(...chunk.transform(result));
|
|
4380
|
+
}
|
|
4381
|
+
return rows;
|
|
4382
|
+
}, args.timeout, `createMany of ${base.data.length} rows`, `Insert at most ${rowsPerChunk} rows per call, which fits one statement, or pass a pool that can check ` +
|
|
4383
|
+
'out a connection.');
|
|
4384
|
+
};
|
|
4385
|
+
if (base.onConflict === undefined)
|
|
4386
|
+
return run();
|
|
4387
|
+
try {
|
|
4388
|
+
return await run();
|
|
4389
|
+
}
|
|
4390
|
+
catch (err) {
|
|
4391
|
+
throw writesMod.explainConflictKeyCollision(this.ctx, base, err);
|
|
4392
|
+
}
|
|
3513
4393
|
});
|
|
3514
4394
|
}
|
|
3515
4395
|
// -------------------------------------------------------------------------
|
|
3516
4396
|
// update
|
|
3517
4397
|
// -------------------------------------------------------------------------
|
|
3518
4398
|
async update(args) {
|
|
4399
|
+
args = this.normalizeArgs(args);
|
|
3519
4400
|
return this.executeWithMiddleware('update', args, async () => {
|
|
3520
4401
|
if ((0, nested_write_js_1.hasRelationFields)(args.data, this.tableMeta)) {
|
|
3521
4402
|
return this.nestedUpdate(args);
|
|
@@ -3628,6 +4509,7 @@ class QueryInterface {
|
|
|
3628
4509
|
// delete
|
|
3629
4510
|
// -------------------------------------------------------------------------
|
|
3630
4511
|
async delete(args) {
|
|
4512
|
+
args = this.normalizeArgs(args);
|
|
3631
4513
|
return this.executeWithMiddleware('delete', args, async () => {
|
|
3632
4514
|
const deferred = this.buildDelete(args);
|
|
3633
4515
|
return this.executeMutation(deferred, args.timeout);
|
|
@@ -3637,7 +4519,11 @@ class QueryInterface {
|
|
|
3637
4519
|
// upsert, INSERT ... ON CONFLICT ... DO UPDATE
|
|
3638
4520
|
// -------------------------------------------------------------------------
|
|
3639
4521
|
async upsert(args) {
|
|
4522
|
+
args = this.normalizeArgs(args);
|
|
3640
4523
|
return this.executeWithMiddleware('upsert', args, async () => {
|
|
4524
|
+
// First: the nested-write branch below decides before upsertLookupReason
|
|
4525
|
+
// would have refused it.
|
|
4526
|
+
writesMod.assertNoOptimisticLock(this.ctx, args, 'upsert');
|
|
3641
4527
|
if (this.upsertNeedsLookup(args)) {
|
|
3642
4528
|
return this.upsertLookupFirst(args);
|
|
3643
4529
|
}
|
|
@@ -3652,6 +4538,7 @@ class QueryInterface {
|
|
|
3652
4538
|
* an upsert or has to run the array inside a transaction.
|
|
3653
4539
|
*/
|
|
3654
4540
|
upsertNeedsLookup(args) {
|
|
4541
|
+
args = this.normalizeArgs(args);
|
|
3655
4542
|
return ((0, nested_write_js_1.hasRelationFields)((args.create ?? {}), this.tableMeta) ||
|
|
3656
4543
|
(0, nested_write_js_1.hasRelationFields)((args.update ?? {}), this.tableMeta) ||
|
|
3657
4544
|
writesMod.upsertLookupReason(this.ctx, args) !== null);
|
|
@@ -3680,7 +4567,11 @@ class QueryInterface {
|
|
|
3680
4567
|
async upsertLookupFirst(args) {
|
|
3681
4568
|
const where = (args.where ?? {});
|
|
3682
4569
|
const create = (args.create ?? {});
|
|
3683
|
-
|
|
4570
|
+
// `expr.excluded(field)` in `update` reads the incoming row, which here is
|
|
4571
|
+
// `create`: resolved to create's value before the update compiles, as the
|
|
4572
|
+
// single-statement path does.
|
|
4573
|
+
const update = ((0, expr_js_1.resolveExcludedFromCreate)(this.ctx, (args.update ?? {}), create) ??
|
|
4574
|
+
{});
|
|
3684
4575
|
(0, compound_unique_js_1.assertMutationWhereIdentifiesOneRow)(this.tableMeta, this.table, (0, compound_unique_js_1.expandCompoundUniqueWhere)(this.tableMeta, where), 'upsert');
|
|
3685
4576
|
this.resolveWriteProjection(args);
|
|
3686
4577
|
// An undefined option is an absent one on every call below.
|
|
@@ -3714,6 +4605,7 @@ class QueryInterface {
|
|
|
3714
4605
|
// updateMany, UPDATE ... WHERE ... returning count
|
|
3715
4606
|
// -------------------------------------------------------------------------
|
|
3716
4607
|
async updateMany(args) {
|
|
4608
|
+
args = this.normalizeArgs(args);
|
|
3717
4609
|
return this.executeWithMiddleware('updateMany', args, async () => {
|
|
3718
4610
|
const deferred = this.buildUpdateMany(args);
|
|
3719
4611
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
|
|
@@ -3724,6 +4616,7 @@ class QueryInterface {
|
|
|
3724
4616
|
// deleteMany, DELETE ... WHERE ... returning count
|
|
3725
4617
|
// -------------------------------------------------------------------------
|
|
3726
4618
|
async deleteMany(args) {
|
|
4619
|
+
args = this.normalizeArgs(args);
|
|
3727
4620
|
return this.executeWithMiddleware('deleteMany', args, async () => {
|
|
3728
4621
|
const deferred = this.buildDeleteMany(args);
|
|
3729
4622
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
|
|
@@ -3734,6 +4627,7 @@ class QueryInterface {
|
|
|
3734
4627
|
// count
|
|
3735
4628
|
// -------------------------------------------------------------------------
|
|
3736
4629
|
async count(args) {
|
|
4630
|
+
args = this.normalizeArgs(args);
|
|
3737
4631
|
return this.executeWithMiddleware('count', (args ?? {}), async () => {
|
|
3738
4632
|
const deferred = this.buildCount(args);
|
|
3739
4633
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3741,6 +4635,7 @@ class QueryInterface {
|
|
|
3741
4635
|
});
|
|
3742
4636
|
}
|
|
3743
4637
|
buildCount(args) {
|
|
4638
|
+
args = this.normalizeArgs(args);
|
|
3744
4639
|
this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args?.skipGlobalFilters);
|
|
3745
4640
|
const effWhere = this.mergeGlobalFilter(args?.where);
|
|
3746
4641
|
const hasWhere = effWhere !== undefined;
|
|
@@ -3778,6 +4673,7 @@ class QueryInterface {
|
|
|
3778
4673
|
* that cannot be typed, so those columns are not projected onto the row type.
|
|
3779
4674
|
*/
|
|
3780
4675
|
async groupBy(args) {
|
|
4676
|
+
args = this.normalizeArgs(args);
|
|
3781
4677
|
return this.executeWithMiddleware('groupBy', args, async () => {
|
|
3782
4678
|
const deferred = this.buildGroupBy(args);
|
|
3783
4679
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3788,15 +4684,16 @@ class QueryInterface {
|
|
|
3788
4684
|
// Aggregate / groupBy compilation (extracted to aggregates.ts).
|
|
3789
4685
|
// ---------------------------------------------------------------------------
|
|
3790
4686
|
buildGroupBy(args) {
|
|
3791
|
-
return aggMod.buildGroupBy(this.ctx, args);
|
|
4687
|
+
return aggMod.buildGroupBy(this.ctx, this.normalizeArgs(args));
|
|
3792
4688
|
}
|
|
3793
4689
|
buildAggregate(args) {
|
|
3794
|
-
return aggMod.buildAggregate(this.ctx, args);
|
|
4690
|
+
return aggMod.buildAggregate(this.ctx, this.normalizeArgs(args));
|
|
3795
4691
|
}
|
|
3796
4692
|
// -------------------------------------------------------------------------
|
|
3797
4693
|
// aggregate, standalone aggregation without groupBy
|
|
3798
4694
|
// -------------------------------------------------------------------------
|
|
3799
4695
|
async aggregate(args) {
|
|
4696
|
+
args = this.normalizeArgs(args);
|
|
3800
4697
|
return this.executeWithMiddleware('aggregate', args, async () => {
|
|
3801
4698
|
const deferred = this.buildAggregate(args);
|
|
3802
4699
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3914,14 +4811,15 @@ class QueryInterface {
|
|
|
3914
4811
|
}
|
|
3915
4812
|
return relationsMod.planFlattenWith(this.ctx, this.table, withClause, includePii);
|
|
3916
4813
|
}
|
|
3917
|
-
/**
|
|
4814
|
+
/** Advice, once per reason: `'flatten'` was refused for the whole query. */
|
|
3918
4815
|
warnFlattenBlocked(reason) {
|
|
3919
|
-
|
|
4816
|
+
const log = this.advice;
|
|
4817
|
+
if (!log)
|
|
3920
4818
|
return;
|
|
3921
4819
|
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.flattenFallback, `${this.table}|query|${reason}`))
|
|
3922
4820
|
return;
|
|
3923
|
-
|
|
3924
|
-
'Every relation loads via the correlated subquery instead (same rows, same values, different plan).');
|
|
4821
|
+
(0, logger_js_1.logTo)(log, 'warn', `relationLoadStrategy: 'flatten' did not engage on "${this.table}": ${reason}. ` +
|
|
4822
|
+
'Every relation loads via the correlated subquery instead (same rows, same values, different plan).', { code: 'flatten-blocked', table: this.table, reason });
|
|
3925
4823
|
}
|
|
3926
4824
|
/**
|
|
3927
4825
|
* Convert a field name to its snake_case column name (unquoted, for non-SQL
|
|
@@ -4056,8 +4954,8 @@ class QueryInterface {
|
|
|
4056
4954
|
// To-many relation orderBy (`{ posts: { _count } }`) uses the same count
|
|
4057
4955
|
// subquery as `_count`: mirror its global-filter params. Pick-row
|
|
4058
4956
|
// ordering mirrors its full param chain (by-path / global filter /
|
|
4059
|
-
// pick.where / pick.orderBy paths). To-one relation orderBy
|
|
4060
|
-
//
|
|
4957
|
+
// pick.where / pick.orderBy paths). To-one relation orderBy is mirrored
|
|
4958
|
+
// by running its build (see collectToOneRelationOrderParams).
|
|
4061
4959
|
if (this.isRelationOrderByValue(dir)) {
|
|
4062
4960
|
// Mirrors the build path's resolution, so a cache HIT binds params for
|
|
4063
4961
|
// the same relation the cached SQL was built from.
|
|
@@ -4070,9 +4968,7 @@ class QueryInterface {
|
|
|
4070
4968
|
this.collectRelationCountParams(relDef, params);
|
|
4071
4969
|
}
|
|
4072
4970
|
else if (relDef) {
|
|
4073
|
-
|
|
4074
|
-
this.collectTargetGlobalFilterAlias(relDef.to, params);
|
|
4075
|
-
}
|
|
4971
|
+
relationsMod.collectToOneRelationOrderParams(this.ctx, key, dir, params);
|
|
4076
4972
|
}
|
|
4077
4973
|
}
|
|
4078
4974
|
}
|
|
@@ -4144,10 +5040,10 @@ class QueryInterface {
|
|
|
4144
5040
|
* one, is the acknowledgement, and the warning exists to surface an
|
|
4145
5041
|
* unacknowledged trade rather than to nag.
|
|
4146
5042
|
*
|
|
4147
|
-
* NOT
|
|
4148
|
-
*
|
|
4149
|
-
*
|
|
4150
|
-
*
|
|
5043
|
+
* NOT ADVICE: it fires whatever `diagnostics` says, deliberately. Production
|
|
5044
|
+
* is exactly where a destructive write commits and where the row cannot be
|
|
5045
|
+
* recovered afterwards, so a warning that goes quiet in production mode is
|
|
5046
|
+
* silent in the only place it matters. The cost is
|
|
4151
5047
|
* bounded to the point of irrelevance: once per process per field, and only
|
|
4152
5048
|
* on a row that actually held an infinity.
|
|
4153
5049
|
*
|
|
@@ -4160,7 +5056,7 @@ class QueryInterface {
|
|
|
4160
5056
|
return;
|
|
4161
5057
|
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.temporalInfinity, `${table}.${field}`))
|
|
4162
5058
|
return;
|
|
4163
|
-
|
|
5059
|
+
(0, logger_js_1.logTo)(this.logger, 'warn', `${table}.${field} holds the Postgres value \`infinity\` (or \`-infinity\`). JavaScript has ` +
|
|
4164
5060
|
'no Date for it, so it reads as the JS number `Infinity` / `-Infinity` on a field the generated ' +
|
|
4165
5061
|
'types declare as `Date`: `.toISOString()` and `.getTime()` throw a TypeError on these rows, and ' +
|
|
4166
5062
|
'`JSON.stringify` still renders them null because JSON has no infinity literal. The number is the ' +
|
|
@@ -4170,7 +5066,7 @@ class QueryInterface {
|
|
|
4170
5066
|
"cannot tell infinity, -infinity and NULL apart. Set `temporalInfinity: 'preserve'` to confirm this " +
|
|
4171
5067
|
"reading and silence the warning, or `'null'` to read null instead, accepting that a stored " +
|
|
4172
5068
|
'infinity then looks exactly like a stored NULL and a read-modify-write over a nullable column ' +
|
|
4173
|
-
'stores SQL NULL, destroying the value with no error.');
|
|
5069
|
+
'stores SQL NULL, destroying the value with no error.', { code: 'temporal-infinity', table, field });
|
|
4174
5070
|
}
|
|
4175
5071
|
/**
|
|
4176
5072
|
* Apply the configured reading to the infinity elements of a temporal ARRAY
|
|
@@ -4250,6 +5146,64 @@ class QueryInterface {
|
|
|
4250
5146
|
// UTC so results don't depend on the server's time zone.
|
|
4251
5147
|
return this.utcTimestamps ? (0, utils_js_1.parseDbDate)(String(value)) : new Date(value);
|
|
4252
5148
|
}
|
|
5149
|
+
/** The non-temporal decode kinds of `table`'s columns, by column and by field name. */
|
|
5150
|
+
getCellKinds(table, meta) {
|
|
5151
|
+
let kinds = this.cellKindCache.get(table);
|
|
5152
|
+
if (!kinds) {
|
|
5153
|
+
kinds = new Map();
|
|
5154
|
+
for (const column of meta.columns) {
|
|
5155
|
+
// Metadata may carry the type on the column or only in the table map.
|
|
5156
|
+
const pgType = column.pgType ?? meta.pgTypes?.[column.name];
|
|
5157
|
+
if (typeof pgType !== 'string')
|
|
5158
|
+
continue;
|
|
5159
|
+
let kind = cellKindOf(pgType, this.schema.enums);
|
|
5160
|
+
const decode = kind === CELL_PLAIN ? this.dialect.decodeColumnValue?.(pgType) : undefined;
|
|
5161
|
+
if (decode !== undefined) {
|
|
5162
|
+
kind = CELL_DIALECT;
|
|
5163
|
+
let decoders = this.dialectDecodeCache.get(table);
|
|
5164
|
+
if (!decoders) {
|
|
5165
|
+
decoders = new Map();
|
|
5166
|
+
this.dialectDecodeCache.set(table, decoders);
|
|
5167
|
+
}
|
|
5168
|
+
decoders.set(column.name, decode);
|
|
5169
|
+
decoders.set(column.field, decode);
|
|
5170
|
+
}
|
|
5171
|
+
if (kind === CELL_PLAIN)
|
|
5172
|
+
continue;
|
|
5173
|
+
kinds.set(column.name, kind);
|
|
5174
|
+
kinds.set(column.field, kind);
|
|
5175
|
+
}
|
|
5176
|
+
this.cellKindCache.set(table, kinds);
|
|
5177
|
+
}
|
|
5178
|
+
return kinds;
|
|
5179
|
+
}
|
|
5180
|
+
/** One cell's decode, for every kind but {@link CELL_PLAIN}. */
|
|
5181
|
+
decodeCell(kind, value, table, field) {
|
|
5182
|
+
switch (kind) {
|
|
5183
|
+
case CELL_TEMPORAL:
|
|
5184
|
+
return this.decodeTemporalCell(value, table, field);
|
|
5185
|
+
case CELL_FLOAT:
|
|
5186
|
+
return (0, utils_js_1.readJsonFloat)(value);
|
|
5187
|
+
case CELL_FLOAT_ARRAY:
|
|
5188
|
+
return (0, utils_js_1.readJsonFloatArray)(value);
|
|
5189
|
+
case CELL_TEXT_ARRAY:
|
|
5190
|
+
return (0, utils_js_1.readTextArray)(value);
|
|
5191
|
+
case CELL_DIALECT:
|
|
5192
|
+
return this.decodeDialectCell(value, table, field);
|
|
5193
|
+
default:
|
|
5194
|
+
return value;
|
|
5195
|
+
}
|
|
5196
|
+
}
|
|
5197
|
+
/**
|
|
5198
|
+
* A {@link CELL_DIALECT} cell: the dialect's decode for the column's declared
|
|
5199
|
+
* type, recorded by {@link getCellKinds} when it classified the column.
|
|
5200
|
+
*/
|
|
5201
|
+
decodeDialectCell(value, table, field) {
|
|
5202
|
+
if (value === null || value === undefined)
|
|
5203
|
+
return value;
|
|
5204
|
+
const decode = this.dialectDecodeCache.get(table)?.get(field);
|
|
5205
|
+
return decode ? decode(value) : value;
|
|
5206
|
+
}
|
|
4253
5207
|
/**
|
|
4254
5208
|
* Build the decode plan for one exact column list, and remember it as this
|
|
4255
5209
|
* table's most recent shape so the next row of the same result set hits the
|
|
@@ -4273,16 +5227,19 @@ class QueryInterface {
|
|
|
4273
5227
|
// camelCase-keyed date fields, so nested json_build_object rows (whose
|
|
4274
5228
|
// keys are already camelCase) get the same Date coercion as top-level rows.
|
|
4275
5229
|
const camelDateFields = this.getCamelDateFields(table, meta);
|
|
5230
|
+
const cellKinds = this.getCellKinds(table, meta);
|
|
4276
5231
|
const cols = keys.slice();
|
|
4277
5232
|
const fields = [];
|
|
4278
|
-
const
|
|
5233
|
+
const kinds = [];
|
|
4279
5234
|
for (const col of cols) {
|
|
4280
5235
|
const field = reverseMap[col] ?? col; // fall back to raw col name, not regex
|
|
4281
5236
|
fields.push(field);
|
|
4282
5237
|
// Top-level rows are snake_case (dateCols); nested rows are camelCase.
|
|
4283
|
-
|
|
5238
|
+
kinds.push(dateCols.has(col) || camelDateFields.has(field)
|
|
5239
|
+
? CELL_TEMPORAL
|
|
5240
|
+
: (cellKinds.get(col) ?? cellKinds.get(field) ?? CELL_PLAIN));
|
|
4284
5241
|
}
|
|
4285
|
-
plan = { cols, fields,
|
|
5242
|
+
plan = { cols, fields, kinds };
|
|
4286
5243
|
this.rowPlanCache.set(shapeKey, plan);
|
|
4287
5244
|
}
|
|
4288
5245
|
this.rowPlanLast.set(table, plan);
|
|
@@ -4326,11 +5283,16 @@ class QueryInterface {
|
|
|
4326
5283
|
const plan = last !== undefined && QueryInterface.rowPlanMatches(last, keys)
|
|
4327
5284
|
? last
|
|
4328
5285
|
: this.buildRowDecodePlan(table, meta, keys);
|
|
4329
|
-
const { cols, fields,
|
|
5286
|
+
const { cols, fields, kinds } = plan;
|
|
4330
5287
|
for (let i = 0; i < cols.length; i++) {
|
|
4331
5288
|
const field = fields[i];
|
|
4332
5289
|
const value = row[cols[i]];
|
|
4333
|
-
|
|
5290
|
+
const kind = kinds[i];
|
|
5291
|
+
// A float from the driver is already a number; only JSON's strings need reading.
|
|
5292
|
+
parsed[field] =
|
|
5293
|
+
kind === CELL_PLAIN || (kind === CELL_FLOAT && typeof value !== 'string')
|
|
5294
|
+
? value
|
|
5295
|
+
: this.decodeCell(kind, value, table, field);
|
|
4334
5296
|
}
|
|
4335
5297
|
}
|
|
4336
5298
|
else {
|