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
package/dist/query/builder.js
CHANGED
|
@@ -12,105 +12,75 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { acquireConnection, openCheckout } from '../checkout.js';
|
|
14
14
|
import { settleEventLoop } from '../connection-guard.js';
|
|
15
|
+
import { resolveDiagnostics } from '../diagnostics.js';
|
|
15
16
|
import { postgresDialect } from '../dialect.js';
|
|
16
|
-
import { explainConnectionLoss, isStaleConnectionError, NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from '../errors.js';
|
|
17
|
+
import { explainConnectionLoss, INVARIANT_REPORT_HINT, InvariantError, isStaleConnectionError, NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from '../errors.js';
|
|
17
18
|
import { missingIndexForRelation, schemaHasIndexInfo } from '../index-advisor.js';
|
|
19
|
+
import { consoleLogger, logTo } from '../logger.js';
|
|
18
20
|
import { executeNestedCreate, executeNestedUpdate, extractRelationFields, hasRelationFields, } from '../nested-write.js';
|
|
19
21
|
import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
22
|
+
import { transactionStateOf, withSavepoint } from '../transaction-state.js';
|
|
20
23
|
import * as aggMod from './aggregates.js';
|
|
21
24
|
import { assertProjectionShape, defaultProjectionFields, includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, rejectNestedPickOrder, resolveCountRelations, stripFields, } from './batched-loader.js';
|
|
22
|
-
import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, } from './compound-unique.js';
|
|
23
|
-
import {
|
|
25
|
+
import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, uniqueKeyNames, whereIdentifiesOneRow, } from './compound-unique.js';
|
|
26
|
+
import { resolveExcludedFromCreate } from './expr.js';
|
|
27
|
+
import { cursorFieldValue, dedupeColumnList, dedupeOrderEntries, isEmptyOrderBy, isJsonPathOrderBy, isRelationPickOrderBy, isVectorOrderBy, normalizeOrderBy, operandColumnKind, orderByEntries, sortedEntries, } from './filters.js';
|
|
28
|
+
import { assertCursorNotEmptied, assertCursorWritten, assertWhereNotEmptiedByPruning, hasCursor, isCursorInclusive, pruneUndefinedArgs, } from './normalize-args.js';
|
|
24
29
|
import { warnUnknownQueryOptions } from './option-surface.js';
|
|
25
|
-
import { applyStableRelationOrderTo, normalizeWithClause } from './relation-names.js';
|
|
30
|
+
import { applyStableRelationOrderTo, normalizeWithClause, normalizeWithPagination } from './relation-names.js';
|
|
26
31
|
import * as relationsMod from './relations.js';
|
|
32
|
+
import { lockOnStreamMessage, lockOutsideTransactionMessage, lockWithDistinctMessage, resolveRowLock, } from './row-lock.js';
|
|
33
|
+
import { resolveSqlComment } from './sql-comment.js';
|
|
34
|
+
import { DEFAULT_STREAM_IDLE_TIMEOUT_MS, newStreamIdleState, resolveStreamIdleTimeout, StreamIdleGuard, streamBackstopStatement, streamIdleExpiredError, watchStreamConnection, } from './stream-idle.js';
|
|
27
35
|
import { resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
|
|
28
|
-
import { availableClause, isTemporalInfinity, LRUCache, normalizePagination, ownLookup, parseDbDate, resolveColumnName, resolveRelation, resolveRelationDef, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
29
|
-
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
36
|
+
import { assertMiddlewareArgsKept, availableClause, boundedPreparedName, canCancelBackend, isNodePostgresPool, isTemporalInfinity, LRUCache, normalizePagination, ownLookup, parseDbDate, readJsonFloat, readJsonFloatArray, readOnlyMiddlewareParams, readTextArray, resolveColumnName, resolveRelation, resolveRelationDef, sendCancelRequest, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
37
|
+
import { resetWarnOnce, shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
30
38
|
import * as whereMod from './where.js';
|
|
31
39
|
import * as writesMod from './writes.js';
|
|
32
40
|
/**
|
|
33
|
-
* SQL-cache lockstep cross-check
|
|
41
|
+
* SQL-cache lockstep cross-check.
|
|
34
42
|
*
|
|
35
43
|
* The SQL template cache requires three code paths to enumerate where-clause
|
|
36
44
|
* keys identically: `fingerprintWhere` (builds the cache key),
|
|
37
45
|
* `buildWhereClause` (builds SQL + `$N` params on a MISS), and
|
|
38
46
|
* `collectWhereParams` (re-collects params on a HIT without rebuilding). Since
|
|
39
47
|
* 0.36 all three consume ONE shared where-key walk (see `where-compile.ts`), so
|
|
40
|
-
* enumeration drift is structurally eliminated;
|
|
48
|
+
* enumeration drift is structurally eliminated; these checks remain as a
|
|
41
49
|
* tripwire in case a future leaf builder or its collect mirror falls out of
|
|
42
|
-
* step.
|
|
43
|
-
*
|
|
50
|
+
* step. Every failure is an {@link InvariantError} (E020), thrown before the
|
|
51
|
+
* statement is sent.
|
|
44
52
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* -
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* -
|
|
52
|
-
* `
|
|
53
|
-
*
|
|
54
|
-
* throws, same as dev. `0`, unset, or an unparseable value means never check.
|
|
55
|
-
*/
|
|
56
|
-
function cacheCrossCheckMode() {
|
|
57
|
-
const env = crossCheckEnv ?? readCrossCheckEnv();
|
|
58
|
-
if (env.mode !== 'sampled')
|
|
59
|
-
return env.mode;
|
|
60
|
-
// The SAMPLING decision, and only it, is genuinely per hit.
|
|
61
|
-
return env.rate >= 1 || Math.random() < env.rate ? 'sampled' : 'off';
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* The env-derived half of {@link cacheCrossCheckMode}, resolved ONCE per
|
|
65
|
-
* process and memoized.
|
|
66
|
-
*
|
|
67
|
-
* `process.env` is not a plain object: every property read crosses a C++
|
|
68
|
-
* interceptor. Measured at 112.7 ns per read against 3.6 ns for a module-level
|
|
69
|
-
* constant, and this function ran TWO reads on every cache hit, which put it
|
|
70
|
-
* at the top of the SQL-build CPU profile (12.53% self time, with a further
|
|
71
|
-
* ~12% inside the interceptor) for a value that does not change.
|
|
72
|
-
*
|
|
73
|
-
* It is memoized rather than hoisted to a `const` because the toggle IS
|
|
74
|
-
* observed at runtime, contrary to what a quick look suggests: the
|
|
75
|
-
* cross-check and sampling suites flip `NODE_ENV` /
|
|
76
|
-
* `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE` around a
|
|
77
|
-
* synchronous block in-process, not at spawn. So the read is deferred to first
|
|
78
|
-
* use and {@link resetCacheCrossCheckEnv} re-arms it; that is the entire
|
|
79
|
-
* contract, and the only thing lost versus reading every time is that a
|
|
80
|
-
* process which mutates these variables mid-run must say so.
|
|
81
|
-
*/
|
|
82
|
-
let crossCheckEnv;
|
|
83
|
-
function readCrossCheckEnv() {
|
|
84
|
-
let resolved;
|
|
85
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
86
|
-
resolved = { mode: process.env.TURBINE_DISABLE_CACHE_CHECK === '1' ? 'off' : 'dev' };
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
const raw = process.env.TURBINE_CACHE_CHECK_SAMPLE;
|
|
90
|
-
const rate = raw === undefined ? Number.NaN : Number.parseFloat(raw);
|
|
91
|
-
resolved = !Number.isFinite(rate) || rate <= 0 ? { mode: 'off' } : { mode: 'sampled', rate };
|
|
92
|
-
}
|
|
93
|
-
crossCheckEnv = resolved;
|
|
94
|
-
return resolved;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Discard the memoized cross-check environment so the next cache hit re-reads
|
|
98
|
-
* `process.env`.
|
|
53
|
+
* Two checks always run, whatever the diagnostics mode, because each costs one
|
|
54
|
+
* array compare and the alternative is executing parameters known to be wrong:
|
|
55
|
+
* - On a MISS, the params the build closure pushed are compared with the params
|
|
56
|
+
* the collect path produced (`sql-cache-lockstep`). The miss already paid
|
|
57
|
+
* for both walks, and a shape run once per instance is otherwise never
|
|
58
|
+
* compared at all.
|
|
59
|
+
* - On a HIT, the collected param count must equal the count the entry was
|
|
60
|
+
* built with (`sql-cache-param-count`). Otherwise the server would reject
|
|
61
|
+
* the statement with a bind error that reads like a connection problem.
|
|
99
62
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
63
|
+
* The full rebuild-and-compare on a HIT follows the client's
|
|
64
|
+
* `diagnostics.cacheCheck`, resolved once at construction (see diagnostics.ts;
|
|
65
|
+
* the legacy `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE`
|
|
66
|
+
* variables are read there and nowhere else):
|
|
67
|
+
* - `'every-hit'` (the development default): every hit is compared, and a
|
|
68
|
+
* mismatch throws.
|
|
69
|
+
* - `{ sample }`: that fraction of hits is compared (per-hit `Math.random()`;
|
|
70
|
+
* a sample of 1 compares every hit). A mismatch is logged at `error` once
|
|
71
|
+
* per distinct fingerprint, then throws.
|
|
72
|
+
* - `'off'` (the production default): never rebuilt on a hit.
|
|
103
73
|
*/
|
|
104
|
-
export function resetCacheCrossCheckEnv() {
|
|
105
|
-
crossCheckEnv = undefined;
|
|
106
|
-
}
|
|
107
74
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
75
|
+
* Warn-once namespace for the sampled cross-check's mismatch log, so a hot
|
|
76
|
+
* mismatch logs exactly once per table and shape instead of flooding the log.
|
|
77
|
+
* `'every-hit'` does not log (it throws immediately), so only the sampled path
|
|
78
|
+
* records here. Keyed on the TABLE as well as the fingerprint: the fingerprint
|
|
79
|
+
* alone carries no table, so a mismatch on one table used to silence the same
|
|
80
|
+
* shape's mismatch on every other table. The shared registry also bounds the
|
|
81
|
+
* set (`WARN_ONCE_CAP`) and is what `resetWarnOnce` clears.
|
|
112
82
|
*/
|
|
113
|
-
const
|
|
83
|
+
const CACHE_MISMATCH_WARN_NS = 'cacheMismatch';
|
|
114
84
|
/**
|
|
115
85
|
* Marginal cost of keeping a to-one relation on the JOIN plan, per parent row.
|
|
116
86
|
*
|
|
@@ -268,6 +238,29 @@ const ROW_PLAN_CACHE_SIZE = 128;
|
|
|
268
238
|
* probe's own limit.
|
|
269
239
|
*/
|
|
270
240
|
const STREAM_SPECULATION_MAX_BATCH = 1000;
|
|
241
|
+
/** Per-cell decode work in a {@link RowDecodePlan}. */
|
|
242
|
+
const CELL_PLAIN = 0;
|
|
243
|
+
const CELL_TEMPORAL = 1;
|
|
244
|
+
const CELL_FLOAT = 2;
|
|
245
|
+
const CELL_FLOAT_ARRAY = 3;
|
|
246
|
+
const CELL_TEXT_ARRAY = 4;
|
|
247
|
+
/** The dialect's own decode for a type whose storage differs (see `Dialect.decodeColumnValue`). */
|
|
248
|
+
const CELL_DIALECT = 5;
|
|
249
|
+
const FLOAT_TYPES = new Set(['float4', 'float8', 'real', 'double precision']);
|
|
250
|
+
const FLOAT_ARRAY_TYPES = new Set(['_float4', '_float8', 'float4[]', 'float8[]', 'real[]', 'double precision[]']);
|
|
251
|
+
/**
|
|
252
|
+
* The decode a column of `pgType` needs beyond the driver's, other than
|
|
253
|
+
* temporal: a float can arrive from JSON as "NaN" / "Infinity", and an enum
|
|
254
|
+
* array, which the driver has no parser for, as array text.
|
|
255
|
+
*/
|
|
256
|
+
function cellKindOf(pgType, enums) {
|
|
257
|
+
if (FLOAT_TYPES.has(pgType))
|
|
258
|
+
return CELL_FLOAT;
|
|
259
|
+
if (FLOAT_ARRAY_TYPES.has(pgType))
|
|
260
|
+
return CELL_FLOAT_ARRAY;
|
|
261
|
+
const element = pgType.startsWith('_') ? pgType.slice(1) : pgType.endsWith('[]') ? pgType.slice(0, -2) : undefined;
|
|
262
|
+
return element !== undefined && enums !== undefined && Object.hasOwn(enums, element) ? CELL_TEXT_ARRAY : CELL_PLAIN;
|
|
263
|
+
}
|
|
271
264
|
/**
|
|
272
265
|
* Strict structural equality for a single SQL parameter value. Handles the
|
|
273
266
|
* value shapes Turbine binds: primitives (incl. `NaN` and `bigint`), `null`/
|
|
@@ -410,35 +403,95 @@ export function unlockNestedWriteTx(tx) {
|
|
|
410
403
|
*/
|
|
411
404
|
// Moved to ./filters.ts in 0.74.0 so PowqlInterface consumes the SAME
|
|
412
405
|
// predicate rather than a second copy of it. See applyStableRelationOrderTo.
|
|
406
|
+
/**
|
|
407
|
+
* Marks `findMany` args built for a `findFirst`: one row, not a page, so
|
|
408
|
+
* {@link QueryInterface.completePageOrder} leaves its order as written. A
|
|
409
|
+
* symbol, so it survives the spreads at the args seam and no JSON body can
|
|
410
|
+
* set it. Set through {@link firstRowOnly} only.
|
|
411
|
+
*/
|
|
412
|
+
const FIRST_ROW_ONLY = Symbol('turbine.firstRowOnly');
|
|
413
|
+
/**
|
|
414
|
+
* The {@link FIRST_ROW_ONLY} mark for a read the caller bounded with neither
|
|
415
|
+
* `limit` nor `offset`. An `offset` makes the row depend on which tied rows it
|
|
416
|
+
* skipped, so `findFirst({ orderBy, offset })` is a page of one and gets the
|
|
417
|
+
* page tiebreaker like any other page.
|
|
418
|
+
*/
|
|
419
|
+
function firstRowOnly(args) {
|
|
420
|
+
return args?.limit === undefined && args?.offset === undefined ? { [FIRST_ROW_ONLY]: true } : {};
|
|
421
|
+
}
|
|
422
|
+
/** The largest delay `setTimeout` honours; Node fires anything larger after 1 ms. */
|
|
423
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
424
|
+
/** The accepted `relationLoadStrategy` values. */
|
|
425
|
+
const RELATION_LOAD_STRATEGIES = Object.freeze([
|
|
426
|
+
'join',
|
|
427
|
+
'batched',
|
|
428
|
+
'auto',
|
|
429
|
+
'flatten',
|
|
430
|
+
]);
|
|
431
|
+
/** A per-query `timeout`: milliseconds up to {@link MAX_TIMEOUT_MS}, or 0 for none (the same rule as `$transaction`). */
|
|
432
|
+
function isValidTimeout(value) {
|
|
433
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= MAX_TIMEOUT_MS;
|
|
434
|
+
}
|
|
435
|
+
/** A short, value-safe rendering of a rejected option value. */
|
|
436
|
+
function describeOptionValue(value) {
|
|
437
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
438
|
+
return String(value);
|
|
439
|
+
if (typeof value === 'string')
|
|
440
|
+
return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}...` : value);
|
|
441
|
+
return value === null ? 'null' : `a value of type ${typeof value}`;
|
|
442
|
+
}
|
|
443
|
+
function relationLoadStrategyMessage(value) {
|
|
444
|
+
return (`\`relationLoadStrategy\` must be one of ${RELATION_LOAD_STRATEGIES.map((v) => `'${v}'`).join(', ')} ` +
|
|
445
|
+
`(received ${describeOptionValue(value)}).`);
|
|
446
|
+
}
|
|
447
|
+
/** A value the findUnique simple path may bind as it is: a primitive, Date or Buffer. */
|
|
448
|
+
function isFastPathScalar(value) {
|
|
449
|
+
switch (typeof value) {
|
|
450
|
+
case 'string':
|
|
451
|
+
case 'number':
|
|
452
|
+
case 'bigint':
|
|
453
|
+
case 'boolean':
|
|
454
|
+
return true;
|
|
455
|
+
case 'object':
|
|
456
|
+
return value instanceof Date || (value !== null && ArrayBuffer.isView(value));
|
|
457
|
+
default:
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/** Resolves a race when a deadline passes first. */
|
|
462
|
+
const TIMED_OUT = Symbol('timedOut');
|
|
463
|
+
/**
|
|
464
|
+
* How long a statement that outlived its `timeout` gets, after the cancel
|
|
465
|
+
* request is sent, to answer before its connection is destroyed instead of
|
|
466
|
+
* reused. A cancel normally lands within a round trip or two.
|
|
467
|
+
*/
|
|
468
|
+
const CANCEL_GRACE_MS = 2000;
|
|
469
|
+
/** Whether `err` is PostgreSQL's query_canceled (57014), as the driver raised it or as Turbine wrapped it. */
|
|
470
|
+
function isQueryCanceled(err) {
|
|
471
|
+
const e = err;
|
|
472
|
+
return e?.code === '57014' || e?.cause?.code === '57014';
|
|
473
|
+
}
|
|
474
|
+
/** The {@link DeferredQuery.rowLock} marker for a locking read, or nothing. */
|
|
475
|
+
function rowLockMarker(rowLock) {
|
|
476
|
+
return rowLock ? { rowLock: true } : {};
|
|
477
|
+
}
|
|
413
478
|
/** The two accepted {@link JsonEncoding} values, frozen so the check is total. */
|
|
414
479
|
const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
|
|
415
480
|
/**
|
|
416
481
|
* The relation JSON encoding a client on `dialect` gets when it names none.
|
|
417
482
|
*
|
|
418
|
-
* `'positional'` (`json_build_array`)
|
|
419
|
-
* (`json_build_object`) everywhere else. The positional form drops
|
|
420
|
-
* key names from every relation row: measured on a 50-parent /
|
|
421
|
-
* against local PostgreSQL 17, server time 0.685 ms → 0.350 ms
|
|
422
|
-
* 100 KB on the wire, returning byte-identical parsed rows.
|
|
423
|
-
*
|
|
424
|
-
* ## Why the test is `dialect.name`, not "does the dialect have buildJsonArray"
|
|
425
|
-
*
|
|
426
|
-
* Because a presence test does not distinguish engines HERE. `buildJsonArray`
|
|
427
|
-
* is declared on `postgresDialect`, and sqlite.ts / mysql.ts / mssql.ts /
|
|
428
|
-
* powdb.ts each build their dialect by SPREADING it, so every engine inherits
|
|
429
|
-
* the hook and an "is it absent" fallback never fires. That is the documented
|
|
430
|
-
* inheritance trap (see `buildPartitionLimit` in {@link
|
|
431
|
-
* QueryInterface.batchedContext} and the `distinct` gate in
|
|
432
|
-
* {@link QueryInterface.buildFindMany}), and it is exactly the shape that
|
|
433
|
-
* silently turned the partition-limit window on for SQLite.
|
|
483
|
+
* `'positional'` (`json_build_array`) where the engine has it (PostgreSQL),
|
|
484
|
+
* `'object'` (`json_build_object`) everywhere else. The positional form drops
|
|
485
|
+
* the repeated key names from every relation row: measured on a 50-parent /
|
|
486
|
+
* ~10-child read against local PostgreSQL 17, server time 0.685 ms → 0.350 ms
|
|
487
|
+
* and 152 KB → 100 KB on the wire, returning byte-identical parsed rows.
|
|
434
488
|
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* cannot.
|
|
489
|
+
* Read from `capabilities.positionalJson`, never from whether the dialect has
|
|
490
|
+
* `buildJsonArray`: that hook is on `postgresDialect` and every engine inherits
|
|
491
|
+
* it by spreading, so a presence test would hand every engine the positional
|
|
492
|
+
* default. `buildSelectWithRelations` (relations.ts) refuses `'positional'` on
|
|
493
|
+
* the SAME flag, so the default can never select an encoding the builder then
|
|
494
|
+
* refuses: one authority, not two.
|
|
442
495
|
*
|
|
443
496
|
* A wire-compatible engine that reaches this on `postgresDialect` itself
|
|
444
497
|
* (CockroachDB, YugabyteDB, AlloyDB, Timescale, all of which are ADAPTERS over
|
|
@@ -446,9 +499,8 @@ const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
|
|
|
446
499
|
* which is correct: they speak `json_build_array`.
|
|
447
500
|
*/
|
|
448
501
|
function defaultJsonEncoding(dialect) {
|
|
449
|
-
return dialect.
|
|
502
|
+
return dialect.capabilities.positionalJson ? 'positional' : 'object';
|
|
450
503
|
}
|
|
451
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no relations known", intentional for untyped table access
|
|
452
504
|
export class QueryInterface {
|
|
453
505
|
pool;
|
|
454
506
|
table;
|
|
@@ -469,6 +521,13 @@ export class QueryInterface {
|
|
|
469
521
|
* inline, not through the top-level cache).
|
|
470
522
|
*/
|
|
471
523
|
lastCacheHit = false;
|
|
524
|
+
/**
|
|
525
|
+
* The params the build closure pushed on the most recent MISS, held until
|
|
526
|
+
* {@link crossCheckCache} compares them with the collected params and clears
|
|
527
|
+
* them. `undefined` after a hit. Same single-flag reasoning as
|
|
528
|
+
* {@link lastCacheHit}.
|
|
529
|
+
*/
|
|
530
|
+
lastMissParams;
|
|
472
531
|
/**
|
|
473
532
|
* Set while a build closure runs when the statement's WHERE (or HAVING)
|
|
474
533
|
* carries a caller-written `AND`/`OR` combinator array, i.e. when its SQL text
|
|
@@ -480,6 +539,21 @@ export class QueryInterface {
|
|
|
480
539
|
*/
|
|
481
540
|
variableArityShape = false;
|
|
482
541
|
middlewares;
|
|
542
|
+
/** The client's resolved `diagnostics` (see diagnostics.ts), fixed for this instance's life. */
|
|
543
|
+
diagnostics;
|
|
544
|
+
/** `streamIdleTimeoutMs`: how long a cursor stream may wait for its consumer. `0` disables. */
|
|
545
|
+
streamIdleTimeoutMs;
|
|
546
|
+
/** `sqlComment`, resolved; `undefined` when statements carry no comment (one falsy check per send). */
|
|
547
|
+
sqlCommenter;
|
|
548
|
+
/** Where this instance's log lines go: the client's `logger`, else the console logger. */
|
|
549
|
+
logger;
|
|
550
|
+
/**
|
|
551
|
+
* {@link logger} when `diagnostics.advice` is on, else `undefined`. Every
|
|
552
|
+
* per-query advice site starts with `const log = this.advice; if (!log)
|
|
553
|
+
* return;`, so with advice off a site costs one property read and builds no
|
|
554
|
+
* message.
|
|
555
|
+
*/
|
|
556
|
+
advice;
|
|
483
557
|
defaultLimit;
|
|
484
558
|
warnOnUnlimited;
|
|
485
559
|
scopedConnect;
|
|
@@ -581,7 +655,6 @@ export class QueryInterface {
|
|
|
581
655
|
* the audit's "Set<string>" guidance and leaves room for future
|
|
582
656
|
* cross-table sharing.
|
|
583
657
|
*/
|
|
584
|
-
warnedTables = new Set();
|
|
585
658
|
/** Cache hit/miss counters for diagnostics */
|
|
586
659
|
cacheHits = 0;
|
|
587
660
|
cacheMisses = 0;
|
|
@@ -602,6 +675,10 @@ export class QueryInterface {
|
|
|
602
675
|
* to Date as well (otherwise nested dates leak through as strings).
|
|
603
676
|
*/
|
|
604
677
|
camelDateFieldCache = new Map();
|
|
678
|
+
/** Per-table memo of {@link cellKindOf}, keyed by column name and field name. */
|
|
679
|
+
cellKindCache = new Map();
|
|
680
|
+
/** The dialect decode of each {@link CELL_DIALECT} column, by table, then by column and field name. */
|
|
681
|
+
dialectDecodeCache = new Map();
|
|
605
682
|
/**
|
|
606
683
|
* Per-table memo of `Object.entries(meta.relations)`, consumed by the nested
|
|
607
684
|
* row parser (see `getRelationEntries` in relations.ts). Same rationale and
|
|
@@ -675,7 +752,14 @@ export class QueryInterface {
|
|
|
675
752
|
}
|
|
676
753
|
this.tableMeta = meta;
|
|
677
754
|
this.middlewares = middlewares ?? [];
|
|
755
|
+
// Resolved by the client once; a QueryInterface built directly (tests,
|
|
756
|
+
// tooling) resolves here, at construction, never per call.
|
|
757
|
+
this.diagnostics = resolveDiagnostics(options?.diagnostics);
|
|
758
|
+
this.logger = options?.logger ?? consoleLogger;
|
|
759
|
+
this.advice = this.diagnostics.advice ? this.logger : undefined;
|
|
678
760
|
this.defaultLimit = options?.defaultLimit;
|
|
761
|
+
this.streamIdleTimeoutMs =
|
|
762
|
+
resolveStreamIdleTimeout(options?.streamIdleTimeoutMs, 'streamIdleTimeoutMs') ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
|
|
679
763
|
// Default to ON: surfacing accidental full-table scans is more valuable
|
|
680
764
|
// than the (small) risk of noisy logs. Callers opt out globally with
|
|
681
765
|
// `warnOnUnlimited: false`, per table with `warnOnUnlimited: { users:
|
|
@@ -712,6 +796,17 @@ export class QueryInterface {
|
|
|
712
796
|
this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
|
|
713
797
|
this.sqlTemplateCache = new LRUCache(sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
|
|
714
798
|
this.dialect = options?.dialect ?? postgresDialect;
|
|
799
|
+
this.sqlCommenter =
|
|
800
|
+
options?._sqlCommenter ??
|
|
801
|
+
resolveSqlComment(options?.sqlComment, this.logger, {
|
|
802
|
+
dialect: this.dialect.name,
|
|
803
|
+
preparesEveryStatement: this.dialect.capabilities.preparesEveryStatement,
|
|
804
|
+
buildsSql: true,
|
|
805
|
+
});
|
|
806
|
+
if (options?.relationLoadStrategy !== undefined &&
|
|
807
|
+
!RELATION_LOAD_STRATEGIES.includes(options.relationLoadStrategy)) {
|
|
808
|
+
throw new ValidationError(relationLoadStrategyMessage(options.relationLoadStrategy));
|
|
809
|
+
}
|
|
715
810
|
this.relationLoadStrategy = options?.relationLoadStrategy ?? 'auto';
|
|
716
811
|
this.stableRelationOrder = options?.stableRelationOrder === true;
|
|
717
812
|
this.implicitPkOrdering = options?.implicitPkOrdering === true;
|
|
@@ -778,6 +873,8 @@ export class QueryInterface {
|
|
|
778
873
|
// per `build*` call, so it is exposed as a LIVE getter (not a copied value).
|
|
779
874
|
const self = this;
|
|
780
875
|
this.ctx = {
|
|
876
|
+
diagnostics: this.diagnostics,
|
|
877
|
+
logger: this.logger,
|
|
781
878
|
dialect: this.dialect,
|
|
782
879
|
table: this.table,
|
|
783
880
|
schema: this.schema,
|
|
@@ -844,7 +941,7 @@ export class QueryInterface {
|
|
|
844
941
|
};
|
|
845
942
|
}
|
|
846
943
|
/**
|
|
847
|
-
*
|
|
944
|
+
* Advice (`diagnostics.advice`), once per table: the columns whose database type is absent from
|
|
848
945
|
* BOTH the column entry and the table-level type maps, the residual case
|
|
849
946
|
* after the two-source resolution above.
|
|
850
947
|
*
|
|
@@ -869,23 +966,24 @@ export class QueryInterface {
|
|
|
869
966
|
* Postgres rejects with `22007 invalid input syntax for type time`.
|
|
870
967
|
* - `timestamptz` and every non-temporal type: unaffected.
|
|
871
968
|
*
|
|
872
|
-
* PostgreSQL only: the
|
|
873
|
-
* `
|
|
874
|
-
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
969
|
+
* PostgreSQL only: the zone-less `timestamp` rewrite is Postgres-gated (see
|
|
970
|
+
* `utcDateTimeRewrite` in where.ts), so on the other engines a missing type
|
|
971
|
+
* costs only the `date` rewrite. Silent when `diagnostics.advice` is off
|
|
972
|
+
* (the production default) like the other advice, and deduped through the
|
|
973
|
+
* shared registry, so a hot table logs one line for the process.
|
|
877
974
|
*
|
|
878
975
|
* Cannot throw on odd metadata: it walks `tableMeta.columns`, the array the
|
|
879
976
|
* constructor loop above has already iterated (and that client.ts validates
|
|
880
977
|
* as an array), never `dateColumns`, which is a `Set` in every first-party
|
|
881
978
|
* metadata path but arrives as a plain object from JSON-round-tripped
|
|
882
|
-
* metadata.
|
|
979
|
+
* metadata. An advisory diagnostic that crashes a shape production would serve
|
|
883
980
|
* is worse than the bug it reports.
|
|
884
981
|
*/
|
|
885
982
|
warnUntypedColumns() {
|
|
886
|
-
|
|
983
|
+
const log = this.advice;
|
|
984
|
+
if (!log)
|
|
887
985
|
return;
|
|
888
|
-
if (this.dialect.
|
|
986
|
+
if (!this.dialect.capabilities.utcTimestampRewrite)
|
|
889
987
|
return;
|
|
890
988
|
const untyped = [];
|
|
891
989
|
for (const col of this.tableMeta.columns) {
|
|
@@ -901,14 +999,14 @@ export class QueryInterface {
|
|
|
901
999
|
const MAX_NAMED = 12;
|
|
902
1000
|
const named = untyped.slice(0, MAX_NAMED).join(', ');
|
|
903
1001
|
const rest = untyped.length > MAX_NAMED ? ` (+${untyped.length - MAX_NAMED} more)` : '';
|
|
904
|
-
|
|
1002
|
+
logTo(log, 'warn', `table "${this.table}": no database type in metadata for column(s) ${named}${rest} (neither the ` +
|
|
905
1003
|
"column entry's `dialectType`/`pgType` nor the table-level `dialectTypes`/`pgTypes` map). Turbine cannot " +
|
|
906
1004
|
'tell which of them are zone-less, so it skips the UTC bind rewrite and a `Date` goes to the driver ' +
|
|
907
1005
|
"as-is: right for `timestamptz`, but a zone-less `date`/`timestamp` then stores the PROCESS's local " +
|
|
908
1006
|
'calendar fields rather than UTC (silently, since the read path shifts back by the same offset), and a ' +
|
|
909
1007
|
'`time`/`timetz` column rejects the value outright (`22007 invalid input syntax for type time`). ' +
|
|
910
1008
|
'Columns whose type IS resolved, every `timestamptz` among them, are unaffected. Fix: regenerate with ' +
|
|
911
|
-
'`npx turbine generate`, or set the column types in `defineSchema`.');
|
|
1009
|
+
'`npx turbine generate`, or set the column types in `defineSchema`.', { code: 'untyped-columns', table: this.table, columns: untyped });
|
|
912
1010
|
}
|
|
913
1011
|
/** Quote an identifier through the active SQL dialect. */
|
|
914
1012
|
q(name) {
|
|
@@ -1075,24 +1173,63 @@ export class QueryInterface {
|
|
|
1075
1173
|
* query/relation-names.ts.
|
|
1076
1174
|
*/
|
|
1077
1175
|
withDeclaredRelationNames(args) {
|
|
1078
|
-
|
|
1176
|
+
const withClause = args?.with;
|
|
1177
|
+
if (!withClause)
|
|
1178
|
+
return args;
|
|
1179
|
+
const normalized = normalizeWithClause(this.schema, this.table, withClause);
|
|
1180
|
+
return normalized === withClause ? args : { ...args, with: normalized };
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* `args` with `take` / `skip` folded into `limit` / `offset` at every `with`
|
|
1184
|
+
* level (see `normalizeWithPagination` in query/relation-names.ts). Runs after
|
|
1185
|
+
* the relation-name pass, so it resolves the declared spelling.
|
|
1186
|
+
*/
|
|
1187
|
+
withRelationPagination(args) {
|
|
1188
|
+
const withClause = args?.with;
|
|
1189
|
+
if (!withClause)
|
|
1079
1190
|
return args;
|
|
1080
|
-
const normalized =
|
|
1081
|
-
return normalized ===
|
|
1191
|
+
const normalized = normalizeWithPagination(this.schema, this.table, withClause);
|
|
1192
|
+
return normalized === withClause ? args : { ...args, with: normalized };
|
|
1082
1193
|
}
|
|
1083
1194
|
/**
|
|
1084
|
-
* Caller args in canonical form:
|
|
1085
|
-
* pagination aliases folded into
|
|
1195
|
+
* Caller args in canonical form: `undefined` pruned (query/normalize-args.ts),
|
|
1196
|
+
* declared relation spellings, and the Prisma pagination aliases folded into
|
|
1197
|
+
* `limit` / `offset`, with `take` / `skip` folded inside every `with` level too.
|
|
1086
1198
|
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1199
|
+
* Called at the seam of EVERY public operation and its `build*` twin, so the
|
|
1200
|
+
* guards, the fingerprint, the build and the collect all read one shape. Each
|
|
1201
|
+
* step returns its input by reference when there was nothing to change, so a
|
|
1202
|
+
* second call on already-normalized args is free and the common path
|
|
1203
|
+
* allocates nothing.
|
|
1204
|
+
*
|
|
1205
|
+
* `refuseEmptiedWhere` names the single-row read (`findFirst`,
|
|
1206
|
+
* `findFirstOrThrow`) that refuses a `where` whose every key was `undefined`.
|
|
1207
|
+
*/
|
|
1208
|
+
normalizeArgs(args, refuseEmptiedWhere) {
|
|
1209
|
+
const pruned = pruneUndefinedArgs(this.schema, this.table, args);
|
|
1210
|
+
if (pruned !== args) {
|
|
1211
|
+
if (refuseEmptiedWhere)
|
|
1212
|
+
assertWhereNotEmptiedByPruning(this.table, refuseEmptiedWhere, args, pruned);
|
|
1213
|
+
assertCursorNotEmptied(this.table, args, pruned);
|
|
1214
|
+
}
|
|
1215
|
+
if (pruned)
|
|
1216
|
+
this.assertQueryOptions(pruned);
|
|
1217
|
+
return normalizePagination(this.withRelationPagination(this.withDeclaredRelationNames(pruned)));
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Validate the per-query options whose bad values used to be ignored or
|
|
1221
|
+
* misread: `timeout` (Node clamps a non-positive, non-finite or oversized
|
|
1222
|
+
* delay to 1 ms) and `relationLoadStrategy`.
|
|
1093
1223
|
*/
|
|
1094
|
-
|
|
1095
|
-
|
|
1224
|
+
assertQueryOptions(args) {
|
|
1225
|
+
const { timeout, relationLoadStrategy } = args;
|
|
1226
|
+
if (timeout !== undefined && !isValidTimeout(timeout)) {
|
|
1227
|
+
throw new ValidationError(`\`timeout\` on ${this.table} must be a positive number of milliseconds up to ${MAX_TIMEOUT_MS}, or 0 for ` +
|
|
1228
|
+
`no timeout (received ${describeOptionValue(timeout)}). Leave it out for no timeout.`);
|
|
1229
|
+
}
|
|
1230
|
+
if (relationLoadStrategy !== undefined && !RELATION_LOAD_STRATEGIES.includes(relationLoadStrategy)) {
|
|
1231
|
+
throw new ValidationError(relationLoadStrategyMessage(relationLoadStrategy));
|
|
1232
|
+
}
|
|
1096
1233
|
}
|
|
1097
1234
|
/**
|
|
1098
1235
|
* Refuse a `findUnique` whose `where` names no unique key. The rule and the
|
|
@@ -1102,6 +1239,30 @@ export class QueryInterface {
|
|
|
1102
1239
|
assertFindUniqueKey(where) {
|
|
1103
1240
|
assertWhereIdentifiesOneRow(this.tableMeta, this.table, where);
|
|
1104
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Refuse a locking read that is not inside a transaction, at the execute
|
|
1244
|
+
* entry of each find method and before any strategy runs. The `build*`
|
|
1245
|
+
* methods accept `lock` (the SQL is right wherever it runs); executing it
|
|
1246
|
+
* in autocommit releases the lock before the caller sees the rows.
|
|
1247
|
+
*
|
|
1248
|
+
* The value is validated first, so an engine with no row locks answers
|
|
1249
|
+
* E017 rather than asking for a transaction it would also refuse.
|
|
1250
|
+
* "Inside a transaction" is `_txScoped`: the accessors a TransactionClient
|
|
1251
|
+
* hands out, at any nesting depth.
|
|
1252
|
+
*/
|
|
1253
|
+
assertLockInTransaction(args, operation) {
|
|
1254
|
+
if (args?.lock === undefined)
|
|
1255
|
+
return;
|
|
1256
|
+
resolveRowLock(args.lock, this.dialect, this.table);
|
|
1257
|
+
if (!this.txScoped)
|
|
1258
|
+
throw new ValidationError(lockOutsideTransactionMessage(this.table, operation));
|
|
1259
|
+
}
|
|
1260
|
+
/** Refuse `lock` on a streaming read (a cursor is not a locking read). */
|
|
1261
|
+
assertNoStreamLock(args, operation) {
|
|
1262
|
+
if (args?.lock !== undefined) {
|
|
1263
|
+
throw new ValidationError(lockOnStreamMessage(this.table, operation));
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1105
1266
|
/**
|
|
1106
1267
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
1107
1268
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
|
@@ -1134,146 +1295,325 @@ export class QueryInterface {
|
|
|
1134
1295
|
const fields = pk.map((c) => this.tableMeta.reverseColumnMap[c] ?? c);
|
|
1135
1296
|
return fields.length === 1 ? { [fields[0]]: 'asc' } : fields.map((f) => ({ [f]: 'asc' }));
|
|
1136
1297
|
}
|
|
1298
|
+
/** Memo for {@link totalOrderKeys}; table metadata is immutable. */
|
|
1299
|
+
totalOrderKeysMemo;
|
|
1137
1300
|
/**
|
|
1138
|
-
* The
|
|
1139
|
-
*
|
|
1140
|
-
* ordering view; the seek itself orders the fields by their `orderBy`
|
|
1141
|
-
* precedence (see {@link cursorSeekEntries}). Empty for a missing cursor or
|
|
1142
|
-
* one whose every value is `undefined` (which emits no seek condition at
|
|
1143
|
-
* all, so it does not paginate).
|
|
1301
|
+
* The unique keys whose columns are all NOT NULL, primary key first: the
|
|
1302
|
+
* column sets that make an order total (no two rows can tie on them).
|
|
1144
1303
|
*/
|
|
1145
|
-
|
|
1146
|
-
if (
|
|
1304
|
+
totalOrderKeys() {
|
|
1305
|
+
if (this.totalOrderKeysMemo)
|
|
1306
|
+
return this.totalOrderKeysMemo;
|
|
1307
|
+
const meta = this.tableMeta;
|
|
1308
|
+
const nullable = (column) => !meta.primaryKey.includes(column) && meta.columns.find((c) => c.name === column)?.nullable === true;
|
|
1309
|
+
this.totalOrderKeysMemo = uniqueKeyNames(meta).filter((cols) => cols.every((c) => !nullable(c)));
|
|
1310
|
+
return this.totalOrderKeysMemo;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* THE tiebreaker rule, shared by cursor pages ({@link planCursor}) and the
|
|
1314
|
+
* pages before them ({@link completePageOrder}), so every page of one walk
|
|
1315
|
+
* is cut from the same total order: the columns to append to an order over
|
|
1316
|
+
* `ordered` so that no two rows tie. `[]` when the order already contains a
|
|
1317
|
+
* unique NOT NULL key; the first such key's missing columns otherwise;
|
|
1318
|
+
* `undefined` when the table has none.
|
|
1319
|
+
*/
|
|
1320
|
+
tiebreakColumns(ordered) {
|
|
1321
|
+
const keys = this.totalOrderKeys();
|
|
1322
|
+
if (keys.some((cols) => cols.every((c) => ordered.has(c))))
|
|
1147
1323
|
return [];
|
|
1148
|
-
return
|
|
1149
|
-
.filter(([, v]) => v !== undefined)
|
|
1150
|
-
.map(([k]) => k)
|
|
1151
|
-
.sort();
|
|
1324
|
+
return keys[0]?.filter((c) => !ordered.has(c));
|
|
1152
1325
|
}
|
|
1153
1326
|
/**
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
* order
|
|
1161
|
-
*
|
|
1162
|
-
* blindly on the primary key when the two differ.
|
|
1327
|
+
* A paginated `findMany` (`limit` / `offset`) whose `orderBy` can tie gets
|
|
1328
|
+
* the tiebreaker appended, in the last term's direction, so a page boundary
|
|
1329
|
+
* cannot fall between tied rows in a different order on each page (skipping
|
|
1330
|
+
* or repeating one), and the first page of a cursor walk uses the order its
|
|
1331
|
+
* cursor pages use. Returns `args` itself when the order is already total,
|
|
1332
|
+
* the query is not paginated, or it pages by `cursor` or `distinct`, which
|
|
1333
|
+
* order themselves. A `findFirst` with no `offset` is one row, not a page,
|
|
1334
|
+
* and is left alone.
|
|
1163
1335
|
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
* object's key order is canonicalized away, so `(a asc, b asc)` would be a
|
|
1169
|
-
* guess at which key the caller meant to lead. The caller must state the
|
|
1170
|
-
* order, and the unordered-page warning says so.
|
|
1171
|
-
* - a field that does not resolve to a real column. Column validation belongs
|
|
1172
|
-
* to the normal build path, which raises a precise error; synthesizing an
|
|
1173
|
-
* ORDER BY on it here would only change which error the caller sees.
|
|
1336
|
+
* An order containing a vector distance gets no tiebreaker: a KNN index
|
|
1337
|
+
* serves only `ORDER BY <distance> LIMIT k`, a compound order turns that
|
|
1338
|
+
* into a scan and sort of the whole table, and two equal distances are
|
|
1339
|
+
* rare. A `cursor` still refuses such an order (see {@link planCursor}).
|
|
1174
1340
|
*/
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
return
|
|
1341
|
+
completePageOrder(args) {
|
|
1342
|
+
if (args.orderBy === undefined || (args.limit === undefined && args.offset === undefined))
|
|
1343
|
+
return args;
|
|
1344
|
+
if (hasCursor(args.cursor) || (args.distinct && args.distinct.length > 0))
|
|
1345
|
+
return args;
|
|
1346
|
+
if (args[FIRST_ROW_ONLY])
|
|
1347
|
+
return args;
|
|
1348
|
+
const entries = orderByEntries(args.orderBy).filter(([, d]) => d !== undefined);
|
|
1349
|
+
if (entries.some(([, d]) => isVectorOrderBy(d)))
|
|
1350
|
+
return args;
|
|
1351
|
+
const ordered = new Set();
|
|
1352
|
+
let lastDesc = false;
|
|
1353
|
+
for (const [key, dir] of entries) {
|
|
1354
|
+
lastDesc = false;
|
|
1355
|
+
if (isVectorOrderBy(dir) || isJsonPathOrderBy(dir) || this.isRelationOrderByValue(dir))
|
|
1356
|
+
continue;
|
|
1357
|
+
const column = resolveColumnName(this.tableMeta, key);
|
|
1358
|
+
if (column === undefined)
|
|
1359
|
+
continue;
|
|
1360
|
+
ordered.add(column);
|
|
1361
|
+
lastDesc = normalizeOrderBy(dir).dir === 'DESC';
|
|
1185
1362
|
}
|
|
1186
|
-
|
|
1363
|
+
const missing = this.tiebreakColumns(ordered);
|
|
1364
|
+
if (!missing || missing.length === 0)
|
|
1365
|
+
return args;
|
|
1366
|
+
const dir = lastDesc ? 'desc' : 'asc';
|
|
1367
|
+
const field = (column) => this.tableMeta.reverseColumnMap[column] ?? column;
|
|
1368
|
+
return {
|
|
1369
|
+
...args,
|
|
1370
|
+
orderBy: [...entries.map(([k, v]) => ({ [k]: v })), ...missing.map((c) => ({ [field(c)]: dir }))],
|
|
1371
|
+
};
|
|
1187
1372
|
}
|
|
1188
1373
|
/**
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
* cache-hit param collect all consume, so the three cannot disagree about
|
|
1192
|
-
* which value binds to which `$n`.
|
|
1374
|
+
* Plan a `cursor` page: THE single authority the cache key, the SQL build and
|
|
1375
|
+
* the cache-hit param collect all read, so the three cannot disagree.
|
|
1193
1376
|
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
* always had.
|
|
1377
|
+
* The page is ordered by the EFFECTIVE order: the caller's `orderBy` plus a
|
|
1378
|
+
* tiebreaker (the primary key, else a unique key on NOT NULL columns) when
|
|
1379
|
+
* the orderBy does not already end in one, in the last term's direction. A
|
|
1380
|
+
* cursor query is always ordered, because a seek over rows the engine may
|
|
1381
|
+
* return in any order skips and repeats rows. `args.orderBy` is rewritten to
|
|
1382
|
+
* that order (by reference when nothing changes).
|
|
1201
1383
|
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1384
|
+
* Two ways to place the page, picked from what the cursor carries:
|
|
1385
|
+
* - VALUES: the cursor has a value for every column of the effective order,
|
|
1386
|
+
* so the seek compares against them directly (`col > $1`, expanded to a
|
|
1387
|
+
* keyset across several columns). Extra cursor fields are ignored.
|
|
1388
|
+
* - ROW: otherwise the cursor must identify one row by a unique key (the
|
|
1389
|
+
* findUnique rule), and the seek reads that row's ordered values in
|
|
1390
|
+
* scalar subqueries, guarded by `EXISTS` so a deleted cursor row gives an
|
|
1391
|
+
* empty page rather than a wrong one.
|
|
1392
|
+
* Anything else is refused (E003) naming the missing fields.
|
|
1208
1393
|
*
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1394
|
+
* NULL sort keys are placed where the engine sorts them (PostgreSQL: last
|
|
1395
|
+
* ascending, first descending; SQLite / MySQL / SQL Server: NULL is smallest)
|
|
1396
|
+
* or where an explicit `nulls` puts them, and each row is visited exactly
|
|
1397
|
+
* once. On PostgreSQL a nullable ordered column has its placement written
|
|
1398
|
+
* out, so a Postgres-compatible engine with a different default reads the
|
|
1399
|
+
* same order the seek assumes.
|
|
1211
1400
|
*/
|
|
1212
|
-
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1401
|
+
planCursor(args) {
|
|
1402
|
+
const raw = args.cursor;
|
|
1403
|
+
if (!hasCursor(raw))
|
|
1404
|
+
return { args, plan: undefined };
|
|
1405
|
+
const meta = this.tableMeta;
|
|
1406
|
+
const field = (column) => meta.reverseColumnMap[column] ?? column;
|
|
1407
|
+
const nullableColumn = (column) => !meta.primaryKey.includes(column) && meta.columns.find((c) => c.name === column)?.nullable === true;
|
|
1408
|
+
// The cursor's own values, by resolved column (either spelling, or a
|
|
1409
|
+
// compound-unique selector, is accepted). Each is a row's value, checked
|
|
1410
|
+
// by the rule every engine shares (cursorFieldValue).
|
|
1411
|
+
const flat = expandCompoundUniqueWhere(meta, raw);
|
|
1412
|
+
const cursorValues = new Map();
|
|
1413
|
+
for (const [key, written] of sortedEntries(flat)) {
|
|
1414
|
+
if (written === undefined)
|
|
1218
1415
|
continue;
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
.
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
return {
|
|
1230
|
-
|
|
1231
|
-
//
|
|
1232
|
-
|
|
1233
|
-
|
|
1416
|
+
const column = this.toColumn(key);
|
|
1417
|
+
const value = cursorFieldValue(key, written, {
|
|
1418
|
+
column,
|
|
1419
|
+
table: this.table,
|
|
1420
|
+
kind: operandColumnKind(this.getColumnPgType(column)),
|
|
1421
|
+
nullable: nullableColumn(column),
|
|
1422
|
+
});
|
|
1423
|
+
cursorValues.set(column, value);
|
|
1424
|
+
}
|
|
1425
|
+
if (cursorValues.size === 0)
|
|
1426
|
+
return { args, plan: undefined };
|
|
1427
|
+
// The caller's order, plain columns only: a relation, JSON-path or vector
|
|
1428
|
+
// ordering has no value a seek can compare against.
|
|
1429
|
+
const terms = [];
|
|
1430
|
+
const orderEntries = orderByEntries(args.orderBy).filter(([, d]) => d !== undefined);
|
|
1431
|
+
for (const [key, dir] of orderEntries) {
|
|
1432
|
+
if (isVectorOrderBy(dir) || isJsonPathOrderBy(dir) || this.isRelationOrderByValue(dir)) {
|
|
1433
|
+
throw new ValidationError(`findMany on "${this.table}" refused: \`cursor\` cannot page past the orderBy entry "${key}", which ` +
|
|
1434
|
+
'orders by a relation, a JSON path or a vector distance rather than a column. Order the cursor ' +
|
|
1435
|
+
'query by columns only, or page it with `offset`.');
|
|
1436
|
+
}
|
|
1437
|
+
const column = this.toColumn(key);
|
|
1438
|
+
if (terms.some((t) => t.column === column))
|
|
1439
|
+
continue;
|
|
1440
|
+
const { dir: d, nulls } = normalizeOrderBy(dir);
|
|
1441
|
+
terms.push({ column, key, desc: d === 'DESC', nulls, nullable: nullableColumn(column) });
|
|
1442
|
+
}
|
|
1443
|
+
// Tiebreaker, the same one a first page gets (see tiebreakColumns).
|
|
1444
|
+
const tiebreak = this.tiebreakColumns(new Set(terms.map((t) => t.column)));
|
|
1445
|
+
if (!tiebreak) {
|
|
1446
|
+
throw new ValidationError(`findMany on "${this.table}" refused: a \`cursor\` page needs an order with no ties, and "${this.table}" ` +
|
|
1447
|
+
'has no primary key or unique key on NOT NULL columns to break them. Add a primary key, or page ' +
|
|
1448
|
+
'this table with `offset`.');
|
|
1449
|
+
}
|
|
1450
|
+
const desc = terms.length > 0 ? terms[terms.length - 1].desc : false;
|
|
1451
|
+
for (const column of tiebreak) {
|
|
1452
|
+
terms.push({ column, key: field(column), desc, nulls: undefined, nullable: false });
|
|
1453
|
+
}
|
|
1454
|
+
const appended = tiebreak.length > 0;
|
|
1455
|
+
// Where NULLs sort, per term. Where the engine sorts them high (PostgreSQL)
|
|
1456
|
+
// a nullable column's placement is written out, so a Postgres-compatible
|
|
1457
|
+
// engine with a different default reads the order the seek assumes.
|
|
1458
|
+
const { nullsSortHigh, nullsOrderingClause } = this.dialect.capabilities;
|
|
1459
|
+
const pinNulls = nullsSortHigh && nullsOrderingClause;
|
|
1460
|
+
let pinned = false;
|
|
1461
|
+
for (const t of terms) {
|
|
1462
|
+
if (t.nulls === undefined && t.nullable && pinNulls) {
|
|
1463
|
+
t.nulls = t.desc ? 'first' : 'last';
|
|
1464
|
+
t.pin = true;
|
|
1465
|
+
pinned = true;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
const nullsFirst = (t) => t.nulls !== undefined ? t.nulls === 'first' : nullsSortHigh ? t.desc : !t.desc;
|
|
1469
|
+
let nextArgs = args;
|
|
1470
|
+
if (appended || pinned) {
|
|
1471
|
+
const orderBy = terms.map((t) => {
|
|
1472
|
+
const dir = t.desc ? 'desc' : 'asc';
|
|
1473
|
+
const original = orderEntries.find(([k]) => k === t.key)?.[1];
|
|
1474
|
+
return { [t.key]: t.pin ? { sort: dir, nulls: t.nulls } : (original ?? dir) };
|
|
1475
|
+
});
|
|
1476
|
+
nextArgs = { ...args, orderBy };
|
|
1477
|
+
}
|
|
1478
|
+
const inclusive = isCursorInclusive(args);
|
|
1479
|
+
const coerce = (column, value) => whereMod.coerceWhereOperand(this.ctx, meta, column, value);
|
|
1480
|
+
const keysetTerms = terms.map((t) => ({
|
|
1481
|
+
column: t.column,
|
|
1482
|
+
desc: t.desc,
|
|
1483
|
+
nullsFirst: nullsFirst(t),
|
|
1484
|
+
nullable: t.nullable,
|
|
1485
|
+
}));
|
|
1486
|
+
// An inclusive cursor (prisma-compat) names a ROW, as Prisma's does, so it
|
|
1487
|
+
// always reads that row: a deleted cursor row gives an empty page, never a
|
|
1488
|
+
// page placed where the row used to be.
|
|
1489
|
+
const namesRow = inclusive && whereIdentifiesOneRow(meta, flat);
|
|
1490
|
+
if (!namesRow && terms.every((t) => cursorValues.has(t.column))) {
|
|
1491
|
+
const values = terms.map((t) => {
|
|
1492
|
+
const v = cursorValues.get(t.column);
|
|
1493
|
+
return v === null ? null : coerce(t.column, v);
|
|
1494
|
+
});
|
|
1495
|
+
const fp = `v:${keysetTerms.map((t, i) => `${t.column}:${t.desc ? 'd' : 'a'}${t.nullable ? (t.nullsFirst ? 'f' : 'l') : ''}${values[i] === null ? '0' : ''}`).join(',')}` +
|
|
1496
|
+
(inclusive ? ':i' : '');
|
|
1497
|
+
return {
|
|
1498
|
+
args: nextArgs,
|
|
1499
|
+
plan: { mode: 'values', terms: keysetTerms, values, lookup: [], inclusive, fingerprint: fp },
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
if (!whereIdentifiesOneRow(meta, flat)) {
|
|
1503
|
+
const missing = terms.filter((t) => !cursorValues.has(t.column)).map((t) => `"${field(t.column)}"`);
|
|
1504
|
+
const order = terms.map((t) => `${field(t.column)} ${t.desc ? 'desc' : 'asc'}`).join(', ');
|
|
1505
|
+
const keys = uniqueKeyNames(meta).map((cols) => cols.length === 1 ? `{ ${field(cols[0])} }` : `{ ${cols.map(field).join(', ')} }`);
|
|
1506
|
+
throw new ValidationError(`findMany on "${this.table}" refused: the \`cursor\` has no value for ${missing.join(', ')}, which the ` +
|
|
1507
|
+
`page order (${order}) needs, and it does not name one row by a unique key. Give the cursor a value ` +
|
|
1508
|
+
`for every ordered field, or name the cursor row by ${keys.length > 0 ? keys.join(' or ') : 'a unique key'}.`);
|
|
1509
|
+
}
|
|
1510
|
+
const lookup = [...cursorValues].map(([column, v]) => ({ column, value: v === null ? null : coerce(column, v) }));
|
|
1511
|
+
const fp = `r:${keysetTerms.map((t) => `${t.column}:${t.desc ? 'd' : 'a'}${t.nullable ? (t.nullsFirst ? 'f' : 'l') : ''}`).join(',')}` +
|
|
1512
|
+
`|k=${lookup.map((l) => `${l.column}${l.value === null ? '0' : ''}`).join(',')}` +
|
|
1513
|
+
(inclusive ? ':i' : '');
|
|
1514
|
+
return {
|
|
1515
|
+
args: nextArgs,
|
|
1516
|
+
plan: { mode: 'row', terms: keysetTerms, values: [], lookup, inclusive, fingerprint: fp },
|
|
1517
|
+
};
|
|
1234
1518
|
}
|
|
1235
1519
|
/**
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
* cursor ever emitted). Two or more expand to
|
|
1240
|
-
* `(c1 > $1 OR (c1 = $1 AND c2 > $2) OR (c1 = $1 AND c2 = $2 AND c3 > $3))`,
|
|
1241
|
-
* with `<` for a `desc` field: the row-value form `(c1, c2) > ($1, $2)` is
|
|
1242
|
-
* shorter, but it cannot express a MIXED direction at all and SQL Server has
|
|
1243
|
-
* no row-value comparison, so the expanded form is the one path every engine
|
|
1244
|
-
* and every direction set share. Each value is bound ONCE and its `$n` is
|
|
1245
|
-
* referenced by every branch that needs it (legal on every supported
|
|
1246
|
-
* placeholder syntax), which is what keeps the param count equal to the field
|
|
1247
|
-
* count on both the build and the collect side.
|
|
1520
|
+
* Render a {@link CursorPlan}'s seek predicate, pushing its params. Run by
|
|
1521
|
+
* the build AND by the cache-hit collect (which discards the SQL), so the two
|
|
1522
|
+
* cannot bind different params.
|
|
1248
1523
|
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1524
|
+
* Each value is bound once and referenced by every branch that needs it. One
|
|
1525
|
+
* non-null column is the plain `col > $1`; several expand to
|
|
1526
|
+
* `(c1 > $1 OR (c1 = $1 AND c2 > $2) OR ...)`, with `<` for a descending
|
|
1527
|
+
* column (a row-value comparison cannot mix directions, and SQL Server has
|
|
1528
|
+
* none). A nullable column gets NULL-aware equality and "after" terms.
|
|
1252
1529
|
*/
|
|
1253
|
-
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1530
|
+
renderCursorPredicate(plan, params) {
|
|
1531
|
+
const qt = this.q(this.table);
|
|
1532
|
+
const col = (t) => `${qt}.${this.q(t.column)}`;
|
|
1533
|
+
let ref;
|
|
1534
|
+
let guard = '';
|
|
1535
|
+
if (plan.mode === 'values') {
|
|
1536
|
+
const refs = plan.values.map((v) => {
|
|
1537
|
+
if (v === null)
|
|
1538
|
+
return null;
|
|
1539
|
+
params.push(v);
|
|
1540
|
+
return this.p(params.length);
|
|
1541
|
+
});
|
|
1542
|
+
ref = (i) => refs[i] ?? null;
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
const alias = 'cursor_row';
|
|
1546
|
+
const conds = plan.lookup.map((l) => {
|
|
1547
|
+
if (l.value === null)
|
|
1548
|
+
return `${alias}.${this.q(l.column)} IS NULL`;
|
|
1549
|
+
params.push(l.value);
|
|
1550
|
+
return `${alias}.${this.q(l.column)} = ${this.p(params.length)}`;
|
|
1551
|
+
});
|
|
1552
|
+
const gf = whereMod.targetGlobalFilterAlias(this.ctx, this.table, alias, params);
|
|
1553
|
+
if (gf)
|
|
1554
|
+
conds.push(gf);
|
|
1555
|
+
const from = `FROM ${qt} ${alias} WHERE ${conds.join(' AND ')}`;
|
|
1556
|
+
ref = (i) => `(SELECT ${alias}.${this.q(plan.terms[i].column)} ${from})`;
|
|
1557
|
+
guard = `EXISTS (SELECT 1 ${from})`;
|
|
1558
|
+
}
|
|
1559
|
+
// Value mode knows each value; row mode knows only whether the column can
|
|
1560
|
+
// hold NULL, so its comparisons stay NULL-aware for nullable columns.
|
|
1561
|
+
const rowMode = plan.mode === 'row';
|
|
1562
|
+
const eq = (i) => {
|
|
1563
|
+
const t = plan.terms[i];
|
|
1564
|
+
const r = ref(i);
|
|
1565
|
+
if (r === null)
|
|
1566
|
+
return `${col(t)} IS NULL`;
|
|
1567
|
+
if (rowMode && t.nullable)
|
|
1568
|
+
return `(${col(t)} = ${r} OR (${col(t)} IS NULL AND ${r} IS NULL))`;
|
|
1569
|
+
return `${col(t)} = ${r}`;
|
|
1570
|
+
};
|
|
1571
|
+
const after = (i, orEqual) => {
|
|
1572
|
+
const t = plan.terms[i];
|
|
1573
|
+
const r = ref(i);
|
|
1574
|
+
const op = `${t.desc ? '<' : '>'}${orEqual ? '=' : ''}`;
|
|
1575
|
+
if (r === null)
|
|
1576
|
+
return t.nullsFirst ? `${col(t)} IS NOT NULL` : null;
|
|
1577
|
+
if (!t.nullable)
|
|
1578
|
+
return `${col(t)} ${op} ${r}`;
|
|
1579
|
+
if (rowMode) {
|
|
1580
|
+
return t.nullsFirst
|
|
1581
|
+
? `((${r} IS NULL AND ${col(t)} IS NOT NULL) OR ${col(t)} ${op} ${r})`
|
|
1582
|
+
: `(${r} IS NOT NULL AND (${col(t)} ${op} ${r} OR ${col(t)} IS NULL))`;
|
|
1583
|
+
}
|
|
1584
|
+
return t.nullsFirst ? `${col(t)} ${op} ${r}` : `(${col(t)} ${op} ${r} OR ${col(t)} IS NULL)`;
|
|
1585
|
+
};
|
|
1586
|
+
const n = plan.terms.length;
|
|
1587
|
+
const last = n - 1;
|
|
1588
|
+
// An inclusive page folds "equal on every column" into the last branch when
|
|
1589
|
+
// that column is NOT NULL and its value known non-null (`>=`); otherwise it
|
|
1590
|
+
// is its own branch.
|
|
1591
|
+
const foldEqual = plan.inclusive && !plan.terms[last].nullable && ref(last) !== null;
|
|
1592
|
+
const branches = [];
|
|
1593
|
+
for (let i = 0; i < n; i++) {
|
|
1594
|
+
const a = after(i, foldEqual && i === last);
|
|
1595
|
+
if (a === null)
|
|
1596
|
+
continue;
|
|
1258
1597
|
if (i === 0)
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1598
|
+
branches.push(a);
|
|
1599
|
+
else
|
|
1600
|
+
branches.push(`(${[...plan.terms.slice(0, i).map((_, j) => eq(j)), a].join(' AND ')})`);
|
|
1601
|
+
}
|
|
1602
|
+
if (plan.inclusive && !foldEqual) {
|
|
1603
|
+
branches.push(n === 1 ? eq(0) : `(${plan.terms.map((_, j) => eq(j)).join(' AND ')})`);
|
|
1604
|
+
}
|
|
1605
|
+
const keyset = branches.length === 0 ? '1 = 0' : branches.length === 1 ? branches[0] : `(${branches.join(' OR ')})`;
|
|
1606
|
+
return guard ? `(${guard} AND ${keyset})` : keyset;
|
|
1264
1607
|
}
|
|
1265
1608
|
/**
|
|
1266
|
-
* Whether a findMany paginates (`limit` / `take` / `offset`
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
* none.
|
|
1609
|
+
* Whether a findMany paginates (`limit` / `take` / `offset`) but declares no
|
|
1610
|
+
* ordering, which makes the returned page NON-DETERMINISTIC: Postgres is
|
|
1611
|
+
* free to return different rows for the same unordered `LIMIT` once the heap
|
|
1612
|
+
* changes underneath it, so a row can appear on two pages or on none.
|
|
1271
1613
|
*
|
|
1272
|
-
* `cursor` counts
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
* (`[]`, or an object whose every value is `undefined`) counts as absent,
|
|
1276
|
-
* because it emits no ordering.
|
|
1614
|
+
* A `cursor` page never counts: it is always ordered by its effective order
|
|
1615
|
+
* (see {@link planCursor}). An empty orderBy (`[]`, or an object whose every
|
|
1616
|
+
* value is `undefined`) counts as absent, because it emits no ordering.
|
|
1277
1617
|
*
|
|
1278
1618
|
* `distinct` is still excluded: that path re-orders in an outer wrapper around
|
|
1279
1619
|
* a `DISTINCT ON` whose ordering picks the representative row, so an implicit
|
|
@@ -1284,11 +1624,11 @@ export class QueryInterface {
|
|
|
1284
1624
|
return false;
|
|
1285
1625
|
if (args.distinct !== undefined)
|
|
1286
1626
|
return false;
|
|
1627
|
+
if (hasCursor(args.cursor))
|
|
1628
|
+
return false;
|
|
1287
1629
|
if (!isEmptyOrderBy(args.orderBy))
|
|
1288
1630
|
return false;
|
|
1289
|
-
|
|
1290
|
-
return true;
|
|
1291
|
-
return this.cursorFields(args.cursor).length > 0;
|
|
1631
|
+
return args.limit !== undefined || args.offset !== undefined;
|
|
1292
1632
|
}
|
|
1293
1633
|
/**
|
|
1294
1634
|
* Opt-in (`implicitPkOrdering`) primary-key ascending ordering for a paginating
|
|
@@ -1296,47 +1636,42 @@ export class QueryInterface {
|
|
|
1296
1636
|
*
|
|
1297
1637
|
* OFF by default in CORE, deliberately: turning it on would add an `ORDER BY`
|
|
1298
1638
|
* to SQL that existing applications already emit, changing both the rows a
|
|
1299
|
-
* given page returns and the plan the engine picks.
|
|
1300
|
-
* in everything but the type signature, so it waits for a major. The
|
|
1639
|
+
* given page returns and the plan the engine picks. The
|
|
1301
1640
|
* `turbine-orm/prisma-compat` layer defaults it ON instead, because reproducing
|
|
1302
1641
|
* Prisma's semantics is that layer's whole contract.
|
|
1303
1642
|
*
|
|
1304
1643
|
* An explicit `orderBy` always wins, a PK-less table is left alone (nothing
|
|
1305
1644
|
* stable to order by), and a composite PK orders on every column in
|
|
1306
|
-
* declaration order. A `cursor`
|
|
1307
|
-
* (see {@link cursorOrderBy}), and is left alone when that shape is ambiguous.
|
|
1645
|
+
* declaration order. A `cursor` page is ordered by its own plan, not here, and
|
|
1308
1646
|
* `distinct` shapes are skipped (see {@link isUnorderedPage}). With the flag
|
|
1309
|
-
* off this returns `undefined` before touching anything
|
|
1310
|
-
* byte-identical to before.
|
|
1647
|
+
* off this returns `undefined` before touching anything.
|
|
1311
1648
|
*/
|
|
1312
1649
|
implicitPkOrderBy(args) {
|
|
1313
1650
|
if (!this.implicitPkOrdering)
|
|
1314
1651
|
return undefined;
|
|
1315
1652
|
if (!this.isUnorderedPage(args))
|
|
1316
1653
|
return undefined;
|
|
1317
|
-
if (this.cursorFields(args?.cursor).length > 0)
|
|
1318
|
-
return this.cursorOrderBy(args?.cursor);
|
|
1319
1654
|
return this.pkOrderBy();
|
|
1320
1655
|
}
|
|
1321
1656
|
/**
|
|
1322
|
-
*
|
|
1657
|
+
* Advice (`diagnostics.advice`), once per query shape: an unordered paginating findMany returns a
|
|
1323
1658
|
* non-deterministic page (see {@link isUnorderedPage}), and on real data it is
|
|
1324
1659
|
* also usually the slower plan (an unordered `LIMIT` can discard tens of
|
|
1325
1660
|
* thousands of heap rows that an index scan on the key would have skipped).
|
|
1326
1661
|
*
|
|
1327
|
-
* Gated exactly like the other
|
|
1328
|
-
*
|
|
1662
|
+
* Gated exactly like the other advice (silent when `diagnostics.advice` is
|
|
1663
|
+
* off) and consistent with `warnOnUnlimited`: a per-call
|
|
1329
1664
|
* `warnOnUnlimited: false` silences it, `true` forces it past a config-level
|
|
1330
1665
|
* opt-out, and a config/per-table `warnOnUnlimited: false` silences it. Deduped
|
|
1331
1666
|
* process-wide through the shared warn registry, so it can never spam.
|
|
1332
1667
|
*
|
|
1333
1668
|
* Suppressed only when `implicitPkOrdering` will ACTUALLY order this query.
|
|
1334
|
-
* The flag being on is not enough: a PK-less table
|
|
1335
|
-
*
|
|
1336
|
-
* the shapes that still need saying out loud.
|
|
1669
|
+
* The flag being on is not enough: a PK-less table gets no injected
|
|
1670
|
+
* ordering, and that is precisely the shape that still needs saying out loud.
|
|
1337
1671
|
*/
|
|
1338
1672
|
maybeWarnUnorderedPage(args) {
|
|
1339
|
-
|
|
1673
|
+
const log = this.advice;
|
|
1674
|
+
if (!log)
|
|
1340
1675
|
return;
|
|
1341
1676
|
if (this.implicitPkOrdering && this.implicitPkOrderBy(args) !== undefined)
|
|
1342
1677
|
return;
|
|
@@ -1347,30 +1682,18 @@ export class QueryInterface {
|
|
|
1347
1682
|
return;
|
|
1348
1683
|
if (!this.isUnorderedPage(args))
|
|
1349
1684
|
return;
|
|
1350
|
-
const
|
|
1351
|
-
const shape = [
|
|
1352
|
-
cursorFields.length > 0 ? 'cursor' : '',
|
|
1353
|
-
args?.limit !== undefined ? 'limit' : '',
|
|
1354
|
-
args?.offset !== undefined ? 'offset' : '',
|
|
1355
|
-
]
|
|
1685
|
+
const shape = [args?.limit !== undefined ? 'limit' : '', args?.offset !== undefined ? 'offset' : '']
|
|
1356
1686
|
.filter(Boolean)
|
|
1357
1687
|
.join('+');
|
|
1358
1688
|
if (!shouldWarnOnce(WARN_NS.unorderedPage, `${this.table}|${shape}`))
|
|
1359
1689
|
return;
|
|
1360
|
-
const asOrderBy = (fields) => fields.length === 1 ? `{ ${fields[0]}: 'asc' }` : `[${fields.map((f) => `{ ${f}: 'asc' }`).join(', ')}]`;
|
|
1361
1690
|
const pk = (this.tableMeta.primaryKey ?? []).map((c) => this.tableMeta.reverseColumnMap[c] ?? c);
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const suggestion = orderFields.length > 0 ? `Add \`orderBy: ${asOrderBy(orderFields)}\`` : 'Add an `orderBy` on a unique column';
|
|
1366
|
-
const seek = cursorFields.length > 0
|
|
1367
|
-
? 'a `cursor` seek with no orderBy compares against rows the engine is free to return in any order, so ' +
|
|
1368
|
-
'paging with it skips and repeats rows: '
|
|
1369
|
-
: '';
|
|
1370
|
-
console.warn(`[turbine] findMany on "${this.table}" paginates (${shape}) with no orderBy: ${seek}the page is NOT ` +
|
|
1691
|
+
const asOrderBy = pk.length === 1 ? `{ ${pk[0]}: 'asc' }` : `[${pk.map((f) => `{ ${f}: 'asc' }`).join(', ')}]`;
|
|
1692
|
+
const suggestion = pk.length > 0 ? `Add \`orderBy: ${asOrderBy}\`` : 'Add an `orderBy` on a unique column';
|
|
1693
|
+
logTo(log, 'warn', `findMany on "${this.table}" paginates (${shape}) with no orderBy: the page is NOT ` +
|
|
1371
1694
|
'deterministic (the same query can return different rows as the table changes, so a row may appear ' +
|
|
1372
1695
|
`on two pages or on none). ${suggestion}, or set \`implicitPkOrdering: true\` in the client config ` +
|
|
1373
|
-
'to order paginated queries
|
|
1696
|
+
'to order paginated queries by the primary key automatically.', { code: 'unordered-page', table: this.table, operation: 'findMany', paging: shape });
|
|
1374
1697
|
}
|
|
1375
1698
|
// -------------------------------------------------------------------------
|
|
1376
1699
|
// relationLoadStrategy: 'auto', per-relation batched fallback when the
|
|
@@ -1597,7 +1920,7 @@ export class QueryInterface {
|
|
|
1597
1920
|
return split;
|
|
1598
1921
|
}
|
|
1599
1922
|
/**
|
|
1600
|
-
*
|
|
1923
|
+
* Advice, once per relation: `'auto'` engaged the batched fallback.
|
|
1601
1924
|
*
|
|
1602
1925
|
* Both lines follow the same four parts: the CONDITION that tripped the rule,
|
|
1603
1926
|
* the MECHANISM (which plan shape was replaced by which, and what that costs),
|
|
@@ -1606,20 +1929,21 @@ export class QueryInterface {
|
|
|
1606
1929
|
* as a bug, so the mechanism sentence is not optional.
|
|
1607
1930
|
*/
|
|
1608
1931
|
emitAutoNotes(engaged) {
|
|
1609
|
-
|
|
1932
|
+
const log = this.advice;
|
|
1933
|
+
if (!log)
|
|
1610
1934
|
return;
|
|
1611
1935
|
for (const e of engaged) {
|
|
1612
1936
|
if (!shouldWarnOnce(WARN_NS.autoStrategy, `${this.table}.${e.relation}`))
|
|
1613
1937
|
continue;
|
|
1614
1938
|
if (e.reason === 'to-one-cardinality') {
|
|
1615
|
-
|
|
1939
|
+
logTo(log, 'warn', `auto strategy: to-one relation "${e.relation}" on "${this.table}" loads batched ` +
|
|
1616
1940
|
`(the query is unbounded or its limit exceeds ${this.autoToOneThreshold()} rows). On the join plan ` +
|
|
1617
1941
|
'a to-one relation is a correlated subquery the engine re-evaluates once per parent row, a per-row ' +
|
|
1618
1942
|
'cost that stands even on a unique index; batched replaces it with ONE follow-up statement ' +
|
|
1619
1943
|
'(`key = ANY(...)` for the whole page), so it trades that per-row cost for a single extra round ' +
|
|
1620
1944
|
'trip. Bound the query with a smaller `limit`, tune `autoToOneJoinMaxRows` (the break-even is ' +
|
|
1621
1945
|
"round-trip time / per-row cost), or set `relationLoadStrategy: 'join'` to force the " +
|
|
1622
|
-
'single-statement plan.');
|
|
1946
|
+
'single-statement plan.', { code: 'auto-strategy', table: this.table, relation: e.relation, reason: e.reason });
|
|
1623
1947
|
continue;
|
|
1624
1948
|
}
|
|
1625
1949
|
const probe = e.miss
|
|
@@ -1637,9 +1961,9 @@ export class QueryInterface {
|
|
|
1637
1961
|
: ' On the join plan that relation is a correlated subquery re-evaluated once per parent row, so an ' +
|
|
1638
1962
|
`unindexed probe is one full scan of "${child}" per parent row; the batched follow-up scans it ` +
|
|
1639
1963
|
'once for the whole page.';
|
|
1640
|
-
|
|
1964
|
+
logTo(log, 'warn', `auto strategy: relation "${e.relation}" on "${this.table}" loads batched (${probe}).${why} ` +
|
|
1641
1965
|
"Create the covering index (or set `relationLoadStrategy: 'join'` to force the single-statement " +
|
|
1642
|
-
'plan); run `npx turbine doctor` for the exact CREATE INDEX SQL.');
|
|
1966
|
+
'plan); run `npx turbine doctor` for the exact CREATE INDEX SQL.', { code: 'auto-strategy', table: this.table, relation: e.relation, reason: e.reason });
|
|
1643
1967
|
}
|
|
1644
1968
|
}
|
|
1645
1969
|
/**
|
|
@@ -1730,16 +2054,15 @@ export class QueryInterface {
|
|
|
1730
2054
|
// a closure rather than passing `this.dialect` so the loader keeps
|
|
1731
2055
|
// needing nothing else from the dialect.
|
|
1732
2056
|
//
|
|
1733
|
-
// The
|
|
1734
|
-
//
|
|
1735
|
-
//
|
|
1736
|
-
//
|
|
1737
|
-
//
|
|
1738
|
-
//
|
|
1739
|
-
//
|
|
1740
|
-
//
|
|
1741
|
-
|
|
1742
|
-
buildPartitionLimit: this.dialect.name === 'postgresql' && this.dialect.buildPartitionLimit
|
|
2057
|
+
// The capability is NOT redundant with the hook's presence: every engine
|
|
2058
|
+
// dialect is built by SPREADING `postgresDialect`, so the hook is
|
|
2059
|
+
// INHERITED by sqlite / mysql / mssql, whose "absent" fallback would then
|
|
2060
|
+
// never be taken. The wrapper is not portable as written either (T-SQL
|
|
2061
|
+
// rejects an ORDER BY inside a derived table without TOP/OFFSET, and the
|
|
2062
|
+
// compiled child query carries one), so the pushdown stays with the
|
|
2063
|
+
// dialect that owns the implementation until another engine ships its own
|
|
2064
|
+
// and asserts it.
|
|
2065
|
+
buildPartitionLimit: this.dialect.capabilities.partitionLimitPushdown && this.dialect.buildPartitionLimit
|
|
1743
2066
|
? (input) => this.dialect.buildPartitionLimit(input)
|
|
1744
2067
|
: undefined,
|
|
1745
2068
|
skipGlobalFilters: skip,
|
|
@@ -1878,15 +2201,14 @@ export class QueryInterface {
|
|
|
1878
2201
|
*
|
|
1879
2202
|
* When `sqlCache` is disabled, always calls `build()` without caching.
|
|
1880
2203
|
*
|
|
1881
|
-
* `build` receives a fresh `$N` param scratch array.
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1886
|
-
* verify the collect path stayed in lockstep with the build path.
|
|
2204
|
+
* `build` receives a fresh `$N` param scratch array. The executed params
|
|
2205
|
+
* always come from each call site's dedicated collect path; on a miss the
|
|
2206
|
+
* scratch params are kept in {@link lastMissParams} so `crossCheckCache` can
|
|
2207
|
+
* compare the two. On a HIT, `build` is skipped here and re-run by
|
|
2208
|
+
* {@link crossCheckCache} only when `diagnostics.cacheCheck` asks for it.
|
|
1887
2209
|
*
|
|
1888
|
-
* Sets {@link lastCacheHit} so the caller's `crossCheckCache` knows
|
|
1889
|
-
*
|
|
2210
|
+
* Sets {@link lastCacheHit} so the caller's `crossCheckCache` knows which
|
|
2211
|
+
* comparison applies.
|
|
1890
2212
|
*/
|
|
1891
2213
|
acquireSql(cacheKey, build) {
|
|
1892
2214
|
if (!this.sqlCacheEnabled) {
|
|
@@ -1897,6 +2219,7 @@ export class QueryInterface {
|
|
|
1897
2219
|
if (cached) {
|
|
1898
2220
|
this.cacheHits++;
|
|
1899
2221
|
this.lastCacheHit = true;
|
|
2222
|
+
this.lastMissParams = undefined;
|
|
1900
2223
|
return cached;
|
|
1901
2224
|
}
|
|
1902
2225
|
this.lastCacheHit = false;
|
|
@@ -1926,40 +2249,68 @@ export class QueryInterface {
|
|
|
1926
2249
|
*/
|
|
1927
2250
|
buildCacheEntry(build) {
|
|
1928
2251
|
this.variableArityShape = false;
|
|
1929
|
-
|
|
2252
|
+
this.lastMissParams = undefined;
|
|
2253
|
+
const built = [];
|
|
2254
|
+
const sql = build(built);
|
|
1930
2255
|
const variableArity = this.variableArityShape;
|
|
1931
2256
|
this.variableArityShape = false;
|
|
1932
2257
|
this.cacheMisses++;
|
|
1933
|
-
|
|
2258
|
+
this.lastMissParams = built;
|
|
2259
|
+
// With `sqlComment` putting a comment on every statement, the template's own
|
|
2260
|
+
// name is never sent (the execute seam sends a `c_` name derived from it),
|
|
2261
|
+
// so it is not claimed from the budget: only names that reach the wire do.
|
|
2262
|
+
const name = variableArity
|
|
2263
|
+
? ''
|
|
2264
|
+
: this.sqlCommenter?.alwaysComments
|
|
2265
|
+
? sqlToPreparedName(sql)
|
|
2266
|
+
: boundedPreparedName(sql);
|
|
2267
|
+
return { sql, name, paramCount: built.length };
|
|
1934
2268
|
}
|
|
1935
2269
|
/**
|
|
1936
|
-
*
|
|
2270
|
+
* The SQL-cache lockstep checks (see the comment above `CACHE_MISMATCH_WARN_NS`).
|
|
1937
2271
|
*
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
2272
|
+
* On a MISS: the collected params must equal the params the build pushed.
|
|
2273
|
+
* On a HIT: the collected param count must equal the entry's, always; then,
|
|
2274
|
+
* when `diagnostics.cacheCheck` selects this hit, the SQL and params are
|
|
2275
|
+
* rebuilt fresh through the same closure and compared:
|
|
1941
2276
|
* (a) the cached SQL string byte-for-byte against the fresh SQL, and
|
|
1942
|
-
* (b) the params the
|
|
1943
|
-
*
|
|
2277
|
+
* (b) the collected params against the fresh build-path params (length
|
|
2278
|
+
* and element-wise strict deep-equal).
|
|
1944
2279
|
*
|
|
1945
2280
|
* A mismatch means the fingerprint / build / collect paths have drifted out
|
|
1946
2281
|
* of lockstep (the exact class of bug that has silently corrupted results
|
|
1947
|
-
* before), so it throws
|
|
1948
|
-
* fingerprint, the operation, and both SQL strings (truncated)
|
|
1949
|
-
*
|
|
2282
|
+
* before), so it throws an {@link InvariantError} (E020) naming the
|
|
2283
|
+
* fingerprint, the operation, and both SQL strings (truncated), never a
|
|
2284
|
+
* parameter value.
|
|
1950
2285
|
*
|
|
1951
2286
|
* @param op human label of the calling build method (for the error message).
|
|
1952
|
-
* @param cacheKey the cache fingerprint
|
|
1953
|
-
* @param entry the
|
|
2287
|
+
* @param cacheKey the cache fingerprint.
|
|
2288
|
+
* @param entry the SQL entry that will be executed.
|
|
1954
2289
|
* @param build the same closure passed to `acquireSql`; re-run here to
|
|
1955
2290
|
* capture the fresh build-path SQL + params.
|
|
1956
2291
|
* @param collectedParams the params the caller's collect path produced.
|
|
1957
2292
|
*/
|
|
1958
2293
|
crossCheckCache(op, cacheKey, entry, build, collectedParams) {
|
|
1959
|
-
if (!this.lastCacheHit)
|
|
2294
|
+
if (!this.lastCacheHit) {
|
|
2295
|
+
const built = this.lastMissParams;
|
|
2296
|
+
this.lastMissParams = undefined;
|
|
2297
|
+
if (built === undefined || cacheParamsEqual(collectedParams, built))
|
|
2298
|
+
return;
|
|
2299
|
+
throw this.lockstepError(op, cacheKey, [
|
|
2300
|
+
`built params and collected params diverge on a cache miss (collected ${collectedParams.length}, built ${built.length})`,
|
|
2301
|
+
], false);
|
|
2302
|
+
}
|
|
2303
|
+
if (entry.paramCount !== undefined && collectedParams.length !== entry.paramCount) {
|
|
2304
|
+
throw new InvariantError('sql-cache-param-count', `SQL cache param count mismatch on ${op} (fingerprint "${cacheKey}"): the cached statement has ` +
|
|
2305
|
+
`${entry.paramCount} placeholders but the cache hit collected ${collectedParams.length} params, so ` +
|
|
2306
|
+
'the statement was not sent.');
|
|
2307
|
+
}
|
|
2308
|
+
const check = this.diagnostics.cacheCheck;
|
|
2309
|
+
if (check === 'off')
|
|
1960
2310
|
return;
|
|
1961
|
-
const
|
|
1962
|
-
|
|
2311
|
+
const sampled = typeof check === 'object';
|
|
2312
|
+
// The sampling decision, and only it, is per hit.
|
|
2313
|
+
if (sampled && check.sample < 1 && Math.random() >= check.sample)
|
|
1963
2314
|
return;
|
|
1964
2315
|
const freshParams = [];
|
|
1965
2316
|
const freshSql = build(freshParams);
|
|
@@ -1975,34 +2326,39 @@ export class QueryInterface {
|
|
|
1975
2326
|
if (!paramsOk) {
|
|
1976
2327
|
details.push(`cache-hit params and freshly-built params diverge (collected ${collectedParams.length}, built ${freshParams.length})`);
|
|
1977
2328
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2329
|
+
throw this.lockstepError(op, cacheKey, details, sampled);
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* The `sql-cache-lockstep` {@link InvariantError}. When `log` is set (the
|
|
2333
|
+
* sampled hit path) the failure is also logged once per table and shape, so
|
|
2334
|
+
* it is durably recorded in the server log before the throw unwinds the
|
|
2335
|
+
* query; the other paths throw straight away.
|
|
2336
|
+
*/
|
|
2337
|
+
lockstepError(op, cacheKey, details, log) {
|
|
2338
|
+
const err = new InvariantError('sql-cache-lockstep', `SQL cache lockstep violation on ${op} (fingerprint "${cacheKey}"). The fingerprint, SQL-build, and ` +
|
|
2339
|
+
`param-collect paths must enumerate where-clause keys identically. ${INVARIANT_REPORT_HINT}\n` +
|
|
2340
|
+
details.join('\n'));
|
|
2341
|
+
if (log && shouldWarnOnce(CACHE_MISMATCH_WARN_NS, `${this.table}|${cacheKey}`)) {
|
|
2342
|
+
logTo(this.logger, 'error', err.message, { code: 'cache-mismatch', table: this.table, operation: op });
|
|
1988
2343
|
}
|
|
1989
|
-
|
|
2344
|
+
return err;
|
|
1990
2345
|
}
|
|
1991
2346
|
/**
|
|
1992
|
-
* Reset the
|
|
1993
|
-
* Exposed for tests so a single test process can
|
|
1994
|
-
*
|
|
2347
|
+
* Reset the unlimited-query warning dedupe, which is process-wide (see
|
|
2348
|
+
* {@link maybeWarnUnlimited}). Exposed for tests so a single test process can
|
|
2349
|
+
* verify the warning fires once per table without bleeding state between
|
|
2350
|
+
* assertions.
|
|
1995
2351
|
*/
|
|
1996
2352
|
resetUnlimitedWarnings() {
|
|
1997
|
-
|
|
2353
|
+
resetWarnOnce(WARN_NS.unlimitedFindMany);
|
|
1998
2354
|
}
|
|
1999
|
-
emitQueryEvent(
|
|
2355
|
+
emitQueryEvent(out, params, duration, action, rows, error, retried) {
|
|
2000
2356
|
const onQuery = this.options?._onQuery;
|
|
2001
2357
|
if (!onQuery)
|
|
2002
2358
|
return;
|
|
2003
2359
|
try {
|
|
2004
2360
|
onQuery({
|
|
2005
|
-
sql,
|
|
2361
|
+
sql: out.sql,
|
|
2006
2362
|
params,
|
|
2007
2363
|
duration,
|
|
2008
2364
|
model: this.table,
|
|
@@ -2012,12 +2368,28 @@ export class QueryInterface {
|
|
|
2012
2368
|
error,
|
|
2013
2369
|
strategy: this.currentStrategyTag,
|
|
2014
2370
|
...(retried ? { retried: true } : {}),
|
|
2371
|
+
...(out.comment === undefined ? {} : { comment: out.comment }),
|
|
2015
2372
|
});
|
|
2016
2373
|
}
|
|
2017
2374
|
catch {
|
|
2018
2375
|
// Listener errors must never crash a query
|
|
2019
2376
|
}
|
|
2020
2377
|
}
|
|
2378
|
+
/**
|
|
2379
|
+
* The statement `sql` as it goes out under `preparedName`: with the
|
|
2380
|
+
* `sqlComment` appended, and under the name that comment calls for (see
|
|
2381
|
+
* query/sql-comment.ts). This is the one place a comment is added, at the
|
|
2382
|
+
* execute seam and after the SQL-template cache, so the cache, its keys and
|
|
2383
|
+
* its lockstep checks only ever see commentless text. Without `sqlComment`
|
|
2384
|
+
* it is the statement unchanged.
|
|
2385
|
+
*/
|
|
2386
|
+
outgoing(sql, preparedName, action) {
|
|
2387
|
+
const commenter = this.sqlCommenter;
|
|
2388
|
+
const c = commenter?.commentFor(this.table, action);
|
|
2389
|
+
if (commenter === undefined || c === undefined)
|
|
2390
|
+
return { sql, text: sql, name: preparedName, comment: undefined };
|
|
2391
|
+
return { sql, text: `${sql} ${c.comment}`, name: commenter.preparedName(preparedName, c), comment: c.comment };
|
|
2392
|
+
}
|
|
2021
2393
|
/**
|
|
2022
2394
|
* Resolve the prepared-statement name a read should execute under, honouring
|
|
2023
2395
|
* the per-query {@link FindManyArgs.forceCustomPlan} opt-in.
|
|
@@ -2108,13 +2480,31 @@ export class QueryInterface {
|
|
|
2108
2480
|
}
|
|
2109
2481
|
/**
|
|
2110
2482
|
* Execute a pool.query with an optional timeout.
|
|
2111
|
-
*
|
|
2483
|
+
*
|
|
2484
|
+
* On a node-postgres pool outside a transaction the timeout is enforced on
|
|
2485
|
+
* the SERVER ({@link queryCancellable}). Anywhere else (a transaction's
|
|
2486
|
+
* connection, an engine shim, an HTTP pool) it races the query against a
|
|
2487
|
+
* timer: the call rejects on time but the statement keeps running, so the
|
|
2488
|
+
* error's `outcome` is `'unknown'`.
|
|
2489
|
+
*
|
|
2112
2490
|
* pg driver errors are translated to typed Turbine errors via wrapPgError.
|
|
2113
2491
|
* `retryIfStale` is {@link readWithTimeout}'s; nothing else sets it.
|
|
2114
2492
|
*/
|
|
2115
|
-
async queryWithTimeout(sql, params,
|
|
2116
|
-
let start = performance.now();
|
|
2493
|
+
async queryWithTimeout(sql, params, requestedTimeout, preparedName, retryIfStale = false, pool = this.pool) {
|
|
2117
2494
|
const action = this.currentAction;
|
|
2495
|
+
const out = this.outgoing(sql, preparedName, action);
|
|
2496
|
+
// 0 is no timeout, as it is for `$transaction`'s option and the server's statement_timeout.
|
|
2497
|
+
const timeout = requestedTimeout === 0 ? undefined : requestedTimeout;
|
|
2498
|
+
if (timeout !== undefined && pool === this.pool && this.cancelsOnTimeout()) {
|
|
2499
|
+
return this.queryCancellable(out, params, timeout, retryIfStale, action);
|
|
2500
|
+
}
|
|
2501
|
+
if (timeout !== undefined && pool === this.pool && this.txScoped && this.dialect.capabilities.backendCancel) {
|
|
2502
|
+
const state = transactionStateOf(this.pool);
|
|
2503
|
+
if (state !== undefined && canCancelBackend(state.client)) {
|
|
2504
|
+
return this.queryCancellableInTransaction(out, params, timeout, state, action);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
let start = performance.now();
|
|
2118
2508
|
// Build the query argument, use object form with `name` for prepared
|
|
2119
2509
|
// statements, or the plain (text, values) form otherwise.
|
|
2120
2510
|
//
|
|
@@ -2122,19 +2512,19 @@ export class QueryInterface {
|
|
|
2122
2512
|
// pool shims only speak `(text, values)`), so it is reached through a
|
|
2123
2513
|
// cast. Guarded at runtime by `preparedStatementsEnabled`, which only the
|
|
2124
2514
|
// drivers that accept it turn on.
|
|
2125
|
-
const usePrepared =
|
|
2515
|
+
const usePrepared = out.name && this.preparedStatementsEnabled;
|
|
2126
2516
|
const send = () => usePrepared
|
|
2127
|
-
?
|
|
2128
|
-
name:
|
|
2129
|
-
text:
|
|
2517
|
+
? pool.query({
|
|
2518
|
+
name: out.name,
|
|
2519
|
+
text: out.text,
|
|
2130
2520
|
values: params,
|
|
2131
2521
|
})
|
|
2132
|
-
:
|
|
2522
|
+
: pool.query(out.text, params);
|
|
2133
2523
|
let exec = send();
|
|
2134
2524
|
// Set when the caller's timeout has already answered, so a first attempt
|
|
2135
2525
|
// that fails AFTER that does not send a retry nobody is waiting for.
|
|
2136
2526
|
let abandoned = false;
|
|
2137
|
-
if (retryIfStale && !this.txScoped) {
|
|
2527
|
+
if (retryIfStale && !this.txScoped && pool === this.pool) {
|
|
2138
2528
|
exec = exec.catch(async (err) => {
|
|
2139
2529
|
if (abandoned || !isStaleConnectionError(err))
|
|
2140
2530
|
throw err;
|
|
@@ -2142,7 +2532,7 @@ export class QueryInterface {
|
|
|
2142
2532
|
// so a listener counting errors sees it and one alerting on failed
|
|
2143
2533
|
// CALLS can skip it. The final event below times the retry alone.
|
|
2144
2534
|
const wrapped = wrapPgError(err);
|
|
2145
|
-
this.emitQueryEvent(
|
|
2535
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined, true);
|
|
2146
2536
|
await settleEventLoop();
|
|
2147
2537
|
if (abandoned)
|
|
2148
2538
|
throw err;
|
|
@@ -2153,12 +2543,12 @@ export class QueryInterface {
|
|
|
2153
2543
|
if (!timeout) {
|
|
2154
2544
|
try {
|
|
2155
2545
|
const result = await exec;
|
|
2156
|
-
this.emitQueryEvent(
|
|
2546
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, result.rowCount ?? 0);
|
|
2157
2547
|
return result;
|
|
2158
2548
|
}
|
|
2159
2549
|
catch (err) {
|
|
2160
2550
|
const wrapped = wrapPgError(err);
|
|
2161
|
-
this.emitQueryEvent(
|
|
2551
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
|
|
2162
2552
|
throw wrapped;
|
|
2163
2553
|
}
|
|
2164
2554
|
}
|
|
@@ -2166,23 +2556,240 @@ export class QueryInterface {
|
|
|
2166
2556
|
const timeoutPromise = new Promise((_, reject) => {
|
|
2167
2557
|
timer = setTimeout(() => {
|
|
2168
2558
|
abandoned = true;
|
|
2169
|
-
reject(new TimeoutError(timeout
|
|
2559
|
+
reject(new TimeoutError(timeout, 'Query', {
|
|
2560
|
+
outcome: 'unknown',
|
|
2561
|
+
message: `Query timed out after ${timeout}ms. It could not be cancelled on the server and may still ` +
|
|
2562
|
+
'complete, so a write may have been applied: check before retrying it.',
|
|
2563
|
+
}));
|
|
2170
2564
|
}, timeout);
|
|
2171
2565
|
});
|
|
2172
2566
|
try {
|
|
2173
2567
|
const result = await Promise.race([exec, timeoutPromise]);
|
|
2174
|
-
this.emitQueryEvent(
|
|
2568
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, result.rowCount ?? 0);
|
|
2175
2569
|
return result;
|
|
2176
2570
|
}
|
|
2177
2571
|
catch (err) {
|
|
2178
2572
|
const wrapped = wrapPgError(err);
|
|
2179
|
-
this.emitQueryEvent(
|
|
2573
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
|
|
2180
2574
|
throw wrapped;
|
|
2181
2575
|
}
|
|
2182
2576
|
finally {
|
|
2183
2577
|
clearTimeout(timer);
|
|
2184
2578
|
}
|
|
2185
2579
|
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Whether a timed statement can be stopped on the server: an engine with a
|
|
2582
|
+
* protocol cancel (PostgreSQL) on a node-postgres pool, outside a transaction
|
|
2583
|
+
* (inside one, the connection belongs to the transaction and is not this
|
|
2584
|
+
* method's to cancel or destroy).
|
|
2585
|
+
*/
|
|
2586
|
+
cancelsOnTimeout() {
|
|
2587
|
+
return !this.txScoped && this.dialect.capabilities.backendCancel && isNodePostgresPool(this.pool);
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* {@link queryWithTimeout} with a timeout the server enforces. The statement
|
|
2591
|
+
* runs on a connection checked out for it, so when the deadline passes the
|
|
2592
|
+
* backend running it is known and gets a protocol cancel request.
|
|
2593
|
+
*
|
|
2594
|
+
* The call then waits, at most {@link CANCEL_GRACE_MS}, for the statement to
|
|
2595
|
+
* answer, and the answer decides the result:
|
|
2596
|
+
* - cancelled (57014): TimeoutError, `outcome: 'not-executed'`;
|
|
2597
|
+
* - finished first: the result, returned late, because reporting a write
|
|
2598
|
+
* that committed as a timeout invites a retry that applies it twice;
|
|
2599
|
+
* - failed: that error;
|
|
2600
|
+
* - no answer: TimeoutError, `outcome: 'unknown'`, and the connection is
|
|
2601
|
+
* destroyed rather than handed to the next caller still busy.
|
|
2602
|
+
* A deadline that passes while still waiting for a connection sends nothing
|
|
2603
|
+
* (`'not-executed'`). Either way the connection goes back to the pool only
|
|
2604
|
+
* once its backend is idle, so timed-out statements cannot hold the pool.
|
|
2605
|
+
*/
|
|
2606
|
+
async queryCancellable(out, params, timeout, retryIfStale, action) {
|
|
2607
|
+
let start = performance.now();
|
|
2608
|
+
const config = out.name && this.preparedStatementsEnabled ? { name: out.name, text: out.text, values: params } : null;
|
|
2609
|
+
const report = (err, retried) => {
|
|
2610
|
+
const wrapped = err instanceof TimeoutError ? err : wrapPgError(err);
|
|
2611
|
+
const error = wrapped instanceof Error ? wrapped : undefined;
|
|
2612
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, error, retried);
|
|
2613
|
+
return wrapped;
|
|
2614
|
+
};
|
|
2615
|
+
let expired = false;
|
|
2616
|
+
let expire = () => { };
|
|
2617
|
+
const deadline = new Promise((resolve) => {
|
|
2618
|
+
expire = resolve;
|
|
2619
|
+
});
|
|
2620
|
+
const timer = setTimeout(() => {
|
|
2621
|
+
expired = true;
|
|
2622
|
+
expire(TIMED_OUT);
|
|
2623
|
+
}, timeout);
|
|
2624
|
+
const neverSent = () => new TimeoutError(timeout, 'Query', {
|
|
2625
|
+
outcome: 'not-executed',
|
|
2626
|
+
message: `Query timed out after ${timeout}ms before it reached the server. It made no change.`,
|
|
2627
|
+
});
|
|
2628
|
+
try {
|
|
2629
|
+
for (let attempt = 0;; attempt++) {
|
|
2630
|
+
const checkingOut = acquireConnection(this.pool);
|
|
2631
|
+
let held;
|
|
2632
|
+
try {
|
|
2633
|
+
held = await Promise.race([checkingOut, deadline]);
|
|
2634
|
+
}
|
|
2635
|
+
catch (err) {
|
|
2636
|
+
throw report(err);
|
|
2637
|
+
}
|
|
2638
|
+
if (held === TIMED_OUT) {
|
|
2639
|
+
checkingOut.then((late) => late.checkout.release(), () => { });
|
|
2640
|
+
throw report(neverSent());
|
|
2641
|
+
}
|
|
2642
|
+
const { client, checkout } = held;
|
|
2643
|
+
const running = (config
|
|
2644
|
+
? client.query(config)
|
|
2645
|
+
: client.query(out.text, params)).then((result) => ({ result }), (error) => ({ error }));
|
|
2646
|
+
const first = await Promise.race([running, deadline]);
|
|
2647
|
+
const settled = first === TIMED_OUT ? await this.cancelTimedOut(held, running, timeout) : first;
|
|
2648
|
+
if ('result' in settled) {
|
|
2649
|
+
if (first !== TIMED_OUT)
|
|
2650
|
+
checkout.release();
|
|
2651
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, settled.result.rowCount ?? 0);
|
|
2652
|
+
return settled.result;
|
|
2653
|
+
}
|
|
2654
|
+
if ('timeout' in settled)
|
|
2655
|
+
throw report(settled.timeout);
|
|
2656
|
+
const err = settled.error;
|
|
2657
|
+
if (first === TIMED_OUT)
|
|
2658
|
+
throw report(err);
|
|
2659
|
+
checkout.release(err instanceof Error ? err : true);
|
|
2660
|
+
if (!(retryIfStale && attempt === 0 && isStaleConnectionError(err)))
|
|
2661
|
+
throw report(err);
|
|
2662
|
+
report(err, true);
|
|
2663
|
+
await settleEventLoop();
|
|
2664
|
+
if (expired)
|
|
2665
|
+
throw report(neverSent());
|
|
2666
|
+
start = performance.now();
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
finally {
|
|
2670
|
+
clearTimeout(timer);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* A timed statement inside a transaction, cancelled on the server when the
|
|
2675
|
+
* deadline passes, on the transaction's own connection.
|
|
2676
|
+
*
|
|
2677
|
+
* A cancelled statement made no change, but PostgreSQL aborts the
|
|
2678
|
+
* transaction it ran in, so the timeout ends the transaction too: it is
|
|
2679
|
+
* marked to roll back ({@link TransactionState.abort}), which also covers a
|
|
2680
|
+
* cancel that arrived after the statement finished. The error is
|
|
2681
|
+
* `outcome: 'not-executed'` either way, because nothing it did can commit.
|
|
2682
|
+
* A statement that finishes before the deadline is returned as usual.
|
|
2683
|
+
*/
|
|
2684
|
+
async queryCancellableInTransaction(out, params, timeout, state, action) {
|
|
2685
|
+
const start = performance.now();
|
|
2686
|
+
const running = (out.name && this.preparedStatementsEnabled
|
|
2687
|
+
? this.pool.query({
|
|
2688
|
+
name: out.name,
|
|
2689
|
+
text: out.text,
|
|
2690
|
+
values: params,
|
|
2691
|
+
})
|
|
2692
|
+
: this.pool.query(out.text, params)).then((result) => ({ result }), (error) => ({ error }));
|
|
2693
|
+
let timer;
|
|
2694
|
+
const deadline = new Promise((resolve) => {
|
|
2695
|
+
timer = setTimeout(() => resolve(TIMED_OUT), timeout);
|
|
2696
|
+
});
|
|
2697
|
+
let settled;
|
|
2698
|
+
try {
|
|
2699
|
+
const first = await Promise.race([running, deadline]);
|
|
2700
|
+
settled = first === TIMED_OUT ? await this.cancelInTransaction(state, running, timeout) : first;
|
|
2701
|
+
}
|
|
2702
|
+
finally {
|
|
2703
|
+
clearTimeout(timer);
|
|
2704
|
+
}
|
|
2705
|
+
if ('result' in settled) {
|
|
2706
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, settled.result.rowCount ?? 0);
|
|
2707
|
+
return settled.result;
|
|
2708
|
+
}
|
|
2709
|
+
const err = 'timeout' in settled ? settled.timeout : wrapPgError(settled.error);
|
|
2710
|
+
this.emitQueryEvent(out, params, performance.now() - start, action, 0, err instanceof Error ? err : undefined);
|
|
2711
|
+
throw err;
|
|
2712
|
+
}
|
|
2713
|
+
/** Cancel a timed-out statement in a transaction and end the transaction. See {@link queryCancellableInTransaction}. */
|
|
2714
|
+
async cancelInTransaction(state, running, timeout) {
|
|
2715
|
+
let graceTimer;
|
|
2716
|
+
const graceOver = new Promise((resolve) => {
|
|
2717
|
+
graceTimer = setTimeout(() => resolve(TIMED_OUT), CANCEL_GRACE_MS);
|
|
2718
|
+
});
|
|
2719
|
+
try {
|
|
2720
|
+
await Promise.race([sendCancelRequest(state.client, CANCEL_GRACE_MS), graceOver]);
|
|
2721
|
+
const answer = await Promise.race([running, graceOver]);
|
|
2722
|
+
if (answer !== TIMED_OUT && 'result' in answer)
|
|
2723
|
+
return answer;
|
|
2724
|
+
const cancelled = answer !== TIMED_OUT && isQueryCanceled(answer.error);
|
|
2725
|
+
if (answer !== TIMED_OUT && !cancelled)
|
|
2726
|
+
return answer;
|
|
2727
|
+
const error = new TimeoutError(timeout, 'Query', {
|
|
2728
|
+
outcome: 'not-executed',
|
|
2729
|
+
...(cancelled ? { cause: answer.error } : {}),
|
|
2730
|
+
message: (cancelled
|
|
2731
|
+
? `Query timed out after ${timeout}ms and was cancelled on the server, so it made no change. `
|
|
2732
|
+
: `Query timed out after ${timeout}ms, and the server did not confirm its cancellation within ` +
|
|
2733
|
+
`${CANCEL_GRACE_MS}ms. `) +
|
|
2734
|
+
'The transaction it ran in is over: it rolls back instead of committing, and no statement in it is ' +
|
|
2735
|
+
'kept. Let this error leave the transaction callback and retry the whole transaction, or run a ' +
|
|
2736
|
+
'statement that may time out in a nested `tx.$transaction(...)`, which rolls back only that block.',
|
|
2737
|
+
});
|
|
2738
|
+
state.abort(error);
|
|
2739
|
+
return { timeout: error };
|
|
2740
|
+
}
|
|
2741
|
+
finally {
|
|
2742
|
+
clearTimeout(graceTimer);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Cancel a statement that outlived its timeout, wait for it to answer, and
|
|
2747
|
+
* release its connection accordingly. See {@link queryCancellable}.
|
|
2748
|
+
*/
|
|
2749
|
+
async cancelTimedOut(held, running, timeout) {
|
|
2750
|
+
let graceTimer;
|
|
2751
|
+
const graceOver = new Promise((resolve) => {
|
|
2752
|
+
graceTimer = setTimeout(() => resolve(TIMED_OUT), CANCEL_GRACE_MS);
|
|
2753
|
+
});
|
|
2754
|
+
try {
|
|
2755
|
+
const delivered = await Promise.race([sendCancelRequest(held.client, CANCEL_GRACE_MS), graceOver]);
|
|
2756
|
+
const answer = await Promise.race([running, graceOver]);
|
|
2757
|
+
if (answer === TIMED_OUT) {
|
|
2758
|
+
held.checkout.release(new Error('the statement did not stop after a cancel request'));
|
|
2759
|
+
return {
|
|
2760
|
+
timeout: new TimeoutError(timeout, 'Query', {
|
|
2761
|
+
outcome: 'unknown',
|
|
2762
|
+
message: `Query timed out after ${timeout}ms and the server did not confirm its cancellation within ` +
|
|
2763
|
+
`${CANCEL_GRACE_MS}ms, so it may still complete and a write may have been applied: check before ` +
|
|
2764
|
+
'retrying it. The connection was closed.',
|
|
2765
|
+
}),
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
// A cancel that was sent but not confirmed could still land on the NEXT
|
|
2769
|
+
// statement this connection runs, so such a connection is not reused.
|
|
2770
|
+
const reusable = delivered === true;
|
|
2771
|
+
if ('result' in answer) {
|
|
2772
|
+
held.checkout.release(reusable ? undefined : true);
|
|
2773
|
+
return answer;
|
|
2774
|
+
}
|
|
2775
|
+
const err = answer.error;
|
|
2776
|
+
if (reusable && err?.code === '57014') {
|
|
2777
|
+
held.checkout.release();
|
|
2778
|
+
return {
|
|
2779
|
+
timeout: new TimeoutError(timeout, 'Query', {
|
|
2780
|
+
outcome: 'not-executed',
|
|
2781
|
+
cause: err,
|
|
2782
|
+
message: `Query timed out after ${timeout}ms and was cancelled on the server. It made no change.`,
|
|
2783
|
+
}),
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
held.checkout.release(err instanceof Error ? err : true);
|
|
2787
|
+
return answer;
|
|
2788
|
+
}
|
|
2789
|
+
finally {
|
|
2790
|
+
clearTimeout(graceTimer);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2186
2793
|
/**
|
|
2187
2794
|
* Execute a write `DeferredQuery` (create/update/delete/upsert) according to
|
|
2188
2795
|
* the active dialect's {@link Dialect.resultStrategy}, then apply its
|
|
@@ -2197,18 +2804,104 @@ export class QueryInterface {
|
|
|
2197
2804
|
* write and a follow-up SELECT; the SELECT's rows feed the transform.
|
|
2198
2805
|
*/
|
|
2199
2806
|
async executeMutation(deferred, timeout) {
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2807
|
+
const reselect = deferred.reselect;
|
|
2808
|
+
if (this.dialect.resultStrategy === 'reselect' && reselect) {
|
|
2809
|
+
// One transaction, so the row returned is the row this call wrote, and
|
|
2810
|
+
// the transform runs before COMMIT so a refusal it raises writes nothing.
|
|
2811
|
+
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.');
|
|
2204
2812
|
}
|
|
2205
2813
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, timeout, deferred.preparedName);
|
|
2206
2814
|
return deferred.transform(result);
|
|
2207
2815
|
}
|
|
2816
|
+
/**
|
|
2817
|
+
* The executor for a write's statements, sharing one deadline: each one gets
|
|
2818
|
+
* what is left of `timeout`, so a chunked `createMany` or a lock, write and
|
|
2819
|
+
* re-read cannot run for N times the timeout. A statement due past the
|
|
2820
|
+
* deadline is not sent. `onClient` routes them to a held connection.
|
|
2821
|
+
*/
|
|
2822
|
+
deadlineExec(timeout, onClient) {
|
|
2823
|
+
const deadline = timeout ? performance.now() + timeout : undefined;
|
|
2824
|
+
return async (sql, params, preparedName) => {
|
|
2825
|
+
let left = timeout;
|
|
2826
|
+
if (deadline !== undefined) {
|
|
2827
|
+
left = Math.ceil(deadline - performance.now());
|
|
2828
|
+
if (left <= 0) {
|
|
2829
|
+
throw new TimeoutError(timeout, 'Query', {
|
|
2830
|
+
outcome: 'not-executed',
|
|
2831
|
+
message: `Query timed out after ${timeout}ms: the write's statements share one deadline, and it passed ` +
|
|
2832
|
+
'before the next one was sent. Its earlier statements are rolled back, so it makes no change.',
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return onClient
|
|
2837
|
+
? this.queryWithTimeout(sql, params, left, preparedName, false, onClient)
|
|
2838
|
+
: this.queryWithTimeout(sql, params, left, preparedName);
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
/**
|
|
2842
|
+
* Run a write that takes several statements as one transaction on one
|
|
2843
|
+
* connection: the caller's transaction when there is one, else one opened
|
|
2844
|
+
* here. A statement that times out is cancelled where the engine allows it,
|
|
2845
|
+
* and its connection is closed rather than rolled back (a ROLLBACK would wait
|
|
2846
|
+
* behind it); the transaction never commits, so the write made no change.
|
|
2847
|
+
* `timeout` is one deadline for every statement (see {@link deadlineExec}).
|
|
2848
|
+
*/
|
|
2849
|
+
async inOneTransaction(work, timeout, operation, alternative) {
|
|
2850
|
+
if (this.txScoped) {
|
|
2851
|
+
// In the caller's transaction, a savepoint keeps the write whole: a later
|
|
2852
|
+
// statement that fails and is caught cannot leave the earlier ones behind.
|
|
2853
|
+
return withSavepoint(this.pool, () => work(this.deadlineExec(timeout)));
|
|
2854
|
+
}
|
|
2855
|
+
if (typeof this.pool.connect !== 'function') {
|
|
2856
|
+
throw new 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 ` +
|
|
2857
|
+
`this pool cannot check one out (a stateless HTTP driver). ${alternative}`);
|
|
2858
|
+
}
|
|
2859
|
+
const { client, checkout } = await openCheckout(this.pool, (c) => c.query(this.dialect.beginStatement()));
|
|
2860
|
+
const onClient = {
|
|
2861
|
+
query: (text, values) => typeof text === 'string'
|
|
2862
|
+
? client.query(text, values)
|
|
2863
|
+
: client.query(text),
|
|
2864
|
+
};
|
|
2865
|
+
try {
|
|
2866
|
+
const value = await work(this.deadlineExec(timeout, onClient));
|
|
2867
|
+
await client.query(this.dialect.commitStatement());
|
|
2868
|
+
checkout.release();
|
|
2869
|
+
return value;
|
|
2870
|
+
}
|
|
2871
|
+
catch (err) {
|
|
2872
|
+
if (err instanceof TimeoutError) {
|
|
2873
|
+
if (await sendCancelRequest(client, CANCEL_GRACE_MS)) {
|
|
2874
|
+
checkout.release(err);
|
|
2875
|
+
}
|
|
2876
|
+
else {
|
|
2877
|
+
// No cancel reached the server (or the engine has none), so the
|
|
2878
|
+
// statement may still be running. The ROLLBACK queues behind it on
|
|
2879
|
+
// this connection, and the connection goes back to the pool only
|
|
2880
|
+
// after it: never with the transaction still open.
|
|
2881
|
+
client.query(this.dialect.rollbackStatement()).then(() => checkout.release(), (e) => checkout.release(e instanceof Error ? e : true));
|
|
2882
|
+
}
|
|
2883
|
+
throw new TimeoutError(err.timeoutMs, 'Query', {
|
|
2884
|
+
outcome: 'not-executed',
|
|
2885
|
+
cause: err,
|
|
2886
|
+
message: `Query timed out after ${err.timeoutMs}ms. Its transaction is rolled back rather than committed, ` +
|
|
2887
|
+
'so it makes no change.',
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
try {
|
|
2891
|
+
await client.query(this.dialect.rollbackStatement());
|
|
2892
|
+
}
|
|
2893
|
+
catch {
|
|
2894
|
+
// Best effort: the connection may be gone.
|
|
2895
|
+
}
|
|
2896
|
+
checkout.release();
|
|
2897
|
+
throw explainConnectionLoss(wrapPgError(err), checkout.lostWith);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2208
2900
|
// ---------------------------------------------------------------------------
|
|
2209
2901
|
// Write compilation (extracted to writes.ts).
|
|
2210
2902
|
// ---------------------------------------------------------------------------
|
|
2211
2903
|
buildCreate(args) {
|
|
2904
|
+
args = this.normalizeArgs(args);
|
|
2212
2905
|
return writesMod.buildCreate(this.ctx, args, this.resolveWriteProjection(args));
|
|
2213
2906
|
}
|
|
2214
2907
|
/**
|
|
@@ -2230,22 +2923,26 @@ export class QueryInterface {
|
|
|
2230
2923
|
return { columns };
|
|
2231
2924
|
}
|
|
2232
2925
|
buildCreateMany(args) {
|
|
2233
|
-
|
|
2926
|
+
args = this.normalizeArgs(args);
|
|
2927
|
+
return writesMod.buildCreateMany(this.ctx, args, 'refuse', this.resolveWriteProjection(args));
|
|
2234
2928
|
}
|
|
2235
2929
|
buildUpdate(args) {
|
|
2930
|
+
args = this.normalizeArgs(args);
|
|
2236
2931
|
return writesMod.buildUpdate(this.ctx, args, this.resolveWriteProjection(args));
|
|
2237
2932
|
}
|
|
2238
2933
|
buildDelete(args) {
|
|
2934
|
+
args = this.normalizeArgs(args);
|
|
2239
2935
|
return writesMod.buildDelete(this.ctx, args, this.resolveWriteProjection(args));
|
|
2240
2936
|
}
|
|
2241
2937
|
buildUpsert(args) {
|
|
2938
|
+
args = this.normalizeArgs(args);
|
|
2242
2939
|
return writesMod.buildUpsert(this.ctx, args, this.resolveWriteProjection(args));
|
|
2243
2940
|
}
|
|
2244
2941
|
buildUpdateMany(args) {
|
|
2245
|
-
return writesMod.buildUpdateMany(this.ctx, args);
|
|
2942
|
+
return writesMod.buildUpdateMany(this.ctx, this.normalizeArgs(args));
|
|
2246
2943
|
}
|
|
2247
2944
|
buildDeleteMany(args) {
|
|
2248
|
-
return writesMod.buildDeleteMany(this.ctx, args);
|
|
2945
|
+
return writesMod.buildDeleteMany(this.ctx, this.normalizeArgs(args));
|
|
2249
2946
|
}
|
|
2250
2947
|
/**
|
|
2251
2948
|
* Best-effort extraction of an auto-generated primary key from a write
|
|
@@ -2261,11 +2958,10 @@ export class QueryInterface {
|
|
|
2261
2958
|
* If no middlewares are registered, executes directly.
|
|
2262
2959
|
*
|
|
2263
2960
|
* Middleware can inspect and log query parameters, measure timing, and
|
|
2264
|
-
* transform the result returned by `next()`.
|
|
2265
|
-
*
|
|
2266
|
-
*
|
|
2267
|
-
* (e.g. soft deletes) belong in the query itself
|
|
2268
|
-
* `where: { deletedAt: null }` or wrap the table accessor in a small helper.
|
|
2961
|
+
* transform the result returned by `next()`. `params.args` is a plain copy
|
|
2962
|
+
* (see `readOnlyMiddlewareParams`): the query is compiled from the caller's
|
|
2963
|
+
* args, so `next()` throws a TypeError when the copy was changed. Cross-cutting
|
|
2964
|
+
* filters (e.g. soft deletes) belong in the query itself, or in `globalFilters`.
|
|
2269
2965
|
*/
|
|
2270
2966
|
async executeWithMiddleware(action, args, executor) {
|
|
2271
2967
|
this.currentAction = action;
|
|
@@ -2274,14 +2970,17 @@ export class QueryInterface {
|
|
|
2274
2970
|
// can be complete. Putting it in each method instead would mean fifteen
|
|
2275
2971
|
// sites and a new one every time an operation is added, which is precisely
|
|
2276
2972
|
// how the surface it checks came to need checking.
|
|
2277
|
-
warnUnknownQueryOptions(this.table, action, args);
|
|
2973
|
+
warnUnknownQueryOptions(this.table, action, args, this.advice ?? null);
|
|
2278
2974
|
if (this.middlewares.length === 0) {
|
|
2279
2975
|
return executor();
|
|
2280
2976
|
}
|
|
2281
|
-
|
|
2977
|
+
// A copy: the query is compiled from `args`, not from what middleware
|
|
2978
|
+
// sees, so `next()` throws on a changed copy rather than doing nothing.
|
|
2979
|
+
const params = readOnlyMiddlewareParams(this.table, action, args);
|
|
2282
2980
|
// Build middleware chain
|
|
2283
2981
|
let index = 0;
|
|
2284
2982
|
const next = async (p) => {
|
|
2983
|
+
assertMiddlewareArgsKept(params, p);
|
|
2285
2984
|
if (index < this.middlewares.length) {
|
|
2286
2985
|
const mw = this.middlewares[index++];
|
|
2287
2986
|
return mw(p, next);
|
|
@@ -2304,6 +3003,7 @@ export class QueryInterface {
|
|
|
2304
3003
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2305
3004
|
// depending on every consumer to re-establish it.
|
|
2306
3005
|
args = this.normalizeArgs(args);
|
|
3006
|
+
this.assertLockInTransaction(args, 'findUnique');
|
|
2307
3007
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
2308
3008
|
if (args.with) {
|
|
2309
3009
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -2356,6 +3056,9 @@ export class QueryInterface {
|
|
|
2356
3056
|
return entity;
|
|
2357
3057
|
}
|
|
2358
3058
|
buildFindUnique(args) {
|
|
3059
|
+
// Canonical args FIRST: the empty-where guard and the unique-key rule below
|
|
3060
|
+
// must read the pruned `where`, never one still carrying `undefined` keys.
|
|
3061
|
+
args = this.normalizeArgs(args);
|
|
2359
3062
|
this.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
2360
3063
|
// Pinned before the cache key, which carries it (see buildFindMany).
|
|
2361
3064
|
const jsonEncoding = this.resolveJsonEncoding(args.jsonEncoding);
|
|
@@ -2377,9 +3080,10 @@ export class QueryInterface {
|
|
|
2377
3080
|
// deliberately so: a tenant filter is not a unique selector, and letting it
|
|
2378
3081
|
// satisfy this would still hand back an arbitrary row from inside the tenant.
|
|
2379
3082
|
//
|
|
2380
|
-
// `findFirst`
|
|
2381
|
-
//
|
|
2382
|
-
//
|
|
3083
|
+
// `findFirst` takes no such guard: "the first row matching an optional
|
|
3084
|
+
// filter" is its contract, and `where: {}` or no `where` asks for any row.
|
|
3085
|
+
// It refuses only a `where` that was emptied by `undefined` values (see
|
|
3086
|
+
// normalizeArgs), which never meant "any row".
|
|
2383
3087
|
if (whereMod.userPredicateIsEmpty(this.ctx, (args.where ?? {}))) {
|
|
2384
3088
|
throw new ValidationError(`findUnique on "${this.table}" refused: the \`where\` clause has no predicate, ` +
|
|
2385
3089
|
'so this would return an arbitrary row rather than a specific one. ' +
|
|
@@ -2399,9 +3103,9 @@ export class QueryInterface {
|
|
|
2399
3103
|
// a global filter is not an identity, and letting one satisfy this would
|
|
2400
3104
|
// hand back an arbitrary row from inside the tenant.
|
|
2401
3105
|
this.assertFindUniqueKey(args.where);
|
|
2402
|
-
//
|
|
2403
|
-
|
|
2404
|
-
|
|
3106
|
+
// The locking clause, read by the cache key and all three SQL paths below.
|
|
3107
|
+
const rowLock = resolveRowLock(args.lock, this.dialect, this.table);
|
|
3108
|
+
const lockSql = rowLock?.sql ?? '';
|
|
2405
3109
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2406
3110
|
// relations before fingerprinting (see buildFindMany).
|
|
2407
3111
|
if (args.with && this.resolveStableOrder(args.stableRelationOrder)) {
|
|
@@ -2431,7 +3135,9 @@ export class QueryInterface {
|
|
|
2431
3135
|
// `|je=`: same rule and same hazard as in buildFindMany. findUnique is never
|
|
2432
3136
|
// flatten-planned, so this is the only plan-shape segment it needs.
|
|
2433
3137
|
const encodingFp = `|je=${jsonEncoding === 'positional' ? 'p' : 'o'}`;
|
|
2434
|
-
|
|
3138
|
+
// Empty without a lock, so every unlocked key is unchanged.
|
|
3139
|
+
const lockFp = rowLock ? `|lk=${rowLock.fp}` : '';
|
|
3140
|
+
const ck = `fu:${whereFingerprint}|c=${colKey}|w=${withFp}|pii=${includePii ? 1 : 0}${encodingFp}${lockFp}${this.globalFilterCacheSegment()}`;
|
|
2435
3141
|
const params = [];
|
|
2436
3142
|
// Check if all where values are simple (plain equality, no operators/null/OR).
|
|
2437
3143
|
// Keys are sorted to match fingerprintWhere, insertion order here would let
|
|
@@ -2439,14 +3145,19 @@ export class QueryInterface {
|
|
|
2439
3145
|
const whereKeys = Object.keys(whereObj)
|
|
2440
3146
|
.filter((k) => whereObj[k] !== undefined)
|
|
2441
3147
|
.sort();
|
|
3148
|
+
// Only a scalar the general path would accept as it is (a primitive, Date
|
|
3149
|
+
// or Buffer) on a non-JSON column takes the simple path, so no operand
|
|
3150
|
+
// reaches SQL without the general path's validation. Resolved, not looked
|
|
3151
|
+
// up: a relation filter spelled the snake_case way must not read as a
|
|
3152
|
+
// plain equality.
|
|
2442
3153
|
const isSimpleWhere = !whereObj.OR &&
|
|
2443
3154
|
!whereObj.AND &&
|
|
2444
3155
|
!whereObj.NOT &&
|
|
2445
3156
|
whereKeys.every((k) => {
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
return
|
|
3157
|
+
if (!isFastPathScalar(whereObj[k]) || resolveRelationDef(this.tableMeta.relations, k))
|
|
3158
|
+
return false;
|
|
3159
|
+
const column = resolveColumnName(this.tableMeta, k);
|
|
3160
|
+
return column !== undefined && !this.isJsonColumnType(this.getColumnPgType(column));
|
|
2450
3161
|
});
|
|
2451
3162
|
// Simple path: plain equality, no operators/null/OR.
|
|
2452
3163
|
//
|
|
@@ -2467,7 +3178,7 @@ export class QueryInterface {
|
|
|
2467
3178
|
});
|
|
2468
3179
|
const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
2469
3180
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
2470
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
3181
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}${lockSql}`;
|
|
2471
3182
|
};
|
|
2472
3183
|
const entry = this.acquireSql(ck, buildSql);
|
|
2473
3184
|
// Collect params (same order and same coercion as build)
|
|
@@ -2484,6 +3195,7 @@ export class QueryInterface {
|
|
|
2484
3195
|
},
|
|
2485
3196
|
tag: `${this.table}.findUnique`,
|
|
2486
3197
|
preparedName: entry.name,
|
|
3198
|
+
...rowLockMarker(rowLock),
|
|
2487
3199
|
};
|
|
2488
3200
|
}
|
|
2489
3201
|
// General path (with operators, null, OR, with clause)
|
|
@@ -2493,7 +3205,7 @@ export class QueryInterface {
|
|
|
2493
3205
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
2494
3206
|
const qt = this.q(this.table);
|
|
2495
3207
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
2496
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
3208
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}${lockSql}`;
|
|
2497
3209
|
};
|
|
2498
3210
|
const entry = this.acquireSql(ck, buildSql);
|
|
2499
3211
|
// Collect params
|
|
@@ -2508,6 +3220,7 @@ export class QueryInterface {
|
|
|
2508
3220
|
},
|
|
2509
3221
|
tag: `${this.table}.findUnique`,
|
|
2510
3222
|
preparedName: entry.name,
|
|
3223
|
+
...rowLockMarker(rowLock),
|
|
2511
3224
|
};
|
|
2512
3225
|
}
|
|
2513
3226
|
// Nested queries with `with` clause.
|
|
@@ -2519,7 +3232,7 @@ export class QueryInterface {
|
|
|
2519
3232
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
2520
3233
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
2521
3234
|
const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList, undefined, undefined, includePii);
|
|
2522
|
-
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}`;
|
|
3235
|
+
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}${lockSql}`;
|
|
2523
3236
|
};
|
|
2524
3237
|
const entry = this.acquireSql(ck, buildSql);
|
|
2525
3238
|
// Collect params in exact build order: where first, then with-clause relations
|
|
@@ -2536,6 +3249,7 @@ export class QueryInterface {
|
|
|
2536
3249
|
},
|
|
2537
3250
|
tag: `${this.table}.findUnique`,
|
|
2538
3251
|
preparedName: entry.name,
|
|
3252
|
+
...rowLockMarker(rowLock),
|
|
2539
3253
|
};
|
|
2540
3254
|
}
|
|
2541
3255
|
// -------------------------------------------------------------------------
|
|
@@ -2551,19 +3265,20 @@ export class QueryInterface {
|
|
|
2551
3265
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2552
3266
|
// depending on every consumer to re-establish it.
|
|
2553
3267
|
args = this.normalizeArgs(args);
|
|
3268
|
+
this.assertLockInTransaction(args, 'findMany');
|
|
2554
3269
|
this.maybeWarnUnlimited(args);
|
|
2555
3270
|
this.maybeWarnUnorderedPage(args);
|
|
2556
|
-
//
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
3271
|
+
// Advice: warn on deeply nested with clauses.
|
|
3272
|
+
const advice = this.advice;
|
|
3273
|
+
if (advice && args?.with) {
|
|
3274
|
+
const depth = this.measureWithDepth(args.with);
|
|
3275
|
+
if (depth > 5 && shouldWarnOnce(WARN_NS.deepWith, this.table)) {
|
|
3276
|
+
logTo(advice, 'warn', `Deep with clause (depth ${depth}) on "${this.tableMeta.name}": every level is a ` +
|
|
3277
|
+
'correlated subquery the engine re-evaluates once per row of the level above, so the work ' +
|
|
3278
|
+
'multiplies down the tree and the whole subtree is built as JSON inside each parent row. Split ' +
|
|
3279
|
+
"into separate queries, or use `relationLoadStrategy: 'batched'` (one flat statement per " +
|
|
3280
|
+
'relation, no per-parent re-evaluation). Advice: silent when `diagnostics.advice` is off, as it ' +
|
|
3281
|
+
'is by default in production.', { code: 'deep-with', table: this.table, operation: 'findMany', depth });
|
|
2567
3282
|
}
|
|
2568
3283
|
}
|
|
2569
3284
|
return this.executeWithMiddleware('findMany', (args ?? {}), async () => {
|
|
@@ -2638,11 +3353,13 @@ export class QueryInterface {
|
|
|
2638
3353
|
}
|
|
2639
3354
|
}
|
|
2640
3355
|
/**
|
|
2641
|
-
* Emit a one-time
|
|
3356
|
+
* Emit a one-time warning when {@link findMany} is called without an
|
|
2642
3357
|
* explicit `limit`/`take` and `warnOnUnlimited` has not been disabled.
|
|
2643
3358
|
*
|
|
2644
|
-
*
|
|
2645
|
-
* loop calling `db.users.findMany()` thousands of times
|
|
3359
|
+
* Said once per table per process (`WARN_NS.unlimitedFindMany`), so a busy
|
|
3360
|
+
* loop calling `db.users.findMany()` thousands of times logs once, and so
|
|
3361
|
+
* does the same call inside every `$transaction`, which builds its own
|
|
3362
|
+
* query interfaces.
|
|
2646
3363
|
* Suppressed when `defaultLimit` is configured (the caller has already
|
|
2647
3364
|
* opted in to a bounded query) and when the user passed an explicit
|
|
2648
3365
|
* `limit`, `take`, or `cursor`. A per-call `warnOnUnlimited` overrides the
|
|
@@ -2658,19 +3375,18 @@ export class QueryInterface {
|
|
|
2658
3375
|
return;
|
|
2659
3376
|
if (this.defaultLimit !== undefined)
|
|
2660
3377
|
return;
|
|
2661
|
-
const hasExplicitLimit = args?.limit !== undefined || args?.cursor
|
|
3378
|
+
const hasExplicitLimit = args?.limit !== undefined || hasCursor(args?.cursor);
|
|
2662
3379
|
if (hasExplicitLimit)
|
|
2663
3380
|
return;
|
|
2664
3381
|
if (this.whereMatchesAtMostOneRow(args?.where))
|
|
2665
3382
|
return;
|
|
2666
|
-
if (
|
|
3383
|
+
if (!shouldWarnOnce(WARN_NS.unlimitedFindMany, this.table))
|
|
2667
3384
|
return;
|
|
2668
|
-
this.
|
|
2669
|
-
console.warn(`[turbine] warning: findMany on "${this.table}" has no limit: the statement is emitted with no row limit, ` +
|
|
3385
|
+
logTo(this.logger, 'warn', `warning: findMany on "${this.table}" has no limit: the statement is emitted with no row limit, ` +
|
|
2670
3386
|
'so the engine returns every matching row and the driver materializes all of them as objects before ' +
|
|
2671
3387
|
'this call resolves (the cost grows with the table, not with the rows you use). Pass `limit`/`take`, ' +
|
|
2672
3388
|
'or set `defaultLimit` in the client config; silence with `warnOnUnlimited: false` (per call, per ' +
|
|
2673
|
-
'table, or in config).');
|
|
3389
|
+
'table, or in config).', { code: 'unlimited-find-many', table: this.table, operation: 'findMany' });
|
|
2674
3390
|
}
|
|
2675
3391
|
/**
|
|
2676
3392
|
* Whether `where` can match at most one row, because it pins every column of
|
|
@@ -2717,7 +3433,7 @@ export class QueryInterface {
|
|
|
2717
3433
|
}
|
|
2718
3434
|
/**
|
|
2719
3435
|
* Recursively measure the maximum depth of a `with` clause tree.
|
|
2720
|
-
* Used by the
|
|
3436
|
+
* Used by the deep-with advice.
|
|
2721
3437
|
*/
|
|
2722
3438
|
measureWithDepth(withClause) {
|
|
2723
3439
|
let maxDepth = 1;
|
|
@@ -2768,7 +3484,7 @@ export class QueryInterface {
|
|
|
2768
3484
|
if (args?.orderBy !== undefined) {
|
|
2769
3485
|
const deduped = dedupeOrderEntries(this.tableMeta, orderByEntries(args.orderBy), this.table);
|
|
2770
3486
|
if (deduped) {
|
|
2771
|
-
warnRedundantSortTerm(this.table, 'orderBy', deduped.dropped);
|
|
3487
|
+
warnRedundantSortTerm(this.advice, this.table, 'orderBy', deduped.dropped);
|
|
2772
3488
|
args = {
|
|
2773
3489
|
...args,
|
|
2774
3490
|
orderBy: deduped.entries.map(([k, v]) => ({ [k]: v })),
|
|
@@ -2778,7 +3494,7 @@ export class QueryInterface {
|
|
|
2778
3494
|
if (args?.distinct && args.distinct.length > 0) {
|
|
2779
3495
|
const deduped = dedupeColumnList(this.tableMeta, args.distinct);
|
|
2780
3496
|
if (deduped) {
|
|
2781
|
-
warnRedundantSortTerm(this.table, 'distinct', deduped.dropped);
|
|
3497
|
+
warnRedundantSortTerm(this.advice, this.table, 'distinct', deduped.dropped);
|
|
2782
3498
|
args = { ...args, distinct: deduped.columns };
|
|
2783
3499
|
}
|
|
2784
3500
|
}
|
|
@@ -2789,29 +3505,40 @@ export class QueryInterface {
|
|
|
2789
3505
|
const implicitOrder = this.implicitPkOrderBy(args);
|
|
2790
3506
|
if (implicitOrder)
|
|
2791
3507
|
args = { ...args, orderBy: implicitOrder };
|
|
3508
|
+
// A `cursor` that names no row (`null`, `{}`) is refused before either
|
|
3509
|
+
// reader below asks whether there is one.
|
|
3510
|
+
assertCursorWritten(this.table, 'findMany', args?.cursor);
|
|
3511
|
+
// A page whose order can tie gets the cursor tiebreaker (completePageOrder),
|
|
3512
|
+
// before fingerprinting, so the cache key carries it.
|
|
3513
|
+
if (args)
|
|
3514
|
+
args = this.completePageOrder(args);
|
|
3515
|
+
// A cursor page is planned (and its orderBy completed to the effective
|
|
3516
|
+
// order) BEFORE fingerprinting, so the key, the SQL and the param collect
|
|
3517
|
+
// all read one plan. See planCursor.
|
|
3518
|
+
let cursorPlan;
|
|
3519
|
+
if (hasCursor(args?.cursor))
|
|
3520
|
+
({ args, plan: cursorPlan } = this.planCursor(args));
|
|
2792
3521
|
// `distinct` compiles to `SELECT DISTINCT ON (...)`, which is PostgreSQL
|
|
2793
3522
|
// syntax and nothing else's. Every other engine parsed it as far as the
|
|
2794
3523
|
// word `ON` and answered with a RAW driver error carrying no TURBINE_ code
|
|
2795
3524
|
// (SQLite: `near "ON": syntax error`; MySQL: ER_PARSE_ERROR), i.e. the one
|
|
2796
3525
|
// shape the typed-error surface exists to prevent. Refused with the same
|
|
2797
|
-
// E017 the OTHER spelling of this
|
|
2798
|
-
// distinctOn })` in aggregates.ts
|
|
2799
|
-
//
|
|
2800
|
-
// `dialect.name` rather than a new capability flag, deliberately, and this
|
|
2801
|
-
// is the exception to the "capability flags, not engine names" rule the
|
|
2802
|
-
// rest of the dialect follows: every engine dialect is built by SPREADING
|
|
2803
|
-
// `postgresDialect`, so a flag defaulting to `true` would be inherited by
|
|
2804
|
-
// all of them (which is the bug) and one defaulting to `false` would have
|
|
2805
|
-
// to be re-declared on Postgres-compatible dialects that already work.
|
|
2806
|
-
// The established precedent for exactly this feature is the `dialect.name`
|
|
2807
|
-
// test in `buildDistinctOnSource`.
|
|
3526
|
+
// E017, on the same `distinctOn` capability, as the OTHER spelling of this
|
|
3527
|
+
// feature, `groupBy({ distinctOn })` in aggregates.ts.
|
|
2808
3528
|
//
|
|
2809
3529
|
// Before the cache, like the two guards below, so a warmed template cannot
|
|
2810
3530
|
// serve the query the cold path refuses.
|
|
2811
|
-
if (args?.distinct && args.distinct.length > 0 && this.dialect.
|
|
3531
|
+
if (args?.distinct && args.distinct.length > 0 && !this.dialect.capabilities.distinctOn) {
|
|
2812
3532
|
throw new UnsupportedFeatureError('DISTINCT ON (findMany distinct)', this.dialect.name, 'findMany({ distinct }) requires PostgreSQL: SELECT DISTINCT ON is not portable. Group in ' +
|
|
2813
3533
|
'application code, or use groupBy({ by }) for one row per combination.');
|
|
2814
3534
|
}
|
|
3535
|
+
// The locking clause, validated and rendered once: the cache key and the
|
|
3536
|
+
// SQL both read this value. PostgreSQL refuses a lock on a DISTINCT query
|
|
3537
|
+
// (0A000), so that pair is refused here, before the cache.
|
|
3538
|
+
const rowLock = resolveRowLock(args?.lock, this.dialect, this.table);
|
|
3539
|
+
if (rowLock && args?.distinct && args.distinct.length > 0) {
|
|
3540
|
+
throw new ValidationError(lockWithDistinctMessage(this.table));
|
|
3541
|
+
}
|
|
2815
3542
|
// `distinct` + relation orderBy is refused up front (E003): the distinct
|
|
2816
3543
|
// path re-orders in an outer wrapper (`... AS "<table>_distinct" ORDER BY
|
|
2817
3544
|
// <userOrder>`) where a correlated relation subquery (pick-row, `_count`,
|
|
@@ -2858,14 +3585,9 @@ export class QueryInterface {
|
|
|
2858
3585
|
})
|
|
2859
3586
|
.join(',')
|
|
2860
3587
|
: '';
|
|
2861
|
-
//
|
|
2862
|
-
//
|
|
2863
|
-
|
|
2864
|
-
const cursorFp = args?.cursor
|
|
2865
|
-
? this.cursorSeekEntries(args.cursor, args.orderBy)
|
|
2866
|
-
.map((e) => `${e.column}:${e.desc ? 'd' : 'a'}`)
|
|
2867
|
-
.join(',')
|
|
2868
|
-
: '';
|
|
3588
|
+
// Mode, keyset order, directions, NULL placement and which values are
|
|
3589
|
+
// NULL all change the predicate's SQL text (see planCursor).
|
|
3590
|
+
const cursorFp = cursorPlan?.fingerprint ?? '';
|
|
2869
3591
|
// distinct must fingerprint in USER order: the SQL emits `DISTINCT ON` in
|
|
2870
3592
|
// the caller's column order, so a permuted array rebuilds different SQL and
|
|
2871
3593
|
// must not collapse onto the same cache entry (would trip the cross-check).
|
|
@@ -2906,7 +3628,9 @@ export class QueryInterface {
|
|
|
2906
3628
|
// non-default one, so the key never depends on what the client default
|
|
2907
3629
|
// happens to be.
|
|
2908
3630
|
const encodingFp = `|je=${jsonEncoding === 'positional' ? 'p' : 'o'}`;
|
|
2909
|
-
|
|
3631
|
+
// Empty without a lock, so every unlocked key is unchanged.
|
|
3632
|
+
const lockFp = rowLock ? `|lk=${rowLock.fp}` : '';
|
|
3633
|
+
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()}`;
|
|
2910
3634
|
const params = [];
|
|
2911
3635
|
const buildSql = (freshParams) => {
|
|
2912
3636
|
// Fresh build: generates SQL and populates freshParams
|
|
@@ -2972,18 +3696,9 @@ export class QueryInterface {
|
|
|
2972
3696
|
// WHERE + cursor conditions accumulate into `tail`, pushing params in
|
|
2973
3697
|
// where → cursor order (the collect path mirrors this exactly).
|
|
2974
3698
|
let tail = freshWhereSql;
|
|
2975
|
-
if (
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
const seek = this.cursorSeekEntries(args.cursor, args.orderBy);
|
|
2979
|
-
if (seek.length > 0) {
|
|
2980
|
-
const terms = seek.map((e) => {
|
|
2981
|
-
freshParams.push(e.value);
|
|
2982
|
-
return { column: `${qt}.${this.q(e.column)}`, placeholder: this.p(freshParams.length), desc: e.desc };
|
|
2983
|
-
});
|
|
2984
|
-
const predicate = this.keysetPredicate(terms);
|
|
2985
|
-
tail += freshWhereSql ? ` AND ${predicate}` : ` WHERE ${predicate}`;
|
|
2986
|
-
}
|
|
3699
|
+
if (cursorPlan) {
|
|
3700
|
+
const predicate = this.renderCursorPredicate(cursorPlan, freshParams);
|
|
3701
|
+
tail += freshWhereSql ? ` AND ${predicate}` : ` WHERE ${predicate}`;
|
|
2987
3702
|
}
|
|
2988
3703
|
// ORDER BY is built AFTER the cursor pushes (param order
|
|
2989
3704
|
// where → with → cursor → orderBy → limit → offset) and BEFORE final
|
|
@@ -3057,6 +3772,9 @@ export class QueryInterface {
|
|
|
3057
3772
|
offsetPh = this.paginationRef(args.offset, freshParams, 'skip/offset');
|
|
3058
3773
|
}
|
|
3059
3774
|
sql += this.buildPagination(limitPh, offsetPh, !!args?.orderBy);
|
|
3775
|
+
// The locking clause follows pagination and binds nothing.
|
|
3776
|
+
if (rowLock)
|
|
3777
|
+
sql += rowLock.sql;
|
|
3060
3778
|
return sql;
|
|
3061
3779
|
};
|
|
3062
3780
|
const entry = this.acquireSql(ck, buildSql);
|
|
@@ -3069,13 +3787,9 @@ export class QueryInterface {
|
|
|
3069
3787
|
if (args?.with) {
|
|
3070
3788
|
this.collectWithParams(args.with, params, undefined, flattenPlan);
|
|
3071
3789
|
}
|
|
3072
|
-
// 3. Cursor params
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
for (const e of this.cursorSeekEntries(args.cursor, args.orderBy)) {
|
|
3076
|
-
params.push(e.value);
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3790
|
+
// 3. Cursor params: the build's own renderer, SQL discarded.
|
|
3791
|
+
if (cursorPlan)
|
|
3792
|
+
this.renderCursorPredicate(cursorPlan, params);
|
|
3079
3793
|
// 4. ORDER BY params (vector KNN ordering binds a `$n::vector` query vector).
|
|
3080
3794
|
// Mirrors buildOrderBy's push order, between cursor and LIMIT.
|
|
3081
3795
|
if (args?.orderBy) {
|
|
@@ -3102,6 +3816,7 @@ export class QueryInterface {
|
|
|
3102
3816
|
transform: (result) => result.rows.map((row) => (parseWith ? parseWith(row) : this.parseRow(row, this.table))),
|
|
3103
3817
|
tag: `${this.table}.findMany`,
|
|
3104
3818
|
preparedName: entry.name,
|
|
3819
|
+
...rowLockMarker(rowLock),
|
|
3105
3820
|
};
|
|
3106
3821
|
}
|
|
3107
3822
|
// -------------------------------------------------------------------------
|
|
@@ -3162,20 +3877,21 @@ export class QueryInterface {
|
|
|
3162
3877
|
* the result set is empty. Callers may therefore treat a yielded batch as
|
|
3163
3878
|
* non-empty.
|
|
3164
3879
|
*/
|
|
3165
|
-
async *streamRaw(args, action) {
|
|
3166
|
-
//
|
|
3167
|
-
//
|
|
3168
|
-
//
|
|
3169
|
-
|
|
3170
|
-
// through.
|
|
3171
|
-
args = normalizePagination(args);
|
|
3880
|
+
async *streamRaw(args, action, idle, unit) {
|
|
3881
|
+
// Canonical args BEFORE the speculative build below, which spreads `args`
|
|
3882
|
+
// and sets its own `limit`: a surviving `take` would reach that spread
|
|
3883
|
+
// beside it and be read as a second value for one bound.
|
|
3884
|
+
args = this.normalizeArgs(args);
|
|
3172
3885
|
const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
|
|
3886
|
+
// Validated on every path, so a bad value is refused whether or not this
|
|
3887
|
+
// stream turns out to need a cursor.
|
|
3888
|
+
const idleTimeoutMs = resolveStreamIdleTimeout(args?.idleTimeoutMs, 'idleTimeoutMs') ?? this.streamIdleTimeoutMs;
|
|
3173
3889
|
this.currentAction = action;
|
|
3174
3890
|
// The two streaming methods do NOT go through `executeWithMiddleware`, so
|
|
3175
3891
|
// the unknown-key diagnostic has to be hung here as well or a stream would
|
|
3176
3892
|
// be the one read that silently drops an `include`. Both public methods
|
|
3177
3893
|
// reach this, and each passes its own name.
|
|
3178
|
-
warnUnknownQueryOptions(this.table, action, args);
|
|
3894
|
+
warnUnknownQueryOptions(this.table, action, args, this.advice ?? null);
|
|
3179
3895
|
// Streaming is ALREADY immune to the generic-plan cliff: the speculative
|
|
3180
3896
|
// fetch has never passed a prepared name, and the cursor path runs through
|
|
3181
3897
|
// DECLARE, so neither statement enters the plan cache. `preparedNameFor` is
|
|
@@ -3212,9 +3928,18 @@ export class QueryInterface {
|
|
|
3212
3928
|
// four round trips on a drain that would have fit, never any transferred
|
|
3213
3929
|
// rows.
|
|
3214
3930
|
if (batchSize <= STREAM_SPECULATION_MAX_BATCH) {
|
|
3931
|
+
// The caller's bound (or the client's `defaultLimit`, exactly as findMany
|
|
3932
|
+
// applies it) caps the speculative fetch. When the bound fits in a batch
|
|
3933
|
+
// the speculative fetch IS the whole answer; otherwise `rows <= batchSize`
|
|
3934
|
+
// still means fewer rows exist than either bound, and an overflow goes to
|
|
3935
|
+
// the cursor, whose statement carries the caller's own LIMIT.
|
|
3936
|
+
const callerLimit = args?.limit ?? this.defaultLimit;
|
|
3937
|
+
const bound = callerLimit === undefined ? undefined : this.paginationValue(callerLimit, 'limit');
|
|
3215
3938
|
const speculativeDeferred = this.buildFindMany({
|
|
3216
3939
|
...args,
|
|
3217
|
-
limit: batchSize + 1,
|
|
3940
|
+
limit: bound !== undefined && bound <= batchSize ? bound : batchSize + 1,
|
|
3941
|
+
// Only the caller's own bound makes this a page (completePageOrder).
|
|
3942
|
+
...firstRowOnly(args),
|
|
3218
3943
|
});
|
|
3219
3944
|
const speculativeResult = await this.readWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout, streamPreparedName);
|
|
3220
3945
|
if (speculativeResult.rows.length <= batchSize) {
|
|
@@ -3238,22 +3963,100 @@ export class QueryInterface {
|
|
|
3238
3963
|
// transaction control of its own (`ambientTransaction`).
|
|
3239
3964
|
const held = this.txScoped ? null : await this.acquireConnection();
|
|
3240
3965
|
const client = held?.client ?? null;
|
|
3241
|
-
const
|
|
3966
|
+
const base = client ?? {
|
|
3242
3967
|
query: async (text, values) => (await this.pool.query(text, values)),
|
|
3243
3968
|
};
|
|
3969
|
+
// The idle guard watches only a stream that holds a connection of its own
|
|
3970
|
+
// between pulls. Inside a caller's transaction the connection is theirs and
|
|
3971
|
+
// their transaction's timeout bounds it. On PostgreSQL the stream's BEGIN
|
|
3972
|
+
// also carries the server-side limit (see streamBackstopStatement), and the
|
|
3973
|
+
// guard keeps a live consumer's transaction from looking idle to it.
|
|
3974
|
+
const watched = held !== null && idleTimeoutMs > 0;
|
|
3975
|
+
const backstop = watched && this.dialect.capabilities.idleTransactionTimeout;
|
|
3976
|
+
let guard;
|
|
3977
|
+
const conn = watched
|
|
3978
|
+
? watchStreamConnection(base, () => guard, backstop
|
|
3979
|
+
? { begin: this.dialect.beginStatement(), statement: streamBackstopStatement(idleTimeoutMs) }
|
|
3980
|
+
: undefined)
|
|
3981
|
+
: base;
|
|
3982
|
+
// The cursor's statement carries the `sqlComment` like any other (the
|
|
3983
|
+
// speculative fetch above got it at the execute seam). Never named: a
|
|
3984
|
+
// cursor is declared by text.
|
|
3985
|
+
const streamSql = this.outgoing(deferred.sql, undefined, action).text;
|
|
3986
|
+
const stream = this.dialect.openStream(conn, streamSql, deferred.params, batchSize, {
|
|
3987
|
+
ambientTransaction: this.txScoped,
|
|
3988
|
+
});
|
|
3989
|
+
if (watched) {
|
|
3990
|
+
guard = new StreamIdleGuard(idle, idleTimeoutMs, () => streamIdleExpiredError(action, this.table, idleTimeoutMs, unit), () => this.expireIdleStream(stream, held, action, idleTimeoutMs, idle, unit), backstop ? () => base.query('SELECT 1') : undefined);
|
|
3991
|
+
}
|
|
3244
3992
|
try {
|
|
3245
|
-
yield
|
|
3246
|
-
|
|
3247
|
-
|
|
3993
|
+
// Driven by hand rather than `yield*`, so an expired stream's cleanup can
|
|
3994
|
+
// run from the timer while this generator is suspended.
|
|
3995
|
+
while (true) {
|
|
3996
|
+
const step = await stream.next();
|
|
3997
|
+
if (step.done)
|
|
3998
|
+
break;
|
|
3999
|
+
yield step.value;
|
|
4000
|
+
if (idle.expired)
|
|
4001
|
+
throw idle.expired;
|
|
4002
|
+
}
|
|
3248
4003
|
}
|
|
3249
4004
|
catch (err) {
|
|
4005
|
+
if (err === idle.expired)
|
|
4006
|
+
throw err;
|
|
3250
4007
|
// Wrap pg constraint errors so streaming surfaces typed errors like the rest of the API
|
|
3251
4008
|
throw explainConnectionLoss(wrapPgError(err), held?.checkout.lostWith);
|
|
3252
4009
|
}
|
|
3253
4010
|
finally {
|
|
3254
|
-
|
|
4011
|
+
guard?.stop();
|
|
4012
|
+
if (guard?.expiry) {
|
|
4013
|
+
// The timer already closed the stream and released the connection;
|
|
4014
|
+
// wait for that so the connection is never handed back mid-cleanup.
|
|
4015
|
+
await guard.expiry;
|
|
4016
|
+
}
|
|
4017
|
+
else {
|
|
4018
|
+
try {
|
|
4019
|
+
// An early exit (`break`) closes the cursor here; after a full drain
|
|
4020
|
+
// or an error the dialect's generator has finished and this is a no-op.
|
|
4021
|
+
await stream.return(undefined);
|
|
4022
|
+
}
|
|
4023
|
+
catch (err) {
|
|
4024
|
+
// Only an early exit can reach this throw: after an error or a full
|
|
4025
|
+
// drain the dialect's generator has finished and return() resolves.
|
|
4026
|
+
// A failed CLOSE / COMMIT on an early exit must reach the caller, as
|
|
4027
|
+
// it did when this loop was a `yield*`.
|
|
4028
|
+
// biome-ignore lint/correctness/noUnsafeFinally: it replaces only an early-exit return, see above
|
|
4029
|
+
throw explainConnectionLoss(wrapPgError(err), held?.checkout.lostWith);
|
|
4030
|
+
}
|
|
4031
|
+
finally {
|
|
4032
|
+
held?.checkout.release();
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
3255
4035
|
}
|
|
3256
4036
|
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Close a stream whose consumer asked for nothing within its idle timeout:
|
|
4039
|
+
* run the dialect's own cleanup (PostgreSQL: CLOSE, COMMIT), release the
|
|
4040
|
+
* connection (with the cleanup's error, if any, so the pool discards it), and
|
|
4041
|
+
* log `stream-idle-timeout`. The log line matters: an abandoned consumer
|
|
4042
|
+
* never pulls again, so it never sees the E002 waiting for it.
|
|
4043
|
+
*/
|
|
4044
|
+
async expireIdleStream(stream, held, action, timeoutMs, idle, unit) {
|
|
4045
|
+
let failure;
|
|
4046
|
+
try {
|
|
4047
|
+
await stream.return(undefined);
|
|
4048
|
+
}
|
|
4049
|
+
catch (err) {
|
|
4050
|
+
failure = err;
|
|
4051
|
+
}
|
|
4052
|
+
finally {
|
|
4053
|
+
held.checkout.release(failure === undefined ? undefined : failure instanceof Error ? failure : true);
|
|
4054
|
+
}
|
|
4055
|
+
logTo(this.logger, 'warn', `${action} on "${this.table}" had no ${unit} requested for ${timeoutMs} ms, so Turbine closed its cursor ` +
|
|
4056
|
+
`and released its connection (${idle.rows} ${idle.rows === 1 ? 'row' : 'rows'} handed out before that). ` +
|
|
4057
|
+
'An iterator that is kept and never finished, or a response stream torn down without calling return(), ' +
|
|
4058
|
+
'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 });
|
|
4059
|
+
}
|
|
3257
4060
|
/**
|
|
3258
4061
|
* Stream rows from a findMany query using PostgreSQL cursors, one BATCH of
|
|
3259
4062
|
* rows at a time.
|
|
@@ -3289,13 +4092,24 @@ export class QueryInterface {
|
|
|
3289
4092
|
* ```
|
|
3290
4093
|
*/
|
|
3291
4094
|
async *findManyStreamBatches(args) {
|
|
4095
|
+
args = this.normalizeArgs(args);
|
|
4096
|
+
this.assertNoStreamLock(args, 'findManyStreamBatches');
|
|
3292
4097
|
const parse = this.makeStreamRowParser(args);
|
|
3293
|
-
|
|
4098
|
+
const idle = newStreamIdleState();
|
|
4099
|
+
for await (const batch of this.streamRaw(args, 'findManyStreamBatches', idle, 'batch')) {
|
|
3294
4100
|
const parsed = new Array(batch.length);
|
|
3295
4101
|
for (let i = 0; i < batch.length; i++) {
|
|
3296
4102
|
parsed[i] = parse(batch[i]);
|
|
3297
4103
|
}
|
|
4104
|
+
idle.rows += parsed.length;
|
|
4105
|
+
// `suspended` and `pulls` are the idle guard's whole view of the
|
|
4106
|
+
// consumer: two writes per yield, no timer work.
|
|
4107
|
+
idle.suspended = true;
|
|
3298
4108
|
yield parsed;
|
|
4109
|
+
idle.suspended = false;
|
|
4110
|
+
idle.pulls++;
|
|
4111
|
+
if (idle.expired)
|
|
4112
|
+
throw idle.expired;
|
|
3299
4113
|
}
|
|
3300
4114
|
}
|
|
3301
4115
|
/**
|
|
@@ -3334,14 +4148,25 @@ export class QueryInterface {
|
|
|
3334
4148
|
* ```
|
|
3335
4149
|
*/
|
|
3336
4150
|
async *findManyStream(args) {
|
|
4151
|
+
args = this.normalizeArgs(args);
|
|
4152
|
+
this.assertNoStreamLock(args, 'findManyStream');
|
|
3337
4153
|
const parse = this.makeStreamRowParser(args);
|
|
3338
4154
|
// Parsed one row at a time, INSIDE the yield loop rather than a batch ahead
|
|
3339
4155
|
// of it, so a consumer that breaks early still pays only for the rows it
|
|
3340
4156
|
// took. That laziness is the reason this loop is not written over
|
|
3341
4157
|
// `findManyStreamBatches`.
|
|
3342
|
-
|
|
4158
|
+
const idle = newStreamIdleState();
|
|
4159
|
+
for await (const batch of this.streamRaw(args, 'findManyStream', idle, 'row')) {
|
|
3343
4160
|
for (let i = 0; i < batch.length; i++) {
|
|
3344
|
-
|
|
4161
|
+
const row = parse(batch[i]);
|
|
4162
|
+
idle.rows++;
|
|
4163
|
+
// See findManyStreamBatches: the idle guard's view of the consumer.
|
|
4164
|
+
idle.suspended = true;
|
|
4165
|
+
yield row;
|
|
4166
|
+
idle.suspended = false;
|
|
4167
|
+
idle.pulls++;
|
|
4168
|
+
if (idle.expired)
|
|
4169
|
+
throw idle.expired;
|
|
3345
4170
|
}
|
|
3346
4171
|
}
|
|
3347
4172
|
}
|
|
@@ -3349,28 +4174,28 @@ export class QueryInterface {
|
|
|
3349
4174
|
// findFirst, like findMany but returns a single row or null
|
|
3350
4175
|
// -------------------------------------------------------------------------
|
|
3351
4176
|
async findFirst(args) {
|
|
3352
|
-
// BEFORE the strategy branch,
|
|
3353
|
-
//
|
|
3354
|
-
//
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
// changes the warning text from the declared name back to the caller's and
|
|
3358
|
-
// nothing else). It is here so the invariant holds at the seam rather than
|
|
3359
|
-
// depending on every consumer to re-establish it.
|
|
3360
|
-
args = this.normalizeArgs(args);
|
|
4177
|
+
// BEFORE the strategy branch, so the batched and auto paths below read the
|
|
4178
|
+
// same canonical args the builders do, and so an all-undefined `where` is
|
|
4179
|
+
// refused before anything runs (see normalizeArgs).
|
|
4180
|
+
args = this.normalizeArgs(args, 'findFirst');
|
|
4181
|
+
this.assertLockInTransaction(args, 'findFirst');
|
|
3361
4182
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
3362
4183
|
if (args?.with) {
|
|
3363
4184
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
3364
4185
|
// findFirst is findMany + LIMIT 1: batch the single base row, then load.
|
|
3365
4186
|
if (strategy === 'batched') {
|
|
3366
|
-
const rows = await this.runFindManyBatched({
|
|
4187
|
+
const rows = await this.runFindManyBatched({
|
|
4188
|
+
...args,
|
|
4189
|
+
limit: 1,
|
|
4190
|
+
...firstRowOnly(args),
|
|
4191
|
+
});
|
|
3367
4192
|
return (rows[0] ?? null);
|
|
3368
4193
|
}
|
|
3369
4194
|
if (strategy === 'auto') {
|
|
3370
4195
|
// findFirst is findMany + LIMIT 1: a one-row parent set.
|
|
3371
4196
|
const split = this.planAuto(args.with, args.stableRelationOrder, false, 1);
|
|
3372
4197
|
if (split) {
|
|
3373
|
-
const rows = (await this.runAutoSplit({ ...args, limit: 1 }, split, false));
|
|
4198
|
+
const rows = (await this.runAutoSplit({ ...args, limit: 1, ...firstRowOnly(args) }, split, false));
|
|
3374
4199
|
return (rows[0] ?? null);
|
|
3375
4200
|
}
|
|
3376
4201
|
}
|
|
@@ -3381,8 +4206,16 @@ export class QueryInterface {
|
|
|
3381
4206
|
});
|
|
3382
4207
|
}
|
|
3383
4208
|
buildFindFirst(args) {
|
|
4209
|
+
return this.buildFindFirstAs(args, 'findFirst');
|
|
4210
|
+
}
|
|
4211
|
+
/**
|
|
4212
|
+
* findFirst / findFirstOrThrow over findMany's builder with `LIMIT 1`.
|
|
4213
|
+
* `operation` names the method in the refusal of an all-undefined `where`.
|
|
4214
|
+
*/
|
|
4215
|
+
buildFindFirstAs(args, operation) {
|
|
4216
|
+
args = this.normalizeArgs(args, operation);
|
|
3384
4217
|
// Reuse findMany's SQL builder but force LIMIT 1
|
|
3385
|
-
const findManyArgs = { ...args, limit: 1 };
|
|
4218
|
+
const findManyArgs = { ...args, limit: 1, ...firstRowOnly(args) };
|
|
3386
4219
|
const deferred = this.buildFindMany(findManyArgs);
|
|
3387
4220
|
return {
|
|
3388
4221
|
sql: deferred.sql,
|
|
@@ -3392,12 +4225,15 @@ export class QueryInterface {
|
|
|
3392
4225
|
return rows.length > 0 ? rows[0] : null;
|
|
3393
4226
|
},
|
|
3394
4227
|
tag: `${this.table}.findFirst`,
|
|
4228
|
+
...(deferred.rowLock ? { rowLock: true } : {}),
|
|
3395
4229
|
};
|
|
3396
4230
|
}
|
|
3397
4231
|
// -------------------------------------------------------------------------
|
|
3398
4232
|
// findFirstOrThrow, like findFirst but throws if no record found
|
|
3399
4233
|
// -------------------------------------------------------------------------
|
|
3400
4234
|
async findFirstOrThrow(args) {
|
|
4235
|
+
args = this.normalizeArgs(args, 'findFirstOrThrow');
|
|
4236
|
+
this.assertLockInTransaction(args, 'findFirstOrThrow');
|
|
3401
4237
|
return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
|
|
3402
4238
|
const deferred = this.buildFindFirstOrThrow(args);
|
|
3403
4239
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3405,7 +4241,8 @@ export class QueryInterface {
|
|
|
3405
4241
|
});
|
|
3406
4242
|
}
|
|
3407
4243
|
buildFindFirstOrThrow(args) {
|
|
3408
|
-
|
|
4244
|
+
args = this.normalizeArgs(args, 'findFirstOrThrow');
|
|
4245
|
+
const inner = this.buildFindFirstAs(args, 'findFirstOrThrow');
|
|
3409
4246
|
return {
|
|
3410
4247
|
sql: inner.sql,
|
|
3411
4248
|
params: inner.params,
|
|
@@ -3421,12 +4258,15 @@ export class QueryInterface {
|
|
|
3421
4258
|
return row;
|
|
3422
4259
|
},
|
|
3423
4260
|
tag: `${this.table}.findFirstOrThrow`,
|
|
4261
|
+
...(inner.rowLock ? { rowLock: true } : {}),
|
|
3424
4262
|
};
|
|
3425
4263
|
}
|
|
3426
4264
|
// -------------------------------------------------------------------------
|
|
3427
4265
|
// findUniqueOrThrow, like findUnique but throws if no record found
|
|
3428
4266
|
// -------------------------------------------------------------------------
|
|
3429
4267
|
async findUniqueOrThrow(args) {
|
|
4268
|
+
args = this.normalizeArgs(args);
|
|
4269
|
+
this.assertLockInTransaction(args, 'findUniqueOrThrow');
|
|
3430
4270
|
return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
|
|
3431
4271
|
const deferred = this.buildFindUniqueOrThrow(args);
|
|
3432
4272
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3434,6 +4274,7 @@ export class QueryInterface {
|
|
|
3434
4274
|
});
|
|
3435
4275
|
}
|
|
3436
4276
|
buildFindUniqueOrThrow(args) {
|
|
4277
|
+
args = this.normalizeArgs(args);
|
|
3437
4278
|
const inner = this.buildFindUnique(args);
|
|
3438
4279
|
return {
|
|
3439
4280
|
sql: inner.sql,
|
|
@@ -3450,12 +4291,14 @@ export class QueryInterface {
|
|
|
3450
4291
|
return row;
|
|
3451
4292
|
},
|
|
3452
4293
|
tag: `${this.table}.findUniqueOrThrow`,
|
|
4294
|
+
...(inner.rowLock ? { rowLock: true } : {}),
|
|
3453
4295
|
};
|
|
3454
4296
|
}
|
|
3455
4297
|
// -------------------------------------------------------------------------
|
|
3456
4298
|
// create
|
|
3457
4299
|
// -------------------------------------------------------------------------
|
|
3458
4300
|
async create(args) {
|
|
4301
|
+
args = this.normalizeArgs(args);
|
|
3459
4302
|
return this.executeWithMiddleware('create', args, async () => {
|
|
3460
4303
|
if (hasRelationFields(args.data, this.tableMeta)) {
|
|
3461
4304
|
return this.nestedCreate(args);
|
|
@@ -3468,16 +4311,55 @@ export class QueryInterface {
|
|
|
3468
4311
|
// createMany, uses UNNEST for performance
|
|
3469
4312
|
// -------------------------------------------------------------------------
|
|
3470
4313
|
async createMany(args) {
|
|
4314
|
+
args = this.normalizeArgs(args);
|
|
3471
4315
|
return this.executeWithMiddleware('createMany', args, async () => {
|
|
3472
|
-
const
|
|
3473
|
-
const
|
|
3474
|
-
|
|
4316
|
+
const projection = this.resolveWriteProjection(args);
|
|
4317
|
+
const base = args;
|
|
4318
|
+
// One statement when the rows fit the engine's per-statement limits,
|
|
4319
|
+
// else chunks that do, all in one transaction so the rows land together
|
|
4320
|
+
// or not at all. (`buildCreateMany`, one statement for a pipeline,
|
|
4321
|
+
// refuses instead.)
|
|
4322
|
+
const { chunks, rowsPerChunk } = writesMod.planCreateMany(this.ctx, base, projection);
|
|
4323
|
+
const run = async () => {
|
|
4324
|
+
// An engine that applies two rows whose onConflict keys are equal only
|
|
4325
|
+
// under the column's collation (SQLite's NOCASE) as an insert and an
|
|
4326
|
+
// update of one row, in one statement, is caught by the transform (the
|
|
4327
|
+
// row comes back twice), so the statement runs in a transaction that
|
|
4328
|
+
// refusal rolls back. A reselect write already runs in one.
|
|
4329
|
+
const appliesRepeatedKeys = base.onConflict !== undefined &&
|
|
4330
|
+
this.dialect.capabilities.bulkUpsertAppliesRepeatedKeys &&
|
|
4331
|
+
this.dialect.resultStrategy !== 'reselect';
|
|
4332
|
+
if (chunks.length === 1 && !appliesRepeatedKeys) {
|
|
4333
|
+
return this.executeMutation(chunks[0], args.timeout);
|
|
4334
|
+
}
|
|
4335
|
+
const reselects = this.dialect.resultStrategy === 'reselect';
|
|
4336
|
+
return this.inOneTransaction(async (exec) => {
|
|
4337
|
+
const rows = [];
|
|
4338
|
+
for (const chunk of chunks) {
|
|
4339
|
+
const result = reselects && chunk.reselect
|
|
4340
|
+
? await chunk.reselect(exec)
|
|
4341
|
+
: await exec(chunk.sql, chunk.params, chunk.preparedName);
|
|
4342
|
+
rows.push(...chunk.transform(result));
|
|
4343
|
+
}
|
|
4344
|
+
return rows;
|
|
4345
|
+
}, 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 ` +
|
|
4346
|
+
'out a connection.');
|
|
4347
|
+
};
|
|
4348
|
+
if (base.onConflict === undefined)
|
|
4349
|
+
return run();
|
|
4350
|
+
try {
|
|
4351
|
+
return await run();
|
|
4352
|
+
}
|
|
4353
|
+
catch (err) {
|
|
4354
|
+
throw writesMod.explainConflictKeyCollision(this.ctx, base, err);
|
|
4355
|
+
}
|
|
3475
4356
|
});
|
|
3476
4357
|
}
|
|
3477
4358
|
// -------------------------------------------------------------------------
|
|
3478
4359
|
// update
|
|
3479
4360
|
// -------------------------------------------------------------------------
|
|
3480
4361
|
async update(args) {
|
|
4362
|
+
args = this.normalizeArgs(args);
|
|
3481
4363
|
return this.executeWithMiddleware('update', args, async () => {
|
|
3482
4364
|
if (hasRelationFields(args.data, this.tableMeta)) {
|
|
3483
4365
|
return this.nestedUpdate(args);
|
|
@@ -3590,6 +4472,7 @@ export class QueryInterface {
|
|
|
3590
4472
|
// delete
|
|
3591
4473
|
// -------------------------------------------------------------------------
|
|
3592
4474
|
async delete(args) {
|
|
4475
|
+
args = this.normalizeArgs(args);
|
|
3593
4476
|
return this.executeWithMiddleware('delete', args, async () => {
|
|
3594
4477
|
const deferred = this.buildDelete(args);
|
|
3595
4478
|
return this.executeMutation(deferred, args.timeout);
|
|
@@ -3599,7 +4482,11 @@ export class QueryInterface {
|
|
|
3599
4482
|
// upsert, INSERT ... ON CONFLICT ... DO UPDATE
|
|
3600
4483
|
// -------------------------------------------------------------------------
|
|
3601
4484
|
async upsert(args) {
|
|
4485
|
+
args = this.normalizeArgs(args);
|
|
3602
4486
|
return this.executeWithMiddleware('upsert', args, async () => {
|
|
4487
|
+
// First: the nested-write branch below decides before upsertLookupReason
|
|
4488
|
+
// would have refused it.
|
|
4489
|
+
writesMod.assertNoOptimisticLock(this.ctx, args, 'upsert');
|
|
3603
4490
|
if (this.upsertNeedsLookup(args)) {
|
|
3604
4491
|
return this.upsertLookupFirst(args);
|
|
3605
4492
|
}
|
|
@@ -3614,6 +4501,7 @@ export class QueryInterface {
|
|
|
3614
4501
|
* an upsert or has to run the array inside a transaction.
|
|
3615
4502
|
*/
|
|
3616
4503
|
upsertNeedsLookup(args) {
|
|
4504
|
+
args = this.normalizeArgs(args);
|
|
3617
4505
|
return (hasRelationFields((args.create ?? {}), this.tableMeta) ||
|
|
3618
4506
|
hasRelationFields((args.update ?? {}), this.tableMeta) ||
|
|
3619
4507
|
writesMod.upsertLookupReason(this.ctx, args) !== null);
|
|
@@ -3642,7 +4530,11 @@ export class QueryInterface {
|
|
|
3642
4530
|
async upsertLookupFirst(args) {
|
|
3643
4531
|
const where = (args.where ?? {});
|
|
3644
4532
|
const create = (args.create ?? {});
|
|
3645
|
-
|
|
4533
|
+
// `expr.excluded(field)` in `update` reads the incoming row, which here is
|
|
4534
|
+
// `create`: resolved to create's value before the update compiles, as the
|
|
4535
|
+
// single-statement path does.
|
|
4536
|
+
const update = (resolveExcludedFromCreate(this.ctx, (args.update ?? {}), create) ??
|
|
4537
|
+
{});
|
|
3646
4538
|
assertMutationWhereIdentifiesOneRow(this.tableMeta, this.table, expandCompoundUniqueWhere(this.tableMeta, where), 'upsert');
|
|
3647
4539
|
this.resolveWriteProjection(args);
|
|
3648
4540
|
// An undefined option is an absent one on every call below.
|
|
@@ -3676,6 +4568,7 @@ export class QueryInterface {
|
|
|
3676
4568
|
// updateMany, UPDATE ... WHERE ... returning count
|
|
3677
4569
|
// -------------------------------------------------------------------------
|
|
3678
4570
|
async updateMany(args) {
|
|
4571
|
+
args = this.normalizeArgs(args);
|
|
3679
4572
|
return this.executeWithMiddleware('updateMany', args, async () => {
|
|
3680
4573
|
const deferred = this.buildUpdateMany(args);
|
|
3681
4574
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
|
|
@@ -3686,6 +4579,7 @@ export class QueryInterface {
|
|
|
3686
4579
|
// deleteMany, DELETE ... WHERE ... returning count
|
|
3687
4580
|
// -------------------------------------------------------------------------
|
|
3688
4581
|
async deleteMany(args) {
|
|
4582
|
+
args = this.normalizeArgs(args);
|
|
3689
4583
|
return this.executeWithMiddleware('deleteMany', args, async () => {
|
|
3690
4584
|
const deferred = this.buildDeleteMany(args);
|
|
3691
4585
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
|
|
@@ -3696,6 +4590,7 @@ export class QueryInterface {
|
|
|
3696
4590
|
// count
|
|
3697
4591
|
// -------------------------------------------------------------------------
|
|
3698
4592
|
async count(args) {
|
|
4593
|
+
args = this.normalizeArgs(args);
|
|
3699
4594
|
return this.executeWithMiddleware('count', (args ?? {}), async () => {
|
|
3700
4595
|
const deferred = this.buildCount(args);
|
|
3701
4596
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3703,6 +4598,7 @@ export class QueryInterface {
|
|
|
3703
4598
|
});
|
|
3704
4599
|
}
|
|
3705
4600
|
buildCount(args) {
|
|
4601
|
+
args = this.normalizeArgs(args);
|
|
3706
4602
|
this.currentSkip = resolveSkipGlobalFilters(args?.skipGlobalFilters);
|
|
3707
4603
|
const effWhere = this.mergeGlobalFilter(args?.where);
|
|
3708
4604
|
const hasWhere = effWhere !== undefined;
|
|
@@ -3740,6 +4636,7 @@ export class QueryInterface {
|
|
|
3740
4636
|
* that cannot be typed, so those columns are not projected onto the row type.
|
|
3741
4637
|
*/
|
|
3742
4638
|
async groupBy(args) {
|
|
4639
|
+
args = this.normalizeArgs(args);
|
|
3743
4640
|
return this.executeWithMiddleware('groupBy', args, async () => {
|
|
3744
4641
|
const deferred = this.buildGroupBy(args);
|
|
3745
4642
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3750,15 +4647,16 @@ export class QueryInterface {
|
|
|
3750
4647
|
// Aggregate / groupBy compilation (extracted to aggregates.ts).
|
|
3751
4648
|
// ---------------------------------------------------------------------------
|
|
3752
4649
|
buildGroupBy(args) {
|
|
3753
|
-
return aggMod.buildGroupBy(this.ctx, args);
|
|
4650
|
+
return aggMod.buildGroupBy(this.ctx, this.normalizeArgs(args));
|
|
3754
4651
|
}
|
|
3755
4652
|
buildAggregate(args) {
|
|
3756
|
-
return aggMod.buildAggregate(this.ctx, args);
|
|
4653
|
+
return aggMod.buildAggregate(this.ctx, this.normalizeArgs(args));
|
|
3757
4654
|
}
|
|
3758
4655
|
// -------------------------------------------------------------------------
|
|
3759
4656
|
// aggregate, standalone aggregation without groupBy
|
|
3760
4657
|
// -------------------------------------------------------------------------
|
|
3761
4658
|
async aggregate(args) {
|
|
4659
|
+
args = this.normalizeArgs(args);
|
|
3762
4660
|
return this.executeWithMiddleware('aggregate', args, async () => {
|
|
3763
4661
|
const deferred = this.buildAggregate(args);
|
|
3764
4662
|
const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -3876,14 +4774,15 @@ export class QueryInterface {
|
|
|
3876
4774
|
}
|
|
3877
4775
|
return relationsMod.planFlattenWith(this.ctx, this.table, withClause, includePii);
|
|
3878
4776
|
}
|
|
3879
|
-
/**
|
|
4777
|
+
/** Advice, once per reason: `'flatten'` was refused for the whole query. */
|
|
3880
4778
|
warnFlattenBlocked(reason) {
|
|
3881
|
-
|
|
4779
|
+
const log = this.advice;
|
|
4780
|
+
if (!log)
|
|
3882
4781
|
return;
|
|
3883
4782
|
if (!shouldWarnOnce(WARN_NS.flattenFallback, `${this.table}|query|${reason}`))
|
|
3884
4783
|
return;
|
|
3885
|
-
|
|
3886
|
-
'Every relation loads via the correlated subquery instead (same rows, same values, different plan).');
|
|
4784
|
+
logTo(log, 'warn', `relationLoadStrategy: 'flatten' did not engage on "${this.table}": ${reason}. ` +
|
|
4785
|
+
'Every relation loads via the correlated subquery instead (same rows, same values, different plan).', { code: 'flatten-blocked', table: this.table, reason });
|
|
3887
4786
|
}
|
|
3888
4787
|
/**
|
|
3889
4788
|
* Convert a field name to its snake_case column name (unquoted, for non-SQL
|
|
@@ -4018,8 +4917,8 @@ export class QueryInterface {
|
|
|
4018
4917
|
// To-many relation orderBy (`{ posts: { _count } }`) uses the same count
|
|
4019
4918
|
// subquery as `_count`: mirror its global-filter params. Pick-row
|
|
4020
4919
|
// ordering mirrors its full param chain (by-path / global filter /
|
|
4021
|
-
// pick.where / pick.orderBy paths). To-one relation orderBy
|
|
4022
|
-
//
|
|
4920
|
+
// pick.where / pick.orderBy paths). To-one relation orderBy is mirrored
|
|
4921
|
+
// by running its build (see collectToOneRelationOrderParams).
|
|
4023
4922
|
if (this.isRelationOrderByValue(dir)) {
|
|
4024
4923
|
// Mirrors the build path's resolution, so a cache HIT binds params for
|
|
4025
4924
|
// the same relation the cached SQL was built from.
|
|
@@ -4032,9 +4931,7 @@ export class QueryInterface {
|
|
|
4032
4931
|
this.collectRelationCountParams(relDef, params);
|
|
4033
4932
|
}
|
|
4034
4933
|
else if (relDef) {
|
|
4035
|
-
|
|
4036
|
-
this.collectTargetGlobalFilterAlias(relDef.to, params);
|
|
4037
|
-
}
|
|
4934
|
+
relationsMod.collectToOneRelationOrderParams(this.ctx, key, dir, params);
|
|
4038
4935
|
}
|
|
4039
4936
|
}
|
|
4040
4937
|
}
|
|
@@ -4106,10 +5003,10 @@ export class QueryInterface {
|
|
|
4106
5003
|
* one, is the acknowledgement, and the warning exists to surface an
|
|
4107
5004
|
* unacknowledged trade rather than to nag.
|
|
4108
5005
|
*
|
|
4109
|
-
* NOT
|
|
4110
|
-
*
|
|
4111
|
-
*
|
|
4112
|
-
*
|
|
5006
|
+
* NOT ADVICE: it fires whatever `diagnostics` says, deliberately. Production
|
|
5007
|
+
* is exactly where a destructive write commits and where the row cannot be
|
|
5008
|
+
* recovered afterwards, so a warning that goes quiet in production mode is
|
|
5009
|
+
* silent in the only place it matters. The cost is
|
|
4113
5010
|
* bounded to the point of irrelevance: once per process per field, and only
|
|
4114
5011
|
* on a row that actually held an infinity.
|
|
4115
5012
|
*
|
|
@@ -4122,7 +5019,7 @@ export class QueryInterface {
|
|
|
4122
5019
|
return;
|
|
4123
5020
|
if (!shouldWarnOnce(WARN_NS.temporalInfinity, `${table}.${field}`))
|
|
4124
5021
|
return;
|
|
4125
|
-
|
|
5022
|
+
logTo(this.logger, 'warn', `${table}.${field} holds the Postgres value \`infinity\` (or \`-infinity\`). JavaScript has ` +
|
|
4126
5023
|
'no Date for it, so it reads as the JS number `Infinity` / `-Infinity` on a field the generated ' +
|
|
4127
5024
|
'types declare as `Date`: `.toISOString()` and `.getTime()` throw a TypeError on these rows, and ' +
|
|
4128
5025
|
'`JSON.stringify` still renders them null because JSON has no infinity literal. The number is the ' +
|
|
@@ -4132,7 +5029,7 @@ export class QueryInterface {
|
|
|
4132
5029
|
"cannot tell infinity, -infinity and NULL apart. Set `temporalInfinity: 'preserve'` to confirm this " +
|
|
4133
5030
|
"reading and silence the warning, or `'null'` to read null instead, accepting that a stored " +
|
|
4134
5031
|
'infinity then looks exactly like a stored NULL and a read-modify-write over a nullable column ' +
|
|
4135
|
-
'stores SQL NULL, destroying the value with no error.');
|
|
5032
|
+
'stores SQL NULL, destroying the value with no error.', { code: 'temporal-infinity', table, field });
|
|
4136
5033
|
}
|
|
4137
5034
|
/**
|
|
4138
5035
|
* Apply the configured reading to the infinity elements of a temporal ARRAY
|
|
@@ -4212,6 +5109,64 @@ export class QueryInterface {
|
|
|
4212
5109
|
// UTC so results don't depend on the server's time zone.
|
|
4213
5110
|
return this.utcTimestamps ? parseDbDate(String(value)) : new Date(value);
|
|
4214
5111
|
}
|
|
5112
|
+
/** The non-temporal decode kinds of `table`'s columns, by column and by field name. */
|
|
5113
|
+
getCellKinds(table, meta) {
|
|
5114
|
+
let kinds = this.cellKindCache.get(table);
|
|
5115
|
+
if (!kinds) {
|
|
5116
|
+
kinds = new Map();
|
|
5117
|
+
for (const column of meta.columns) {
|
|
5118
|
+
// Metadata may carry the type on the column or only in the table map.
|
|
5119
|
+
const pgType = column.pgType ?? meta.pgTypes?.[column.name];
|
|
5120
|
+
if (typeof pgType !== 'string')
|
|
5121
|
+
continue;
|
|
5122
|
+
let kind = cellKindOf(pgType, this.schema.enums);
|
|
5123
|
+
const decode = kind === CELL_PLAIN ? this.dialect.decodeColumnValue?.(pgType) : undefined;
|
|
5124
|
+
if (decode !== undefined) {
|
|
5125
|
+
kind = CELL_DIALECT;
|
|
5126
|
+
let decoders = this.dialectDecodeCache.get(table);
|
|
5127
|
+
if (!decoders) {
|
|
5128
|
+
decoders = new Map();
|
|
5129
|
+
this.dialectDecodeCache.set(table, decoders);
|
|
5130
|
+
}
|
|
5131
|
+
decoders.set(column.name, decode);
|
|
5132
|
+
decoders.set(column.field, decode);
|
|
5133
|
+
}
|
|
5134
|
+
if (kind === CELL_PLAIN)
|
|
5135
|
+
continue;
|
|
5136
|
+
kinds.set(column.name, kind);
|
|
5137
|
+
kinds.set(column.field, kind);
|
|
5138
|
+
}
|
|
5139
|
+
this.cellKindCache.set(table, kinds);
|
|
5140
|
+
}
|
|
5141
|
+
return kinds;
|
|
5142
|
+
}
|
|
5143
|
+
/** One cell's decode, for every kind but {@link CELL_PLAIN}. */
|
|
5144
|
+
decodeCell(kind, value, table, field) {
|
|
5145
|
+
switch (kind) {
|
|
5146
|
+
case CELL_TEMPORAL:
|
|
5147
|
+
return this.decodeTemporalCell(value, table, field);
|
|
5148
|
+
case CELL_FLOAT:
|
|
5149
|
+
return readJsonFloat(value);
|
|
5150
|
+
case CELL_FLOAT_ARRAY:
|
|
5151
|
+
return readJsonFloatArray(value);
|
|
5152
|
+
case CELL_TEXT_ARRAY:
|
|
5153
|
+
return readTextArray(value);
|
|
5154
|
+
case CELL_DIALECT:
|
|
5155
|
+
return this.decodeDialectCell(value, table, field);
|
|
5156
|
+
default:
|
|
5157
|
+
return value;
|
|
5158
|
+
}
|
|
5159
|
+
}
|
|
5160
|
+
/**
|
|
5161
|
+
* A {@link CELL_DIALECT} cell: the dialect's decode for the column's declared
|
|
5162
|
+
* type, recorded by {@link getCellKinds} when it classified the column.
|
|
5163
|
+
*/
|
|
5164
|
+
decodeDialectCell(value, table, field) {
|
|
5165
|
+
if (value === null || value === undefined)
|
|
5166
|
+
return value;
|
|
5167
|
+
const decode = this.dialectDecodeCache.get(table)?.get(field);
|
|
5168
|
+
return decode ? decode(value) : value;
|
|
5169
|
+
}
|
|
4215
5170
|
/**
|
|
4216
5171
|
* Build the decode plan for one exact column list, and remember it as this
|
|
4217
5172
|
* table's most recent shape so the next row of the same result set hits the
|
|
@@ -4235,16 +5190,19 @@ export class QueryInterface {
|
|
|
4235
5190
|
// camelCase-keyed date fields, so nested json_build_object rows (whose
|
|
4236
5191
|
// keys are already camelCase) get the same Date coercion as top-level rows.
|
|
4237
5192
|
const camelDateFields = this.getCamelDateFields(table, meta);
|
|
5193
|
+
const cellKinds = this.getCellKinds(table, meta);
|
|
4238
5194
|
const cols = keys.slice();
|
|
4239
5195
|
const fields = [];
|
|
4240
|
-
const
|
|
5196
|
+
const kinds = [];
|
|
4241
5197
|
for (const col of cols) {
|
|
4242
5198
|
const field = reverseMap[col] ?? col; // fall back to raw col name, not regex
|
|
4243
5199
|
fields.push(field);
|
|
4244
5200
|
// Top-level rows are snake_case (dateCols); nested rows are camelCase.
|
|
4245
|
-
|
|
5201
|
+
kinds.push(dateCols.has(col) || camelDateFields.has(field)
|
|
5202
|
+
? CELL_TEMPORAL
|
|
5203
|
+
: (cellKinds.get(col) ?? cellKinds.get(field) ?? CELL_PLAIN));
|
|
4246
5204
|
}
|
|
4247
|
-
plan = { cols, fields,
|
|
5205
|
+
plan = { cols, fields, kinds };
|
|
4248
5206
|
this.rowPlanCache.set(shapeKey, plan);
|
|
4249
5207
|
}
|
|
4250
5208
|
this.rowPlanLast.set(table, plan);
|
|
@@ -4288,11 +5246,16 @@ export class QueryInterface {
|
|
|
4288
5246
|
const plan = last !== undefined && QueryInterface.rowPlanMatches(last, keys)
|
|
4289
5247
|
? last
|
|
4290
5248
|
: this.buildRowDecodePlan(table, meta, keys);
|
|
4291
|
-
const { cols, fields,
|
|
5249
|
+
const { cols, fields, kinds } = plan;
|
|
4292
5250
|
for (let i = 0; i < cols.length; i++) {
|
|
4293
5251
|
const field = fields[i];
|
|
4294
5252
|
const value = row[cols[i]];
|
|
4295
|
-
|
|
5253
|
+
const kind = kinds[i];
|
|
5254
|
+
// A float from the driver is already a number; only JSON's strings need reading.
|
|
5255
|
+
parsed[field] =
|
|
5256
|
+
kind === CELL_PLAIN || (kind === CELL_FLOAT && typeof value !== 'string')
|
|
5257
|
+
? value
|
|
5258
|
+
: this.decodeCell(kind, value, table, field);
|
|
4296
5259
|
}
|
|
4297
5260
|
}
|
|
4298
5261
|
else {
|