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/relations.js
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
* reached through the ctx. See builder.ts for the thin delegating methods and
|
|
13
13
|
* the findMany/findUnique execute assembly.
|
|
14
14
|
*/
|
|
15
|
-
import { CircularRelationError, RelationError, UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
15
|
+
import { CircularRelationError, InvariantError, RelationError, UnsupportedFeatureError, ValidationError, } from '../errors.js';
|
|
16
16
|
import { missingIndexForRelation } from '../index-advisor.js';
|
|
17
|
+
import { logTo } from '../logger.js';
|
|
17
18
|
import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
18
|
-
import { resolveCountRelations } from './batched-loader.js';
|
|
19
|
-
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, MAX_NAMED_ORDER_KEYS, normalizeOrderBy, orderByEntries, sortedEntries, } from './filters.js';
|
|
19
|
+
import { resolveCountRelations, stitchOnlyFields } from './batched-loader.js';
|
|
20
|
+
import { describeOperand, isJsonPathOrderBy, isOrderBySpec, isPlainRecord, isRelationPickOrderBy, isVectorOrderBy, MAX_NAMED_ORDER_KEYS, normalizeOrderBy, orderByEntries, sortedEntries, } from './filters.js';
|
|
20
21
|
import { assertDirectionToken, assertOrderDirection } from './types.js';
|
|
21
|
-
import { availableClause, canonicalColumnOrder, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelation, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, unknownFieldMessage, } from './utils.js';
|
|
22
|
+
import { availableClause, canonicalColumnOrder, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelation, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, suggestKey, unknownFieldMessage, } from './utils.js';
|
|
22
23
|
import { hasWarnedOnce, shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
23
24
|
import * as whereMod from './where.js';
|
|
24
25
|
import * as writesMod from './writes.js';
|
|
@@ -45,6 +46,19 @@ function projectionColumn(table, meta, field, clause) {
|
|
|
45
46
|
throw new ValidationError(relationInProjectionMessage(table, field, clause));
|
|
46
47
|
throw new ValidationError(unknownFieldMessage(table, field, meta));
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The E003 text for a PII-tagged column named in `select` without
|
|
51
|
+
* `includePii: UNSAFE`. Shared by every projection path through
|
|
52
|
+
* {@link resolveProjection}, so a top-level read, a `with` relation (join,
|
|
53
|
+
* batched, flatten and positional alike) and a single-row write refuse in the
|
|
54
|
+
* same words. The write path is named because it cannot pass the opt-in at all.
|
|
55
|
+
*/
|
|
56
|
+
export function piiSelectRefusedMessage(table, field) {
|
|
57
|
+
return (`\`select\` on "${table}" names "${field}", which is tagged PII. A PII column is returned only by a read that ` +
|
|
58
|
+
'passes `includePii: UNSAFE` (import { UNSAFE } from "turbine-orm"); naming it in `select` does not unlock it, ' +
|
|
59
|
+
'because `select` can arrive in request data and must not be able to. A write never returns a PII column, so ' +
|
|
60
|
+
`read the row back with \`includePii: UNSAFE\`, or remove "${field}" from \`select\`.`);
|
|
61
|
+
}
|
|
48
62
|
/**
|
|
49
63
|
* Resolve `select` / `omit` into a list of snake_case column names, for the
|
|
50
64
|
* query's own table and for a relation target alike. `null` means "no
|
|
@@ -114,8 +128,15 @@ export function resolveProjection(qi, table, meta, select, omit, includePii) {
|
|
|
114
128
|
if (Array.isArray(select)) {
|
|
115
129
|
throw new ValidationError(`"select" must be an object mapping field names to true ` + `(e.g. { id: true, name: true }), not an array.`);
|
|
116
130
|
}
|
|
117
|
-
// Only include columns where value is true.
|
|
118
|
-
//
|
|
131
|
+
// Only include columns where value is true.
|
|
132
|
+
//
|
|
133
|
+
// A PII-tagged column named here needs `includePii: UNSAFE`, exactly like
|
|
134
|
+
// the default-projection path below. `select` is ordinary request data (a
|
|
135
|
+
// parsed query string is a `select` object), and the `UNSAFE` symbol is the
|
|
136
|
+
// one thing a request cannot produce, so letting a bare `select: { ssn: true }`
|
|
137
|
+
// unlock the column would make the privilege option decorative. The one
|
|
138
|
+
// exemption is a key the batched loader added to stitch rows together
|
|
139
|
+
// (STITCH_ONLY_FIELDS), which it strips before the caller sees any entity.
|
|
119
140
|
//
|
|
120
141
|
// Emitted in the TABLE's column order, not the caller's key order, which is
|
|
121
142
|
// the same order the `omit` and default branches below produce. Two reasons,
|
|
@@ -143,9 +164,17 @@ export function resolveProjection(qi, table, meta, select, omit, includePii) {
|
|
|
143
164
|
// keys are all derived from it, so they move together. The one observable
|
|
144
165
|
// consequence is that a projected row's KEY INSERTION order now follows the
|
|
145
166
|
// table rather than the `select` literal.
|
|
167
|
+
const piiCols = includePii ? undefined : writesMod.piiColumns(qi, meta);
|
|
168
|
+
const stitchOnly = piiCols && piiCols.size > 0 ? stitchOnlyFields(select) : undefined;
|
|
146
169
|
return canonicalColumnOrder(meta, Object.entries(select)
|
|
147
170
|
.filter(([, v]) => v)
|
|
148
|
-
.map(([k]) =>
|
|
171
|
+
.map(([k]) => {
|
|
172
|
+
const column = projectionColumn(table, meta, k, 'select');
|
|
173
|
+
if (piiCols?.has(column) && !stitchOnly?.has(k)) {
|
|
174
|
+
throw new ValidationError(piiSelectRefusedMessage(table, meta.reverseColumnMap[column] ?? column));
|
|
175
|
+
}
|
|
176
|
+
return column;
|
|
177
|
+
}));
|
|
149
178
|
}
|
|
150
179
|
// Default / omit-only projection: PII-tagged columns are excluded unless the
|
|
151
180
|
// caller opted in with `includePii: UNSAFE`. An empty set (untagged schema) keeps the
|
|
@@ -214,6 +243,88 @@ const EMPTY_WITH_OPTIONS = Object.freeze({});
|
|
|
214
243
|
export function relationOptions(spec) {
|
|
215
244
|
return spec === true ? EMPTY_WITH_OPTIONS : spec;
|
|
216
245
|
}
|
|
246
|
+
/** The options a relation in `with` reads; any other key is ignored by every relation strategy. */
|
|
247
|
+
const WITH_OPTION_KEYS = new Set([
|
|
248
|
+
'with',
|
|
249
|
+
'where',
|
|
250
|
+
'orderBy',
|
|
251
|
+
'limit',
|
|
252
|
+
'offset',
|
|
253
|
+
'take',
|
|
254
|
+
'skip',
|
|
255
|
+
'select',
|
|
256
|
+
'omit',
|
|
257
|
+
]);
|
|
258
|
+
/**
|
|
259
|
+
* Check one relation's `with` options. The fingerprint walk reads every level
|
|
260
|
+
* of every `with` on every call, whichever strategy then loads it, so this is
|
|
261
|
+
* the one place that sees them all. `lock` is refused: the row lock compiles to
|
|
262
|
+
* `FOR <mode> OF "<table>"` on the query's own table, so a lock written on a
|
|
263
|
+
* relation locked nothing and said nothing. Any other key no relation reads
|
|
264
|
+
* (`wehre`, `cursor`, `distinct`) is named once in the advice log.
|
|
265
|
+
*/
|
|
266
|
+
function checkWithOptions(qi, table, relName, opts) {
|
|
267
|
+
for (const key in opts) {
|
|
268
|
+
if (WITH_OPTION_KEYS.has(key))
|
|
269
|
+
continue;
|
|
270
|
+
const value = opts[key];
|
|
271
|
+
if (value === undefined)
|
|
272
|
+
continue;
|
|
273
|
+
if (key === 'lock') {
|
|
274
|
+
throw new ValidationError(`\`lock\` is not an option of a relation in \`with\` (\`${relName}\` on "${table}"): a row lock applies to ` +
|
|
275
|
+
"the rows of the query's own table only, so the related rows would not be locked. Lock them with a " +
|
|
276
|
+
'locking read on their own table, in the same transaction.');
|
|
277
|
+
}
|
|
278
|
+
const advice = whereMod.adviceLogger(qi);
|
|
279
|
+
if (!advice || !shouldWarnOnce(WARN_NS.unknownQueryOption, `${table}.with.${relName}.${key}`))
|
|
280
|
+
continue;
|
|
281
|
+
const suggestion = suggestKey(key, WITH_OPTION_KEYS);
|
|
282
|
+
logTo(advice, 'warn', `unknown option "${key}" on relation "${relName}" in \`with\` on ${table}, it is ignored.` +
|
|
283
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : ''), { code: 'unknown-query-option', table, operation: 'with', key });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* `_count` in `with` takes `true` (every relation) or an object naming
|
|
288
|
+
* relations with `true`. Anything else under a relation, `{ where }` in
|
|
289
|
+
* particular, was read as `true` and counted every related row.
|
|
290
|
+
*/
|
|
291
|
+
function assertWithCount(table, count) {
|
|
292
|
+
if (count === true)
|
|
293
|
+
return;
|
|
294
|
+
if (!isPlainRecord(count)) {
|
|
295
|
+
throw new ValidationError(`\`_count\` in \`with\` on "${table}" takes true or an object naming relations, for example ` +
|
|
296
|
+
`\`_count: { posts: true }\`; received ${describeOperand(count)}.`);
|
|
297
|
+
}
|
|
298
|
+
for (const rel of Object.keys(count)) {
|
|
299
|
+
const value = count[rel];
|
|
300
|
+
if (value === undefined || typeof value === 'boolean')
|
|
301
|
+
continue;
|
|
302
|
+
throw new ValidationError(`\`_count.${rel}\` in \`with\` on "${table}" takes true; received ${describeOperand(value)}. ` +
|
|
303
|
+
'`_count` counts every related row. To count only the rows that match a filter, use ' +
|
|
304
|
+
`\`count({ where })\` on the related table.`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* A to-many relation's `offset` (`skip` is folded into it at the args seam by
|
|
309
|
+
* `normalizeWithPagination`, which also refuses it on a to-one relation).
|
|
310
|
+
*/
|
|
311
|
+
export function relationOffset(opts) {
|
|
312
|
+
return opts.offset;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The LIMIT / OFFSET tail of a to-many relation's inner row query. Binds the
|
|
316
|
+
* limit, then the offset, the order {@link collectRelationSubqueryParams}
|
|
317
|
+
* mirrors. Without an offset the tail is the plain ` LIMIT $n` it has always
|
|
318
|
+
* been; with one it goes through the dialect's pagination hook, which knows
|
|
319
|
+
* how to write an offset with no limit.
|
|
320
|
+
*/
|
|
321
|
+
function relationPagination(qi, limit, offset, params, hasOrder) {
|
|
322
|
+
const limitRef = limit === undefined ? undefined : qi.paginationRef(limit, params, 'relation limit');
|
|
323
|
+
if (offset === undefined)
|
|
324
|
+
return limitRef === undefined ? '' : ` LIMIT ${limitRef}`;
|
|
325
|
+
const offsetRef = qi.paginationRef(offset, params, 'relation offset');
|
|
326
|
+
return qi.buildPagination(limitRef, offsetRef, hasOrder);
|
|
327
|
+
}
|
|
217
328
|
/**
|
|
218
329
|
* Produce a fingerprint for a `with` clause tree. Recursion mirrors
|
|
219
330
|
* buildSelectWithRelations / buildRelationSubquery.
|
|
@@ -235,6 +346,7 @@ export function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
235
346
|
// Reserved `_count` key, fingerprint by the selected relation set so
|
|
236
347
|
// `_count: true` and `_count: { posts: true }` never share a cache entry.
|
|
237
348
|
if (relName === '_count') {
|
|
349
|
+
assertWithCount(meta.name, spec);
|
|
238
350
|
const c = spec;
|
|
239
351
|
parts.push(c === true
|
|
240
352
|
? '_count(*)'
|
|
@@ -251,6 +363,8 @@ export function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
251
363
|
continue;
|
|
252
364
|
}
|
|
253
365
|
const opts = relationOptions(spec);
|
|
366
|
+
if (opts !== EMPTY_WITH_OPTIONS)
|
|
367
|
+
checkWithOptions(qi, meta.name, relName, opts);
|
|
254
368
|
const subParts = [];
|
|
255
369
|
// select/omit shape
|
|
256
370
|
if (opts.select) {
|
|
@@ -285,6 +399,10 @@ export function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
285
399
|
if (opts.limit !== undefined) {
|
|
286
400
|
subParts.push(qi.dialect.inlineLimitOffset ? `l=${opts.limit}` : 'l=1');
|
|
287
401
|
}
|
|
402
|
+
const offset = relationOffset(opts);
|
|
403
|
+
if (offset !== undefined) {
|
|
404
|
+
subParts.push(qi.dialect.inlineLimitOffset ? `off=${offset}` : 'off=1');
|
|
405
|
+
}
|
|
288
406
|
// nested with (recurse)
|
|
289
407
|
if (opts.with) {
|
|
290
408
|
const nested = withFingerprint(qi, opts.with, relDef.to, depth + 1);
|
|
@@ -357,6 +475,10 @@ export function collectRelationSubqueryParams(qi, relDef, spec, params, _parentR
|
|
|
357
475
|
if (opts.limit !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
358
476
|
params.push(qi.paginationValue(opts.limit, 'relation limit'));
|
|
359
477
|
}
|
|
478
|
+
const m2mOffset = relationOffset(opts);
|
|
479
|
+
if (m2mOffset !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
480
|
+
params.push(qi.paginationValue(m2mOffset, 'relation offset'));
|
|
481
|
+
}
|
|
360
482
|
if (opts.with) {
|
|
361
483
|
for (const [nestedRelName, nestedSpec] of sortedEntries(opts.with)) {
|
|
362
484
|
const nestedRelDef = ownLookup(targetMeta.relations, nestedRelName);
|
|
@@ -370,7 +492,8 @@ export function collectRelationSubqueryParams(qi, relDef, spec, params, _parentR
|
|
|
370
492
|
// Mirrors buildRelationSubquery's willWrap: `orderBy: {}` is treated as absent.
|
|
371
493
|
const relOrderEntries = opts.orderBy ? orderByEntries(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
372
494
|
const hasOrder = relOrderEntries.length > 0;
|
|
373
|
-
const
|
|
495
|
+
const offset = relationOffset(opts);
|
|
496
|
+
const willWrap = relDef.type === 'hasMany' && (opts.limit !== undefined || offset !== undefined || hasOrder);
|
|
374
497
|
// Non-wrapped path: nested relations BEFORE where/limit
|
|
375
498
|
if (!willWrap && opts.with) {
|
|
376
499
|
for (const [nestedRelName, nestedSpec] of sortedEntries(opts.with)) {
|
|
@@ -400,6 +523,9 @@ export function collectRelationSubqueryParams(qi, relDef, spec, params, _parentR
|
|
|
400
523
|
if (relDef.type === 'hasMany' && opts.limit !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
401
524
|
params.push(qi.paginationValue(opts.limit, 'relation limit'));
|
|
402
525
|
}
|
|
526
|
+
if (relDef.type === 'hasMany' && offset !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
527
|
+
params.push(qi.paginationValue(offset, 'relation offset'));
|
|
528
|
+
}
|
|
403
529
|
// Wrapped path: nested relations AFTER where/limit (inside inner subquery)
|
|
404
530
|
if (willWrap && opts.with) {
|
|
405
531
|
for (const [nestedRelName, nestedSpec] of sortedEntries(opts.with)) {
|
|
@@ -557,14 +683,14 @@ export function isRelationOrderByValue(_qi, value) {
|
|
|
557
683
|
}
|
|
558
684
|
/**
|
|
559
685
|
* Render the ` NULLS FIRST` / ` NULLS LAST` suffix for a column ordering.
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
* {@link UnsupportedFeatureError} (E017) instead of broken SQL.
|
|
686
|
+
* On an engine without the `NULLS FIRST/LAST` grammar (`nullsOrderingClause`:
|
|
687
|
+
* PostgreSQL and SQLite have it) a caller asking for explicit nulls placement
|
|
688
|
+
* gets a clear {@link UnsupportedFeatureError} (E017) instead of broken SQL.
|
|
563
689
|
*/
|
|
564
690
|
export function nullsSuffix(qi, nulls) {
|
|
565
691
|
if (!nulls)
|
|
566
692
|
return '';
|
|
567
|
-
if (qi.dialect.
|
|
693
|
+
if (!qi.dialect.capabilities.nullsOrderingClause) {
|
|
568
694
|
throw new UnsupportedFeatureError('NULLS FIRST/LAST ordering', qi.dialect.name, 'Explicit nulls placement in orderBy is only available on PostgreSQL and SQLite.');
|
|
569
695
|
}
|
|
570
696
|
return nulls === 'first' ? ' NULLS FIRST' : ' NULLS LAST';
|
|
@@ -638,7 +764,7 @@ export function buildJsonPathOrderEntry(qi, table, meta, field, spec, prefix, pa
|
|
|
638
764
|
// the grammar gate matches nullsSuffix.
|
|
639
765
|
const nullsSql = spec.nulls
|
|
640
766
|
? nullsSuffix(qi, spec.nulls)
|
|
641
|
-
: qi.dialect.
|
|
767
|
+
: qi.dialect.capabilities.nullsOrderingClause
|
|
642
768
|
? ' NULLS LAST'
|
|
643
769
|
: '';
|
|
644
770
|
return `${lhs} ${dir}${nullsSql}`;
|
|
@@ -847,6 +973,18 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
847
973
|
})
|
|
848
974
|
.join(', ');
|
|
849
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Param-collect mirror of a TO-ONE relation orderBy entry, done by running
|
|
978
|
+
* {@link buildRelationOrderBy} and discarding its SQL. The build pushes the
|
|
979
|
+
* target's global filter once per ordered column and, for a chained
|
|
980
|
+
* `{ author: { org: { name } } }` ordering, once more per hop table, in hop
|
|
981
|
+
* order. A hand-written mirror that pushed one filter per column bound too few
|
|
982
|
+
* params (and in the wrong order) as soon as a hop table had a filter, so this
|
|
983
|
+
* mirror is the build itself and cannot drift from it.
|
|
984
|
+
*/
|
|
985
|
+
export function collectToOneRelationOrderParams(qi, relName, value, params, ctx) {
|
|
986
|
+
buildRelationOrderBy(qi, relName, value, 'ord0', params, ctx);
|
|
987
|
+
}
|
|
850
988
|
/**
|
|
851
989
|
* Validate a {@link RelationPickOrderBy} entry's scope and shape. Shared by
|
|
852
990
|
* the SQL-build path ({@link buildRelationPickOrderBy}) and the cache-hit
|
|
@@ -946,7 +1084,7 @@ export function buildRelationPickOrderBy(qi, relName, relDef, spec, alias, paren
|
|
|
946
1084
|
// the caller set `nulls` explicitly; the grammar gate matches nullsSuffix.
|
|
947
1085
|
const nullsSql = spec.nulls
|
|
948
1086
|
? nullsSuffix(qi, spec.nulls)
|
|
949
|
-
: qi.dialect.
|
|
1087
|
+
: qi.dialect.capabilities.nullsOrderingClause
|
|
950
1088
|
? ' NULLS LAST'
|
|
951
1089
|
: '';
|
|
952
1090
|
// Lateral plan: splice a `LEFT JOIN LATERAL (... LIMIT 1) ON true` into the
|
|
@@ -1109,14 +1247,18 @@ export function collectRelationOrderParams(qi, targetTable, targetMeta, orderEnt
|
|
|
1109
1247
|
if (isRelationPickOrderBy(dirValue)) {
|
|
1110
1248
|
throw pickOrderNestedError(qi, key);
|
|
1111
1249
|
}
|
|
1112
|
-
|
|
1250
|
+
// resolveRelationDef, as buildRelationOrderBy resolves it, so a relation
|
|
1251
|
+
// written in its table spelling binds its params on a cache hit too.
|
|
1252
|
+
const relDef = resolveRelationDef(targetMeta.relations, key);
|
|
1113
1253
|
if (relDef && (relDef.type === 'hasMany' || relDef.type === 'manyToMany')) {
|
|
1114
1254
|
collectRelationCountParams(qi, relDef, params);
|
|
1115
1255
|
}
|
|
1116
1256
|
else if (relDef) {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1257
|
+
collectToOneRelationOrderParams(qi, key, dirValue, params, {
|
|
1258
|
+
meta: targetMeta,
|
|
1259
|
+
table: targetTable,
|
|
1260
|
+
parentRef: targetTable,
|
|
1261
|
+
});
|
|
1120
1262
|
}
|
|
1121
1263
|
continue;
|
|
1122
1264
|
}
|
|
@@ -1171,26 +1313,27 @@ export function buildRelationCountExpr(qi, relDef, parentRef, alias, params) {
|
|
|
1171
1313
|
* {@link collectManyToManyTargetGlobalFilter}.
|
|
1172
1314
|
*/
|
|
1173
1315
|
export function manyToManyTargetGlobalFilterExists(qi, relDef, alias, jalias, params) {
|
|
1174
|
-
const
|
|
1175
|
-
if (!
|
|
1316
|
+
const through = relDef.through;
|
|
1317
|
+
if (!through)
|
|
1176
1318
|
return '';
|
|
1177
1319
|
const tMeta = qi.schema.tables[relDef.to];
|
|
1178
1320
|
if (!tMeta || tMeta.primaryKey.length === 0)
|
|
1179
1321
|
return '';
|
|
1180
|
-
const
|
|
1181
|
-
const targetKeys = normalizeKeyColumns(relDef.through.targetKey);
|
|
1322
|
+
const targetKeys = normalizeKeyColumns(through.targetKey);
|
|
1182
1323
|
const pk = tMeta.primaryKey;
|
|
1183
1324
|
if (targetKeys.length !== pk.length)
|
|
1184
1325
|
return '';
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1326
|
+
return whereMod.withTargetGlobalFilter(qi, relDef.to, '', (gf) => {
|
|
1327
|
+
const talias = `${alias}t`;
|
|
1328
|
+
const join = targetKeys.map((jc, i) => `${talias}.${qi.q(pk[i])} = ${jalias}.${qi.q(jc)}`).join(' AND ');
|
|
1329
|
+
const gfClause = whereMod.buildAliasWhere(qi, relDef.to, tMeta, talias, gf, params);
|
|
1330
|
+
const gfAnd = gfClause ? ` AND ${gfClause}` : '';
|
|
1331
|
+
return `EXISTS (SELECT 1 FROM ${qi.q(relDef.to)} ${talias} WHERE ${join}${gfAnd})`;
|
|
1332
|
+
});
|
|
1189
1333
|
}
|
|
1190
1334
|
/** Param-collect mirror of {@link manyToManyTargetGlobalFilterExists}. */
|
|
1191
1335
|
export function collectManyToManyTargetGlobalFilter(qi, relDef, params) {
|
|
1192
|
-
|
|
1193
|
-
if (!gf || !relDef.through)
|
|
1336
|
+
if (!relDef.through)
|
|
1194
1337
|
return;
|
|
1195
1338
|
const tMeta = qi.schema.tables[relDef.to];
|
|
1196
1339
|
if (!tMeta || tMeta.primaryKey.length === 0)
|
|
@@ -1198,7 +1341,7 @@ export function collectManyToManyTargetGlobalFilter(qi, relDef, params) {
|
|
|
1198
1341
|
const targetKeys = normalizeKeyColumns(relDef.through.targetKey);
|
|
1199
1342
|
if (targetKeys.length !== tMeta.primaryKey.length)
|
|
1200
1343
|
return;
|
|
1201
|
-
whereMod.collectAliasWhereParams(qi, relDef.to, tMeta, gf, params);
|
|
1344
|
+
whereMod.withTargetGlobalFilter(qi, relDef.to, false, (gf) => whereMod.collectAliasWhereParams(qi, relDef.to, tMeta, gf, params));
|
|
1202
1345
|
}
|
|
1203
1346
|
/**
|
|
1204
1347
|
* Param-collect mirror of {@link buildRelationCountExpr}'s global-filter
|
|
@@ -1333,13 +1476,15 @@ export function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1333
1476
|
// find it.
|
|
1334
1477
|
//
|
|
1335
1478
|
// The warn is gated and deduped because this sits in parseNestedRow's
|
|
1336
|
-
// per-relation loop: one 10,000-row page was 10,000
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1479
|
+
// per-relation loop: one 10,000-row page was 10,000 warnings. The
|
|
1480
|
+
// parse error stays out of the entry: its message quotes the payload,
|
|
1481
|
+
// which is row data.
|
|
1482
|
+
const log = whereMod.adviceLogger(qi);
|
|
1483
|
+
if (log && shouldWarnOnce(WARN_NS.relationParseFailure, `${qi.table}.${relName}`)) {
|
|
1484
|
+
logTo(log, 'warn', `Relation "${relName}" on "${qi.table}" returned a payload that is not valid JSON. ` +
|
|
1485
|
+
`This is a bug; please report it with the emitted SQL.`, { code: 'relation-payload-json', table: qi.table, relation: relName });
|
|
1341
1486
|
}
|
|
1342
|
-
throw new
|
|
1487
|
+
throw new InvariantError('relation-payload-json', `Relation "${relName}" on table "${qi.table}" returned a payload that could not be parsed as JSON.`, { cause: err });
|
|
1343
1488
|
}
|
|
1344
1489
|
}
|
|
1345
1490
|
else if (Array.isArray(rawValue)) {
|
|
@@ -1834,7 +1979,7 @@ function flattenNodeSignature(node) {
|
|
|
1834
1979
|
return `${node.relName}@${node.alias}(${node.cols.map(([col]) => col).join(',')})${slots}`;
|
|
1835
1980
|
}
|
|
1836
1981
|
/**
|
|
1837
|
-
*
|
|
1982
|
+
* Per-query advice, once per (table, relation, reason): an explicitly
|
|
1838
1983
|
* requested `relationLoadStrategy: 'flatten'` did NOT engage for a relation.
|
|
1839
1984
|
*
|
|
1840
1985
|
* Worth its own warning because the fallback is otherwise INVISIBLE: the query
|
|
@@ -1843,21 +1988,23 @@ function flattenNodeSignature(node) {
|
|
|
1843
1988
|
* Silence there is indistinguishable from the strategy being a no-op, which is
|
|
1844
1989
|
* exactly the shape of thing that gets filed as a bug.
|
|
1845
1990
|
*
|
|
1846
|
-
*
|
|
1847
|
-
* the shared registry so a hot query logs once rather than per
|
|
1848
|
-
* a flatten plan is recomputed on the build, param-collect and
|
|
1849
|
-
* paths for the same query; the registry is what keeps that to
|
|
1991
|
+
* Off when `diagnostics` turn advice off, like the other planner notes, and
|
|
1992
|
+
* deduped through the shared registry so a hot query logs once rather than per
|
|
1993
|
+
* call. Note that a flatten plan is recomputed on the build, param-collect and
|
|
1994
|
+
* row-assembly paths for the same query; the registry is what keeps that to
|
|
1995
|
+
* one line.
|
|
1850
1996
|
*/
|
|
1851
|
-
function warnFlattenFallback(table, rejects) {
|
|
1997
|
+
function warnFlattenFallback(qi, table, rejects) {
|
|
1852
1998
|
if (rejects.length === 0)
|
|
1853
1999
|
return;
|
|
1854
|
-
|
|
2000
|
+
const log = whereMod.adviceLogger(qi);
|
|
2001
|
+
if (!log)
|
|
1855
2002
|
return;
|
|
1856
2003
|
for (const { relation, reason } of rejects) {
|
|
1857
2004
|
if (!shouldWarnOnce(WARN_NS.flattenFallback, `${table}.${relation}|${reason}`))
|
|
1858
2005
|
continue;
|
|
1859
|
-
|
|
1860
|
-
`${reason}. It loads via the correlated subquery instead (same rows, same values, different plan)
|
|
2006
|
+
logTo(log, 'warn', `relationLoadStrategy: 'flatten' did not engage for relation "${relation}" on "${table}": ` +
|
|
2007
|
+
`${reason}. It loads via the correlated subquery instead (same rows, same values, different plan).`, { code: 'flatten-fallback', table, relation, reason });
|
|
1861
2008
|
}
|
|
1862
2009
|
}
|
|
1863
2010
|
/**
|
|
@@ -1888,7 +2035,7 @@ export function planFlattenWith(qi, table, withClause, includePii) {
|
|
|
1888
2035
|
if (node)
|
|
1889
2036
|
nodes[relName] = node;
|
|
1890
2037
|
}
|
|
1891
|
-
warnFlattenFallback(table, rejects);
|
|
2038
|
+
warnFlattenFallback(qi, table, rejects);
|
|
1892
2039
|
if (Object.keys(nodes).length === 0)
|
|
1893
2040
|
return null;
|
|
1894
2041
|
// Flat rows carry the root table's own columns, one column per non-flattened
|
|
@@ -1898,7 +2045,7 @@ export function planFlattenWith(qi, table, withClause, includePii) {
|
|
|
1898
2045
|
const reserved = new Set([...meta.allColumns, ...Object.keys(withClause)]);
|
|
1899
2046
|
for (const alias of aliases) {
|
|
1900
2047
|
if (reserved.has(alias)) {
|
|
1901
|
-
warnFlattenFallback(table, [
|
|
2048
|
+
warnFlattenFallback(qi, table, [
|
|
1902
2049
|
{
|
|
1903
2050
|
relation: Object.keys(nodes).join(', '),
|
|
1904
2051
|
reason: `the projected alias "${alias}" collides with a column or relation name on "${table}"`,
|
|
@@ -2171,11 +2318,12 @@ export function buildSelectWithRelations(qi, table, withClause, params, columnsL
|
|
|
2171
2318
|
const meta = qi.schema.tables[table];
|
|
2172
2319
|
if (!meta)
|
|
2173
2320
|
throw new ValidationError(`Unknown table "${table}"`);
|
|
2174
|
-
// Positional JSON encoding
|
|
2175
|
-
//
|
|
2176
|
-
//
|
|
2177
|
-
//
|
|
2178
|
-
|
|
2321
|
+
// Positional JSON encoding needs `positionalJson`, the flag the default in
|
|
2322
|
+
// builder.ts reads too, so the default never picks an encoding refused here.
|
|
2323
|
+
// Gate here, the single entry point for every `with` clause, so no engine
|
|
2324
|
+
// ever emits the json_build_array shape its dialect can't produce (and
|
|
2325
|
+
// mssql's FOR JSON override path is never reached with positional active).
|
|
2326
|
+
if (qi.jsonEncoding === 'positional' && !qi.dialect.capabilities.positionalJson) {
|
|
2179
2327
|
throw new UnsupportedFeatureError("jsonEncoding: 'positional'", qi.dialect.name, 'Positional relation encoding is only available on PostgreSQL in this version.');
|
|
2180
2328
|
}
|
|
2181
2329
|
const cols = columnsList ?? meta.allColumns;
|
|
@@ -2329,12 +2477,13 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
2329
2477
|
// The raw `spec` is still what crosses the dialect seam below, since
|
|
2330
2478
|
// RelationSubqueryContext is a published contract.
|
|
2331
2479
|
const opts = relationOptions(spec);
|
|
2332
|
-
//
|
|
2333
|
-
// row, so a missing FK index multiplies into per-parent full-table
|
|
2334
|
-
// batched-loader ORM pays the same missing index only once, which is
|
|
2335
|
-
// schemas migrated from one often lack these). Name the exact index to
|
|
2336
|
-
// instead of letting the slowness look like an ORM problem.
|
|
2337
|
-
|
|
2480
|
+
// Per-query advice: correlated relation loading probes the child table once
|
|
2481
|
+
// per parent row, so a missing FK index multiplies into per-parent full-table
|
|
2482
|
+
// scans (a batched-loader ORM pays the same missing index only once, which is
|
|
2483
|
+
// why schemas migrated from one often lack these). Name the exact index to
|
|
2484
|
+
// create instead of letting the slowness look like an ORM problem.
|
|
2485
|
+
const log = whereMod.adviceLogger(qi);
|
|
2486
|
+
if (log) {
|
|
2338
2487
|
const warnKey = `${relDef.from}.${relDef.name}`;
|
|
2339
2488
|
// Compute the (potentially costly) probe only for a key not yet warned, and
|
|
2340
2489
|
// claim the key through the process-wide registry ONLY when we actually warn,
|
|
@@ -2343,10 +2492,16 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
2343
2492
|
if (!hasWarnedOnce(WARN_NS.unindexedRelation, warnKey)) {
|
|
2344
2493
|
const miss = missingIndexForRelation(qi.schema, relDef);
|
|
2345
2494
|
if (miss && shouldWarnOnce(WARN_NS.unindexedRelation, warnKey)) {
|
|
2346
|
-
|
|
2495
|
+
logTo(log, 'warn', `Relation "${relDef.name}" on "${relDef.from}" probes ` +
|
|
2347
2496
|
`"${miss.table}"(${miss.columns.join(', ')}) which has no covering index, ` +
|
|
2348
2497
|
`each parent row scans the full table. Fix: ${miss.createSql}; ` +
|
|
2349
|
-
'or run `npx turbine doctor` for a full report.'
|
|
2498
|
+
'or run `npx turbine doctor` for a full report.', {
|
|
2499
|
+
code: 'unindexed-relation',
|
|
2500
|
+
table: relDef.from,
|
|
2501
|
+
relation: relDef.name,
|
|
2502
|
+
probeTable: miss.table,
|
|
2503
|
+
columns: miss.columns,
|
|
2504
|
+
});
|
|
2350
2505
|
}
|
|
2351
2506
|
}
|
|
2352
2507
|
}
|
|
@@ -2416,7 +2571,8 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
2416
2571
|
// it must neither trigger the wrap (dropping nested relations) nor render a
|
|
2417
2572
|
// dangling `ORDER BY `. `limit: 0` is meaningful (LIMIT 0) and DOES wrap.
|
|
2418
2573
|
const relOrderEntries = opts.orderBy ? orderByEntries(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
2419
|
-
const
|
|
2574
|
+
const offset = relationOffset(opts);
|
|
2575
|
+
const willWrap = relDef.type === 'hasMany' && (opts.limit !== undefined || offset !== undefined || relOrderEntries.length > 0);
|
|
2420
2576
|
// manyToMany takes a dedicated JOIN-through-junction path. Nested relations,
|
|
2421
2577
|
// where, orderBy, and select/omit are handled there (the target alias is the
|
|
2422
2578
|
// row source, exactly like hasMany), so short-circuit before the hasMany logic.
|
|
@@ -2484,8 +2640,8 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
2484
2640
|
// (and shifts every later placeholder by one). To-one relations ignore limit.
|
|
2485
2641
|
// `limit: 0` is honored (LIMIT 0 → empty array), so check !== undefined.
|
|
2486
2642
|
let limitClause = '';
|
|
2487
|
-
if (relDef.type === 'hasMany'
|
|
2488
|
-
limitClause =
|
|
2643
|
+
if (relDef.type === 'hasMany') {
|
|
2644
|
+
limitClause = relationPagination(qi, opts.limit, offset, params, relOrderEntries.length > 0);
|
|
2489
2645
|
}
|
|
2490
2646
|
if (relDef.type === 'hasMany') {
|
|
2491
2647
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -2603,11 +2759,8 @@ export function buildManyToManySubquery(qi, relDef, spec, params, parentRef, ali
|
|
|
2603
2759
|
const gfExtra = whereMod.targetGlobalFilterAlias(qi, targetTable, talias, params);
|
|
2604
2760
|
if (gfExtra)
|
|
2605
2761
|
whereClause += ` AND ${gfExtra}`;
|
|
2606
|
-
// LIMIT, `limit: 0` is honored (LIMIT 0 → empty array)
|
|
2607
|
-
|
|
2608
|
-
if (opts.limit !== undefined) {
|
|
2609
|
-
limitClause = ` LIMIT ${qi.paginationRef(opts.limit, params, 'relation limit')}`;
|
|
2610
|
-
}
|
|
2762
|
+
// LIMIT / OFFSET, `limit: 0` is honored (LIMIT 0 → empty array)
|
|
2763
|
+
const limitClause = relationPagination(qi, opts.limit, relationOffset(opts), params, relOrderEntries.length > 0);
|
|
2611
2764
|
const fromJoin = `FROM ${qTarget} ${talias} JOIN ${qJunction} ${jalias} ON ${joinOn}`;
|
|
2612
2765
|
// When LIMIT or ORDER BY is present, wrap the joined rows in an inner subquery
|
|
2613
2766
|
// so the LIMIT applies to rows BEFORE aggregation (same approach as hasMany).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm: the `lock` read option (`SELECT ... FOR UPDATE OF "t"`).
|
|
3
|
+
*
|
|
4
|
+
* {@link resolveRowLock} is the one place a `lock` value is validated and
|
|
5
|
+
* rendered. The SQL build and the cache key both read its result, so the two
|
|
6
|
+
* cannot disagree about which lock a statement carries.
|
|
7
|
+
*
|
|
8
|
+
* The clause always names the queried table (`OF "t"`). That keeps the lock on
|
|
9
|
+
* the returned rows of that table and nothing else, on every relation
|
|
10
|
+
* strategy: correlated `with` subqueries are never locked anyway, and without
|
|
11
|
+
* `OF` PostgreSQL refuses the `flatten` and lateral-pick plans (their outer
|
|
12
|
+
* joins would put a table on the nullable side of the lock).
|
|
13
|
+
*/
|
|
14
|
+
import type { Dialect } from '../dialect.js';
|
|
15
|
+
/** A validated `lock`: the clause to append after pagination, and its cache-key segment. */
|
|
16
|
+
export interface ResolvedRowLock {
|
|
17
|
+
/** e.g. ` FOR UPDATE OF "jobs" SKIP LOCKED` */
|
|
18
|
+
readonly sql: string;
|
|
19
|
+
/** e.g. `u.sl`; one value per distinct clause, never empty. */
|
|
20
|
+
readonly fp: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate `lock` for a read on `table` and render its clause.
|
|
24
|
+
*
|
|
25
|
+
* `undefined` means no lock. Anything that is not a mode string or a
|
|
26
|
+
* `{ mode, noWait?, skipLocked? }` object is `ValidationError` (E003), and so
|
|
27
|
+
* is asking for both wait policies at once. A mode the dialect does not list in
|
|
28
|
+
* {@link Dialect.rowLockModes} is `UnsupportedFeatureError` (E017).
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveRowLock(lock: unknown, dialect: Dialect, table: string): ResolvedRowLock | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* The refusal for a locking read outside a transaction. In autocommit the lock
|
|
33
|
+
* is released when the statement ends, before the caller sees the rows, so a
|
|
34
|
+
* `skipLocked` queue claim outside a transaction claims nothing, silently.
|
|
35
|
+
*/
|
|
36
|
+
export declare function lockOutsideTransactionMessage(table: string, operation: string): string;
|
|
37
|
+
/** The refusal for `lock` on a streaming read. */
|
|
38
|
+
export declare function lockOnStreamMessage(table: string, operation: string): string;
|
|
39
|
+
/** The refusal for `lock` together with `distinct`. */
|
|
40
|
+
export declare function lockWithDistinctMessage(table: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Refuse a locking read in a pipeline that runs each statement in autocommit
|
|
43
|
+
* (`pipeline(..., { transactional: false })`). A transactional pipeline, and
|
|
44
|
+
* `$transaction([...])`, keep the lock for the rest of the batch.
|
|
45
|
+
*/
|
|
46
|
+
export declare function assertNoLockInAutocommitPipeline(queries: readonly {
|
|
47
|
+
rowLock?: true;
|
|
48
|
+
tag?: string;
|
|
49
|
+
}[]): void;
|