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
|
@@ -47,9 +47,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
47
47
|
};
|
|
48
48
|
})();
|
|
49
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.piiSelectRefusedMessage = piiSelectRefusedMessage;
|
|
50
51
|
exports.resolveProjection = resolveProjection;
|
|
51
52
|
exports.resolveColumns = resolveColumns;
|
|
52
53
|
exports.relationOptions = relationOptions;
|
|
54
|
+
exports.relationOffset = relationOffset;
|
|
53
55
|
exports.withFingerprint = withFingerprint;
|
|
54
56
|
exports.collectWithParams = collectWithParams;
|
|
55
57
|
exports.collectRelationSubqueryParams = collectRelationSubqueryParams;
|
|
@@ -61,6 +63,7 @@ exports.resolveOrderByColumn = resolveOrderByColumn;
|
|
|
61
63
|
exports.validateJsonPathOrderBy = validateJsonPathOrderBy;
|
|
62
64
|
exports.buildJsonPathOrderEntry = buildJsonPathOrderEntry;
|
|
63
65
|
exports.buildRelationOrderBy = buildRelationOrderBy;
|
|
66
|
+
exports.collectToOneRelationOrderParams = collectToOneRelationOrderParams;
|
|
64
67
|
exports.pickOrderNestedError = pickOrderNestedError;
|
|
65
68
|
exports.validatePickOrderBy = validatePickOrderBy;
|
|
66
69
|
exports.buildRelationPickOrderBy = buildRelationPickOrderBy;
|
|
@@ -94,6 +97,7 @@ exports.buildRelationSubquery = buildRelationSubquery;
|
|
|
94
97
|
exports.buildManyToManySubquery = buildManyToManySubquery;
|
|
95
98
|
const errors_js_1 = require("../errors.js");
|
|
96
99
|
const index_advisor_js_1 = require("../index-advisor.js");
|
|
100
|
+
const logger_js_1 = require("../logger.js");
|
|
97
101
|
const schema_js_1 = require("../schema.js");
|
|
98
102
|
const batched_loader_js_1 = require("./batched-loader.js");
|
|
99
103
|
const filters_js_1 = require("./filters.js");
|
|
@@ -125,6 +129,19 @@ function projectionColumn(table, meta, field, clause) {
|
|
|
125
129
|
throw new errors_js_1.ValidationError((0, utils_js_1.relationInProjectionMessage)(table, field, clause));
|
|
126
130
|
throw new errors_js_1.ValidationError((0, utils_js_1.unknownFieldMessage)(table, field, meta));
|
|
127
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* The E003 text for a PII-tagged column named in `select` without
|
|
134
|
+
* `includePii: UNSAFE`. Shared by every projection path through
|
|
135
|
+
* {@link resolveProjection}, so a top-level read, a `with` relation (join,
|
|
136
|
+
* batched, flatten and positional alike) and a single-row write refuse in the
|
|
137
|
+
* same words. The write path is named because it cannot pass the opt-in at all.
|
|
138
|
+
*/
|
|
139
|
+
function piiSelectRefusedMessage(table, field) {
|
|
140
|
+
return (`\`select\` on "${table}" names "${field}", which is tagged PII. A PII column is returned only by a read that ` +
|
|
141
|
+
'passes `includePii: UNSAFE` (import { UNSAFE } from "turbine-orm"); naming it in `select` does not unlock it, ' +
|
|
142
|
+
'because `select` can arrive in request data and must not be able to. A write never returns a PII column, so ' +
|
|
143
|
+
`read the row back with \`includePii: UNSAFE\`, or remove "${field}" from \`select\`.`);
|
|
144
|
+
}
|
|
128
145
|
/**
|
|
129
146
|
* Resolve `select` / `omit` into a list of snake_case column names, for the
|
|
130
147
|
* query's own table and for a relation target alike. `null` means "no
|
|
@@ -194,8 +211,15 @@ function resolveProjection(qi, table, meta, select, omit, includePii) {
|
|
|
194
211
|
if (Array.isArray(select)) {
|
|
195
212
|
throw new errors_js_1.ValidationError(`"select" must be an object mapping field names to true ` + `(e.g. { id: true, name: true }), not an array.`);
|
|
196
213
|
}
|
|
197
|
-
// Only include columns where value is true.
|
|
198
|
-
//
|
|
214
|
+
// Only include columns where value is true.
|
|
215
|
+
//
|
|
216
|
+
// A PII-tagged column named here needs `includePii: UNSAFE`, exactly like
|
|
217
|
+
// the default-projection path below. `select` is ordinary request data (a
|
|
218
|
+
// parsed query string is a `select` object), and the `UNSAFE` symbol is the
|
|
219
|
+
// one thing a request cannot produce, so letting a bare `select: { ssn: true }`
|
|
220
|
+
// unlock the column would make the privilege option decorative. The one
|
|
221
|
+
// exemption is a key the batched loader added to stitch rows together
|
|
222
|
+
// (STITCH_ONLY_FIELDS), which it strips before the caller sees any entity.
|
|
199
223
|
//
|
|
200
224
|
// Emitted in the TABLE's column order, not the caller's key order, which is
|
|
201
225
|
// the same order the `omit` and default branches below produce. Two reasons,
|
|
@@ -223,9 +247,17 @@ function resolveProjection(qi, table, meta, select, omit, includePii) {
|
|
|
223
247
|
// keys are all derived from it, so they move together. The one observable
|
|
224
248
|
// consequence is that a projected row's KEY INSERTION order now follows the
|
|
225
249
|
// table rather than the `select` literal.
|
|
250
|
+
const piiCols = includePii ? undefined : writesMod.piiColumns(qi, meta);
|
|
251
|
+
const stitchOnly = piiCols && piiCols.size > 0 ? (0, batched_loader_js_1.stitchOnlyFields)(select) : undefined;
|
|
226
252
|
return (0, utils_js_1.canonicalColumnOrder)(meta, Object.entries(select)
|
|
227
253
|
.filter(([, v]) => v)
|
|
228
|
-
.map(([k]) =>
|
|
254
|
+
.map(([k]) => {
|
|
255
|
+
const column = projectionColumn(table, meta, k, 'select');
|
|
256
|
+
if (piiCols?.has(column) && !stitchOnly?.has(k)) {
|
|
257
|
+
throw new errors_js_1.ValidationError(piiSelectRefusedMessage(table, meta.reverseColumnMap[column] ?? column));
|
|
258
|
+
}
|
|
259
|
+
return column;
|
|
260
|
+
}));
|
|
229
261
|
}
|
|
230
262
|
// Default / omit-only projection: PII-tagged columns are excluded unless the
|
|
231
263
|
// caller opted in with `includePii: UNSAFE`. An empty set (untagged schema) keeps the
|
|
@@ -294,6 +326,88 @@ const EMPTY_WITH_OPTIONS = Object.freeze({});
|
|
|
294
326
|
function relationOptions(spec) {
|
|
295
327
|
return spec === true ? EMPTY_WITH_OPTIONS : spec;
|
|
296
328
|
}
|
|
329
|
+
/** The options a relation in `with` reads; any other key is ignored by every relation strategy. */
|
|
330
|
+
const WITH_OPTION_KEYS = new Set([
|
|
331
|
+
'with',
|
|
332
|
+
'where',
|
|
333
|
+
'orderBy',
|
|
334
|
+
'limit',
|
|
335
|
+
'offset',
|
|
336
|
+
'take',
|
|
337
|
+
'skip',
|
|
338
|
+
'select',
|
|
339
|
+
'omit',
|
|
340
|
+
]);
|
|
341
|
+
/**
|
|
342
|
+
* Check one relation's `with` options. The fingerprint walk reads every level
|
|
343
|
+
* of every `with` on every call, whichever strategy then loads it, so this is
|
|
344
|
+
* the one place that sees them all. `lock` is refused: the row lock compiles to
|
|
345
|
+
* `FOR <mode> OF "<table>"` on the query's own table, so a lock written on a
|
|
346
|
+
* relation locked nothing and said nothing. Any other key no relation reads
|
|
347
|
+
* (`wehre`, `cursor`, `distinct`) is named once in the advice log.
|
|
348
|
+
*/
|
|
349
|
+
function checkWithOptions(qi, table, relName, opts) {
|
|
350
|
+
for (const key in opts) {
|
|
351
|
+
if (WITH_OPTION_KEYS.has(key))
|
|
352
|
+
continue;
|
|
353
|
+
const value = opts[key];
|
|
354
|
+
if (value === undefined)
|
|
355
|
+
continue;
|
|
356
|
+
if (key === 'lock') {
|
|
357
|
+
throw new errors_js_1.ValidationError(`\`lock\` is not an option of a relation in \`with\` (\`${relName}\` on "${table}"): a row lock applies to ` +
|
|
358
|
+
"the rows of the query's own table only, so the related rows would not be locked. Lock them with a " +
|
|
359
|
+
'locking read on their own table, in the same transaction.');
|
|
360
|
+
}
|
|
361
|
+
const advice = whereMod.adviceLogger(qi);
|
|
362
|
+
if (!advice || !(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unknownQueryOption, `${table}.with.${relName}.${key}`))
|
|
363
|
+
continue;
|
|
364
|
+
const suggestion = (0, utils_js_1.suggestKey)(key, WITH_OPTION_KEYS);
|
|
365
|
+
(0, logger_js_1.logTo)(advice, 'warn', `unknown option "${key}" on relation "${relName}" in \`with\` on ${table}, it is ignored.` +
|
|
366
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : ''), { code: 'unknown-query-option', table, operation: 'with', key });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* `_count` in `with` takes `true` (every relation) or an object naming
|
|
371
|
+
* relations with `true`. Anything else under a relation, `{ where }` in
|
|
372
|
+
* particular, was read as `true` and counted every related row.
|
|
373
|
+
*/
|
|
374
|
+
function assertWithCount(table, count) {
|
|
375
|
+
if (count === true)
|
|
376
|
+
return;
|
|
377
|
+
if (!(0, filters_js_1.isPlainRecord)(count)) {
|
|
378
|
+
throw new errors_js_1.ValidationError(`\`_count\` in \`with\` on "${table}" takes true or an object naming relations, for example ` +
|
|
379
|
+
`\`_count: { posts: true }\`; received ${(0, filters_js_1.describeOperand)(count)}.`);
|
|
380
|
+
}
|
|
381
|
+
for (const rel of Object.keys(count)) {
|
|
382
|
+
const value = count[rel];
|
|
383
|
+
if (value === undefined || typeof value === 'boolean')
|
|
384
|
+
continue;
|
|
385
|
+
throw new errors_js_1.ValidationError(`\`_count.${rel}\` in \`with\` on "${table}" takes true; received ${(0, filters_js_1.describeOperand)(value)}. ` +
|
|
386
|
+
'`_count` counts every related row. To count only the rows that match a filter, use ' +
|
|
387
|
+
`\`count({ where })\` on the related table.`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* A to-many relation's `offset` (`skip` is folded into it at the args seam by
|
|
392
|
+
* `normalizeWithPagination`, which also refuses it on a to-one relation).
|
|
393
|
+
*/
|
|
394
|
+
function relationOffset(opts) {
|
|
395
|
+
return opts.offset;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The LIMIT / OFFSET tail of a to-many relation's inner row query. Binds the
|
|
399
|
+
* limit, then the offset, the order {@link collectRelationSubqueryParams}
|
|
400
|
+
* mirrors. Without an offset the tail is the plain ` LIMIT $n` it has always
|
|
401
|
+
* been; with one it goes through the dialect's pagination hook, which knows
|
|
402
|
+
* how to write an offset with no limit.
|
|
403
|
+
*/
|
|
404
|
+
function relationPagination(qi, limit, offset, params, hasOrder) {
|
|
405
|
+
const limitRef = limit === undefined ? undefined : qi.paginationRef(limit, params, 'relation limit');
|
|
406
|
+
if (offset === undefined)
|
|
407
|
+
return limitRef === undefined ? '' : ` LIMIT ${limitRef}`;
|
|
408
|
+
const offsetRef = qi.paginationRef(offset, params, 'relation offset');
|
|
409
|
+
return qi.buildPagination(limitRef, offsetRef, hasOrder);
|
|
410
|
+
}
|
|
297
411
|
/**
|
|
298
412
|
* Produce a fingerprint for a `with` clause tree. Recursion mirrors
|
|
299
413
|
* buildSelectWithRelations / buildRelationSubquery.
|
|
@@ -315,6 +429,7 @@ function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
315
429
|
// Reserved `_count` key, fingerprint by the selected relation set so
|
|
316
430
|
// `_count: true` and `_count: { posts: true }` never share a cache entry.
|
|
317
431
|
if (relName === '_count') {
|
|
432
|
+
assertWithCount(meta.name, spec);
|
|
318
433
|
const c = spec;
|
|
319
434
|
parts.push(c === true
|
|
320
435
|
? '_count(*)'
|
|
@@ -331,6 +446,8 @@ function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
331
446
|
continue;
|
|
332
447
|
}
|
|
333
448
|
const opts = relationOptions(spec);
|
|
449
|
+
if (opts !== EMPTY_WITH_OPTIONS)
|
|
450
|
+
checkWithOptions(qi, meta.name, relName, opts);
|
|
334
451
|
const subParts = [];
|
|
335
452
|
// select/omit shape
|
|
336
453
|
if (opts.select) {
|
|
@@ -365,6 +482,10 @@ function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
365
482
|
if (opts.limit !== undefined) {
|
|
366
483
|
subParts.push(qi.dialect.inlineLimitOffset ? `l=${opts.limit}` : 'l=1');
|
|
367
484
|
}
|
|
485
|
+
const offset = relationOffset(opts);
|
|
486
|
+
if (offset !== undefined) {
|
|
487
|
+
subParts.push(qi.dialect.inlineLimitOffset ? `off=${offset}` : 'off=1');
|
|
488
|
+
}
|
|
368
489
|
// nested with (recurse)
|
|
369
490
|
if (opts.with) {
|
|
370
491
|
const nested = withFingerprint(qi, opts.with, relDef.to, depth + 1);
|
|
@@ -437,6 +558,10 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
437
558
|
if (opts.limit !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
438
559
|
params.push(qi.paginationValue(opts.limit, 'relation limit'));
|
|
439
560
|
}
|
|
561
|
+
const m2mOffset = relationOffset(opts);
|
|
562
|
+
if (m2mOffset !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
563
|
+
params.push(qi.paginationValue(m2mOffset, 'relation offset'));
|
|
564
|
+
}
|
|
440
565
|
if (opts.with) {
|
|
441
566
|
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
442
567
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
@@ -450,7 +575,8 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
450
575
|
// Mirrors buildRelationSubquery's willWrap: `orderBy: {}` is treated as absent.
|
|
451
576
|
const relOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
452
577
|
const hasOrder = relOrderEntries.length > 0;
|
|
453
|
-
const
|
|
578
|
+
const offset = relationOffset(opts);
|
|
579
|
+
const willWrap = relDef.type === 'hasMany' && (opts.limit !== undefined || offset !== undefined || hasOrder);
|
|
454
580
|
// Non-wrapped path: nested relations BEFORE where/limit
|
|
455
581
|
if (!willWrap && opts.with) {
|
|
456
582
|
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
@@ -480,6 +606,9 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
480
606
|
if (relDef.type === 'hasMany' && opts.limit !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
481
607
|
params.push(qi.paginationValue(opts.limit, 'relation limit'));
|
|
482
608
|
}
|
|
609
|
+
if (relDef.type === 'hasMany' && offset !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
610
|
+
params.push(qi.paginationValue(offset, 'relation offset'));
|
|
611
|
+
}
|
|
483
612
|
// Wrapped path: nested relations AFTER where/limit (inside inner subquery)
|
|
484
613
|
if (willWrap && opts.with) {
|
|
485
614
|
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
@@ -637,14 +766,14 @@ function isRelationOrderByValue(_qi, value) {
|
|
|
637
766
|
}
|
|
638
767
|
/**
|
|
639
768
|
* Render the ` NULLS FIRST` / ` NULLS LAST` suffix for a column ordering.
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
* {@link UnsupportedFeatureError} (E017) instead of broken SQL.
|
|
769
|
+
* On an engine without the `NULLS FIRST/LAST` grammar (`nullsOrderingClause`:
|
|
770
|
+
* PostgreSQL and SQLite have it) a caller asking for explicit nulls placement
|
|
771
|
+
* gets a clear {@link UnsupportedFeatureError} (E017) instead of broken SQL.
|
|
643
772
|
*/
|
|
644
773
|
function nullsSuffix(qi, nulls) {
|
|
645
774
|
if (!nulls)
|
|
646
775
|
return '';
|
|
647
|
-
if (qi.dialect.
|
|
776
|
+
if (!qi.dialect.capabilities.nullsOrderingClause) {
|
|
648
777
|
throw new errors_js_1.UnsupportedFeatureError('NULLS FIRST/LAST ordering', qi.dialect.name, 'Explicit nulls placement in orderBy is only available on PostgreSQL and SQLite.');
|
|
649
778
|
}
|
|
650
779
|
return nulls === 'first' ? ' NULLS FIRST' : ' NULLS LAST';
|
|
@@ -718,7 +847,7 @@ function buildJsonPathOrderEntry(qi, table, meta, field, spec, prefix, params) {
|
|
|
718
847
|
// the grammar gate matches nullsSuffix.
|
|
719
848
|
const nullsSql = spec.nulls
|
|
720
849
|
? nullsSuffix(qi, spec.nulls)
|
|
721
|
-
: qi.dialect.
|
|
850
|
+
: qi.dialect.capabilities.nullsOrderingClause
|
|
722
851
|
? ' NULLS LAST'
|
|
723
852
|
: '';
|
|
724
853
|
return `${lhs} ${dir}${nullsSql}`;
|
|
@@ -927,6 +1056,18 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
|
|
|
927
1056
|
})
|
|
928
1057
|
.join(', ');
|
|
929
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Param-collect mirror of a TO-ONE relation orderBy entry, done by running
|
|
1061
|
+
* {@link buildRelationOrderBy} and discarding its SQL. The build pushes the
|
|
1062
|
+
* target's global filter once per ordered column and, for a chained
|
|
1063
|
+
* `{ author: { org: { name } } }` ordering, once more per hop table, in hop
|
|
1064
|
+
* order. A hand-written mirror that pushed one filter per column bound too few
|
|
1065
|
+
* params (and in the wrong order) as soon as a hop table had a filter, so this
|
|
1066
|
+
* mirror is the build itself and cannot drift from it.
|
|
1067
|
+
*/
|
|
1068
|
+
function collectToOneRelationOrderParams(qi, relName, value, params, ctx) {
|
|
1069
|
+
buildRelationOrderBy(qi, relName, value, 'ord0', params, ctx);
|
|
1070
|
+
}
|
|
930
1071
|
/**
|
|
931
1072
|
* Validate a {@link RelationPickOrderBy} entry's scope and shape. Shared by
|
|
932
1073
|
* the SQL-build path ({@link buildRelationPickOrderBy}) and the cache-hit
|
|
@@ -1026,7 +1167,7 @@ function buildRelationPickOrderBy(qi, relName, relDef, spec, alias, parentRef, p
|
|
|
1026
1167
|
// the caller set `nulls` explicitly; the grammar gate matches nullsSuffix.
|
|
1027
1168
|
const nullsSql = spec.nulls
|
|
1028
1169
|
? nullsSuffix(qi, spec.nulls)
|
|
1029
|
-
: qi.dialect.
|
|
1170
|
+
: qi.dialect.capabilities.nullsOrderingClause
|
|
1030
1171
|
? ' NULLS LAST'
|
|
1031
1172
|
: '';
|
|
1032
1173
|
// Lateral plan: splice a `LEFT JOIN LATERAL (... LIMIT 1) ON true` into the
|
|
@@ -1189,14 +1330,18 @@ function collectRelationOrderParams(qi, targetTable, targetMeta, orderEntries, p
|
|
|
1189
1330
|
if ((0, filters_js_1.isRelationPickOrderBy)(dirValue)) {
|
|
1190
1331
|
throw pickOrderNestedError(qi, key);
|
|
1191
1332
|
}
|
|
1192
|
-
|
|
1333
|
+
// resolveRelationDef, as buildRelationOrderBy resolves it, so a relation
|
|
1334
|
+
// written in its table spelling binds its params on a cache hit too.
|
|
1335
|
+
const relDef = (0, utils_js_1.resolveRelationDef)(targetMeta.relations, key);
|
|
1193
1336
|
if (relDef && (relDef.type === 'hasMany' || relDef.type === 'manyToMany')) {
|
|
1194
1337
|
collectRelationCountParams(qi, relDef, params);
|
|
1195
1338
|
}
|
|
1196
1339
|
else if (relDef) {
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1340
|
+
collectToOneRelationOrderParams(qi, key, dirValue, params, {
|
|
1341
|
+
meta: targetMeta,
|
|
1342
|
+
table: targetTable,
|
|
1343
|
+
parentRef: targetTable,
|
|
1344
|
+
});
|
|
1200
1345
|
}
|
|
1201
1346
|
continue;
|
|
1202
1347
|
}
|
|
@@ -1251,26 +1396,27 @@ function buildRelationCountExpr(qi, relDef, parentRef, alias, params) {
|
|
|
1251
1396
|
* {@link collectManyToManyTargetGlobalFilter}.
|
|
1252
1397
|
*/
|
|
1253
1398
|
function manyToManyTargetGlobalFilterExists(qi, relDef, alias, jalias, params) {
|
|
1254
|
-
const
|
|
1255
|
-
if (!
|
|
1399
|
+
const through = relDef.through;
|
|
1400
|
+
if (!through)
|
|
1256
1401
|
return '';
|
|
1257
1402
|
const tMeta = qi.schema.tables[relDef.to];
|
|
1258
1403
|
if (!tMeta || tMeta.primaryKey.length === 0)
|
|
1259
1404
|
return '';
|
|
1260
|
-
const
|
|
1261
|
-
const targetKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.targetKey);
|
|
1405
|
+
const targetKeys = (0, schema_js_1.normalizeKeyColumns)(through.targetKey);
|
|
1262
1406
|
const pk = tMeta.primaryKey;
|
|
1263
1407
|
if (targetKeys.length !== pk.length)
|
|
1264
1408
|
return '';
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1409
|
+
return whereMod.withTargetGlobalFilter(qi, relDef.to, '', (gf) => {
|
|
1410
|
+
const talias = `${alias}t`;
|
|
1411
|
+
const join = targetKeys.map((jc, i) => `${talias}.${qi.q(pk[i])} = ${jalias}.${qi.q(jc)}`).join(' AND ');
|
|
1412
|
+
const gfClause = whereMod.buildAliasWhere(qi, relDef.to, tMeta, talias, gf, params);
|
|
1413
|
+
const gfAnd = gfClause ? ` AND ${gfClause}` : '';
|
|
1414
|
+
return `EXISTS (SELECT 1 FROM ${qi.q(relDef.to)} ${talias} WHERE ${join}${gfAnd})`;
|
|
1415
|
+
});
|
|
1269
1416
|
}
|
|
1270
1417
|
/** Param-collect mirror of {@link manyToManyTargetGlobalFilterExists}. */
|
|
1271
1418
|
function collectManyToManyTargetGlobalFilter(qi, relDef, params) {
|
|
1272
|
-
|
|
1273
|
-
if (!gf || !relDef.through)
|
|
1419
|
+
if (!relDef.through)
|
|
1274
1420
|
return;
|
|
1275
1421
|
const tMeta = qi.schema.tables[relDef.to];
|
|
1276
1422
|
if (!tMeta || tMeta.primaryKey.length === 0)
|
|
@@ -1278,7 +1424,7 @@ function collectManyToManyTargetGlobalFilter(qi, relDef, params) {
|
|
|
1278
1424
|
const targetKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.targetKey);
|
|
1279
1425
|
if (targetKeys.length !== tMeta.primaryKey.length)
|
|
1280
1426
|
return;
|
|
1281
|
-
whereMod.collectAliasWhereParams(qi, relDef.to, tMeta, gf, params);
|
|
1427
|
+
whereMod.withTargetGlobalFilter(qi, relDef.to, false, (gf) => whereMod.collectAliasWhereParams(qi, relDef.to, tMeta, gf, params));
|
|
1282
1428
|
}
|
|
1283
1429
|
/**
|
|
1284
1430
|
* Param-collect mirror of {@link buildRelationCountExpr}'s global-filter
|
|
@@ -1413,13 +1559,15 @@ function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1413
1559
|
// find it.
|
|
1414
1560
|
//
|
|
1415
1561
|
// The warn is gated and deduped because this sits in parseNestedRow's
|
|
1416
|
-
// per-relation loop: one 10,000-row page was 10,000
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1562
|
+
// per-relation loop: one 10,000-row page was 10,000 warnings. The
|
|
1563
|
+
// parse error stays out of the entry: its message quotes the payload,
|
|
1564
|
+
// which is row data.
|
|
1565
|
+
const log = whereMod.adviceLogger(qi);
|
|
1566
|
+
if (log && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.relationParseFailure, `${qi.table}.${relName}`)) {
|
|
1567
|
+
(0, logger_js_1.logTo)(log, 'warn', `Relation "${relName}" on "${qi.table}" returned a payload that is not valid JSON. ` +
|
|
1568
|
+
`This is a bug; please report it with the emitted SQL.`, { code: 'relation-payload-json', table: qi.table, relation: relName });
|
|
1421
1569
|
}
|
|
1422
|
-
throw new errors_js_1.
|
|
1570
|
+
throw new errors_js_1.InvariantError('relation-payload-json', `Relation "${relName}" on table "${qi.table}" returned a payload that could not be parsed as JSON.`, { cause: err });
|
|
1423
1571
|
}
|
|
1424
1572
|
}
|
|
1425
1573
|
else if (Array.isArray(rawValue)) {
|
|
@@ -1914,7 +2062,7 @@ function flattenNodeSignature(node) {
|
|
|
1914
2062
|
return `${node.relName}@${node.alias}(${node.cols.map(([col]) => col).join(',')})${slots}`;
|
|
1915
2063
|
}
|
|
1916
2064
|
/**
|
|
1917
|
-
*
|
|
2065
|
+
* Per-query advice, once per (table, relation, reason): an explicitly
|
|
1918
2066
|
* requested `relationLoadStrategy: 'flatten'` did NOT engage for a relation.
|
|
1919
2067
|
*
|
|
1920
2068
|
* Worth its own warning because the fallback is otherwise INVISIBLE: the query
|
|
@@ -1923,21 +2071,23 @@ function flattenNodeSignature(node) {
|
|
|
1923
2071
|
* Silence there is indistinguishable from the strategy being a no-op, which is
|
|
1924
2072
|
* exactly the shape of thing that gets filed as a bug.
|
|
1925
2073
|
*
|
|
1926
|
-
*
|
|
1927
|
-
* the shared registry so a hot query logs once rather than per
|
|
1928
|
-
* a flatten plan is recomputed on the build, param-collect and
|
|
1929
|
-
* paths for the same query; the registry is what keeps that to
|
|
2074
|
+
* Off when `diagnostics` turn advice off, like the other planner notes, and
|
|
2075
|
+
* deduped through the shared registry so a hot query logs once rather than per
|
|
2076
|
+
* call. Note that a flatten plan is recomputed on the build, param-collect and
|
|
2077
|
+
* row-assembly paths for the same query; the registry is what keeps that to
|
|
2078
|
+
* one line.
|
|
1930
2079
|
*/
|
|
1931
|
-
function warnFlattenFallback(table, rejects) {
|
|
2080
|
+
function warnFlattenFallback(qi, table, rejects) {
|
|
1932
2081
|
if (rejects.length === 0)
|
|
1933
2082
|
return;
|
|
1934
|
-
|
|
2083
|
+
const log = whereMod.adviceLogger(qi);
|
|
2084
|
+
if (!log)
|
|
1935
2085
|
return;
|
|
1936
2086
|
for (const { relation, reason } of rejects) {
|
|
1937
2087
|
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.flattenFallback, `${table}.${relation}|${reason}`))
|
|
1938
2088
|
continue;
|
|
1939
|
-
|
|
1940
|
-
`${reason}. It loads via the correlated subquery instead (same rows, same values, different plan)
|
|
2089
|
+
(0, logger_js_1.logTo)(log, 'warn', `relationLoadStrategy: 'flatten' did not engage for relation "${relation}" on "${table}": ` +
|
|
2090
|
+
`${reason}. It loads via the correlated subquery instead (same rows, same values, different plan).`, { code: 'flatten-fallback', table, relation, reason });
|
|
1941
2091
|
}
|
|
1942
2092
|
}
|
|
1943
2093
|
/**
|
|
@@ -1968,7 +2118,7 @@ function planFlattenWith(qi, table, withClause, includePii) {
|
|
|
1968
2118
|
if (node)
|
|
1969
2119
|
nodes[relName] = node;
|
|
1970
2120
|
}
|
|
1971
|
-
warnFlattenFallback(table, rejects);
|
|
2121
|
+
warnFlattenFallback(qi, table, rejects);
|
|
1972
2122
|
if (Object.keys(nodes).length === 0)
|
|
1973
2123
|
return null;
|
|
1974
2124
|
// Flat rows carry the root table's own columns, one column per non-flattened
|
|
@@ -1978,7 +2128,7 @@ function planFlattenWith(qi, table, withClause, includePii) {
|
|
|
1978
2128
|
const reserved = new Set([...meta.allColumns, ...Object.keys(withClause)]);
|
|
1979
2129
|
for (const alias of aliases) {
|
|
1980
2130
|
if (reserved.has(alias)) {
|
|
1981
|
-
warnFlattenFallback(table, [
|
|
2131
|
+
warnFlattenFallback(qi, table, [
|
|
1982
2132
|
{
|
|
1983
2133
|
relation: Object.keys(nodes).join(', '),
|
|
1984
2134
|
reason: `the projected alias "${alias}" collides with a column or relation name on "${table}"`,
|
|
@@ -2251,11 +2401,12 @@ function buildSelectWithRelations(qi, table, withClause, params, columnsList, de
|
|
|
2251
2401
|
const meta = qi.schema.tables[table];
|
|
2252
2402
|
if (!meta)
|
|
2253
2403
|
throw new errors_js_1.ValidationError(`Unknown table "${table}"`);
|
|
2254
|
-
// Positional JSON encoding
|
|
2255
|
-
//
|
|
2256
|
-
//
|
|
2257
|
-
//
|
|
2258
|
-
|
|
2404
|
+
// Positional JSON encoding needs `positionalJson`, the flag the default in
|
|
2405
|
+
// builder.ts reads too, so the default never picks an encoding refused here.
|
|
2406
|
+
// Gate here, the single entry point for every `with` clause, so no engine
|
|
2407
|
+
// ever emits the json_build_array shape its dialect can't produce (and
|
|
2408
|
+
// mssql's FOR JSON override path is never reached with positional active).
|
|
2409
|
+
if (qi.jsonEncoding === 'positional' && !qi.dialect.capabilities.positionalJson) {
|
|
2259
2410
|
throw new errors_js_1.UnsupportedFeatureError("jsonEncoding: 'positional'", qi.dialect.name, 'Positional relation encoding is only available on PostgreSQL in this version.');
|
|
2260
2411
|
}
|
|
2261
2412
|
const cols = columnsList ?? meta.allColumns;
|
|
@@ -2409,12 +2560,13 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2409
2560
|
// The raw `spec` is still what crosses the dialect seam below, since
|
|
2410
2561
|
// RelationSubqueryContext is a published contract.
|
|
2411
2562
|
const opts = relationOptions(spec);
|
|
2412
|
-
//
|
|
2413
|
-
// row, so a missing FK index multiplies into per-parent full-table
|
|
2414
|
-
// batched-loader ORM pays the same missing index only once, which is
|
|
2415
|
-
// schemas migrated from one often lack these). Name the exact index to
|
|
2416
|
-
// instead of letting the slowness look like an ORM problem.
|
|
2417
|
-
|
|
2563
|
+
// Per-query advice: correlated relation loading probes the child table once
|
|
2564
|
+
// per parent row, so a missing FK index multiplies into per-parent full-table
|
|
2565
|
+
// scans (a batched-loader ORM pays the same missing index only once, which is
|
|
2566
|
+
// why schemas migrated from one often lack these). Name the exact index to
|
|
2567
|
+
// create instead of letting the slowness look like an ORM problem.
|
|
2568
|
+
const log = whereMod.adviceLogger(qi);
|
|
2569
|
+
if (log) {
|
|
2418
2570
|
const warnKey = `${relDef.from}.${relDef.name}`;
|
|
2419
2571
|
// Compute the (potentially costly) probe only for a key not yet warned, and
|
|
2420
2572
|
// claim the key through the process-wide registry ONLY when we actually warn,
|
|
@@ -2423,10 +2575,16 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2423
2575
|
if (!(0, warn_registry_js_1.hasWarnedOnce)(warn_registry_js_1.WARN_NS.unindexedRelation, warnKey)) {
|
|
2424
2576
|
const miss = (0, index_advisor_js_1.missingIndexForRelation)(qi.schema, relDef);
|
|
2425
2577
|
if (miss && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unindexedRelation, warnKey)) {
|
|
2426
|
-
|
|
2578
|
+
(0, logger_js_1.logTo)(log, 'warn', `Relation "${relDef.name}" on "${relDef.from}" probes ` +
|
|
2427
2579
|
`"${miss.table}"(${miss.columns.join(', ')}) which has no covering index, ` +
|
|
2428
2580
|
`each parent row scans the full table. Fix: ${miss.createSql}; ` +
|
|
2429
|
-
'or run `npx turbine doctor` for a full report.'
|
|
2581
|
+
'or run `npx turbine doctor` for a full report.', {
|
|
2582
|
+
code: 'unindexed-relation',
|
|
2583
|
+
table: relDef.from,
|
|
2584
|
+
relation: relDef.name,
|
|
2585
|
+
probeTable: miss.table,
|
|
2586
|
+
columns: miss.columns,
|
|
2587
|
+
});
|
|
2430
2588
|
}
|
|
2431
2589
|
}
|
|
2432
2590
|
}
|
|
@@ -2496,7 +2654,8 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2496
2654
|
// it must neither trigger the wrap (dropping nested relations) nor render a
|
|
2497
2655
|
// dangling `ORDER BY `. `limit: 0` is meaningful (LIMIT 0) and DOES wrap.
|
|
2498
2656
|
const relOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
2499
|
-
const
|
|
2657
|
+
const offset = relationOffset(opts);
|
|
2658
|
+
const willWrap = relDef.type === 'hasMany' && (opts.limit !== undefined || offset !== undefined || relOrderEntries.length > 0);
|
|
2500
2659
|
// manyToMany takes a dedicated JOIN-through-junction path. Nested relations,
|
|
2501
2660
|
// where, orderBy, and select/omit are handled there (the target alias is the
|
|
2502
2661
|
// row source, exactly like hasMany), so short-circuit before the hasMany logic.
|
|
@@ -2564,8 +2723,8 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2564
2723
|
// (and shifts every later placeholder by one). To-one relations ignore limit.
|
|
2565
2724
|
// `limit: 0` is honored (LIMIT 0 → empty array), so check !== undefined.
|
|
2566
2725
|
let limitClause = '';
|
|
2567
|
-
if (relDef.type === 'hasMany'
|
|
2568
|
-
limitClause =
|
|
2726
|
+
if (relDef.type === 'hasMany') {
|
|
2727
|
+
limitClause = relationPagination(qi, opts.limit, offset, params, relOrderEntries.length > 0);
|
|
2569
2728
|
}
|
|
2570
2729
|
if (relDef.type === 'hasMany') {
|
|
2571
2730
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -2683,11 +2842,8 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
2683
2842
|
const gfExtra = whereMod.targetGlobalFilterAlias(qi, targetTable, talias, params);
|
|
2684
2843
|
if (gfExtra)
|
|
2685
2844
|
whereClause += ` AND ${gfExtra}`;
|
|
2686
|
-
// LIMIT, `limit: 0` is honored (LIMIT 0 → empty array)
|
|
2687
|
-
|
|
2688
|
-
if (opts.limit !== undefined) {
|
|
2689
|
-
limitClause = ` LIMIT ${qi.paginationRef(opts.limit, params, 'relation limit')}`;
|
|
2690
|
-
}
|
|
2845
|
+
// LIMIT / OFFSET, `limit: 0` is honored (LIMIT 0 → empty array)
|
|
2846
|
+
const limitClause = relationPagination(qi, opts.limit, relationOffset(opts), params, relOrderEntries.length > 0);
|
|
2691
2847
|
const fromJoin = `FROM ${qTarget} ${talias} JOIN ${qJunction} ${jalias} ON ${joinOn}`;
|
|
2692
2848
|
// When LIMIT or ORDER BY is present, wrap the joined rows in an inner subquery
|
|
2693
2849
|
// 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;
|