joist-orm 1.218.2 → 1.220.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.
Files changed (42) hide show
  1. package/build/AliasAssigner.d.ts +2 -0
  2. package/build/AliasAssigner.d.ts.map +1 -1
  3. package/build/AliasAssigner.js +23 -8
  4. package/build/AliasAssigner.js.map +1 -1
  5. package/build/Aliases.d.ts +18 -0
  6. package/build/Aliases.d.ts.map +1 -1
  7. package/build/Aliases.js +24 -0
  8. package/build/Aliases.js.map +1 -1
  9. package/build/EntityFilter.d.ts +1 -0
  10. package/build/EntityFilter.d.ts.map +1 -1
  11. package/build/QueryParser.d.ts +86 -17
  12. package/build/QueryParser.d.ts.map +1 -1
  13. package/build/QueryParser.js +453 -143
  14. package/build/QueryParser.js.map +1 -1
  15. package/build/QueryVisitor.d.ts +1 -1
  16. package/build/QueryVisitor.d.ts.map +1 -1
  17. package/build/QueryVisitor.js +11 -2
  18. package/build/QueryVisitor.js.map +1 -1
  19. package/build/dataloaders/findByUniqueDataLoader.js +1 -1
  20. package/build/dataloaders/findByUniqueDataLoader.js.map +1 -1
  21. package/build/dataloaders/findCountDataLoader.d.ts.map +1 -1
  22. package/build/dataloaders/findCountDataLoader.js +31 -24
  23. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  24. package/build/dataloaders/findDataLoader.d.ts +7 -14
  25. package/build/dataloaders/findDataLoader.d.ts.map +1 -1
  26. package/build/dataloaders/findDataLoader.js +71 -87
  27. package/build/dataloaders/findDataLoader.js.map +1 -1
  28. package/build/drivers/Driver.d.ts +1 -1
  29. package/build/drivers/Driver.d.ts.map +1 -1
  30. package/build/drivers/buildKnexQuery.d.ts.map +1 -1
  31. package/build/drivers/buildKnexQuery.js +14 -12
  32. package/build/drivers/buildKnexQuery.js.map +1 -1
  33. package/build/drivers/buildRawQuery.d.ts.map +1 -1
  34. package/build/drivers/buildRawQuery.js +18 -14
  35. package/build/drivers/buildRawQuery.js.map +1 -1
  36. package/build/drivers/buildUtils.d.ts +3 -1
  37. package/build/drivers/buildUtils.d.ts.map +1 -1
  38. package/build/drivers/buildUtils.js +3 -4
  39. package/build/drivers/buildUtils.js.map +1 -1
  40. package/build/plugins/PreloadPlugin.d.ts +2 -4
  41. package/build/plugins/PreloadPlugin.d.ts.map +1 -1
  42. package/package.json +2 -2
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConditionBuilder = exports.skipCondition = void 0;
4
4
  exports.parseFindQuery = parseFindQuery;
5
+ exports.parseAlias = parseAlias;
5
6
  exports.parseEntityFilter = parseEntityFilter;
6
7
  exports.parseValueFilter = parseValueFilter;
7
8
  exports.mapToDb = mapToDb;
@@ -9,9 +10,6 @@ exports.maybeAddOrderBy = maybeAddOrderBy;
9
10
  exports.addTablePerClassJoinsAndClassTag = addTablePerClassJoinsAndClassTag;
10
11
  exports.maybeAddNotSoftDeleted = maybeAddNotSoftDeleted;
11
12
  exports.getTables = getTables;
12
- exports.joinKeywords = joinKeywords;
13
- exports.joinClause = joinClause;
14
- exports.joinClauses = joinClauses;
15
13
  exports.makeLike = makeLike;
16
14
  const joist_utils_1 = require("joist-utils");
17
15
  const Aliases_1 = require("./Aliases");
@@ -20,6 +18,7 @@ const EntityMetadata_1 = require("./EntityMetadata");
20
18
  const QueryBuilder_1 = require("./QueryBuilder");
21
19
  const QueryVisitor_1 = require("./QueryVisitor");
22
20
  const configure_1 = require("./configure");
21
+ const buildUtils_1 = require("./drivers/buildUtils");
23
22
  const index_1 = require("./index");
24
23
  const keywords_1 = require("./keywords");
25
24
  const utils_1 = require("./utils");
@@ -37,61 +36,200 @@ function parseFindQuery(meta, filter, opts = {}) {
37
36
  const tables = [];
38
37
  const orderBys = [];
39
38
  const query = { selects, tables, orderBys };
40
- const { orderBy = undefined, conditions: optsExpression = undefined, softDeletes = "exclude", pruneJoins = true, keepAliases = [], } = opts;
39
+ const { orderBy = undefined, softDeletes = "exclude", pruneJoins = true, keepAliases = [] } = opts;
41
40
  const cb = new ConditionBuilder();
42
- const aliases = {};
41
+ const aliases = opts.aliases ?? {};
43
42
  function getAlias(tableName) {
44
43
  const abbrev = (0, QueryBuilder_1.abbreviation)(tableName);
45
44
  const i = aliases[abbrev] || 0;
46
45
  aliases[abbrev] = i + 1;
47
46
  return i === 0 ? abbrev : `${abbrev}${i}`;
48
47
  }
49
- function maybeAddNotSoftDeleted(meta, alias) {
50
- if (filterSoftDeletes(meta, softDeletes)) {
51
- const column = meta.allFields[(0, EntityMetadata_1.getBaseMeta)(meta).timestampFields.deletedAt].serde?.columns[0];
48
+ // If they passed extra `conditions: ...`, parse that.
49
+ // We can do this up-front b/c it doesn't require any join-tree metadata to perform,
50
+ // and then it's available for our `addLateralJoin`s to rewrite.
51
+ if (opts.conditions)
52
+ cb.maybeAddExpression(opts.conditions);
53
+ // Also see if outer `addLateralJoin` is passing us its join conditions
54
+ if (opts.rawConditions) {
55
+ for (const rc of opts.rawConditions)
56
+ cb.addRawCondition(rc);
57
+ }
58
+ function addLateralJoin(meta, fromAlias, alias, filter,
59
+ // The join condition for the subquery from the outer table
60
+ condition,
61
+ // If we're a m2m, the join table to inject (which will use the outer table)
62
+ joinTable,
63
+ // Allow addOrderBy to do its own post-addTable lateral join building
64
+ orderByTables = undefined) {
65
+ const ef = parseEntityFilter(meta, filter);
66
+ // Maybe skip
67
+ if (!ef && !(0, Aliases_1.isAlias)(filter) && !orderByTables)
68
+ return;
69
+ bindAlias(filter, meta, alias);
70
+ // Create an alias to use for our subquery's `where parent_id = id` condition
71
+ const a = (0, Aliases_1.newAliasProxy)(meta.cstr);
72
+ // This is kinda janky, but take the `ef: ParsedEntityFilter` and re-work it back into a
73
+ // `subFilter/count` pair that we can use for our recursive `parseFindQuery` call.
74
+ function convertFilter() {
75
+ if (ef) {
76
+ if (ef.kind === "join") {
77
+ // subFilter will be unprocessed, so we can pass it recursively into `parseFindQuery`
78
+ return { subFilter: ef.subFilter, count: undefined };
79
+ }
80
+ else if (ef.kind === "not-null") {
81
+ return { subFilter: {}, count: { kind: "gt", value: 0 } };
82
+ }
83
+ else if (ef.kind === "is-null") {
84
+ return { subFilter: {}, count: { kind: "eq", value: 0 } };
85
+ }
86
+ else if (ef.kind === "eq") {
87
+ return { subFilter: { id: ef.value }, count: undefined };
88
+ }
89
+ else if (ef.kind === "in") {
90
+ return { subFilter: { id: { in: ef.value } }, count: undefined };
91
+ }
92
+ else {
93
+ // If `ef` is set, it's already parsed, which `parseFindQuery` won't expect, so pass the original `filter`
94
+ return { subFilter: { id: filter }, count: undefined };
95
+ }
96
+ }
97
+ else {
98
+ return { subFilter: {}, count: undefined };
99
+ }
100
+ }
101
+ const { subFilter,
102
+ // If `ef` was is-null/not-null, use that as the count, otherwise probe for subFilter[$count]
103
+ count = subFilter && "$count" in subFilter ? { kind: "eq", value: subFilter["$count"] } : undefined, } = convertFilter();
104
+ const subQuery = parseFindQuery(meta, { as: a, ...subFilter }, {
105
+ // Only pass through a subset of the opts...
106
+ softDeletes: opts.softDeletes,
107
+ pruneJoins: opts.pruneJoins,
108
+ keepAliases: opts.keepAliases,
109
+ aliases,
110
+ alias,
111
+ // And set our own complex condition as the join condition (for o2m, two for m2m)
112
+ rawConditions: [condition],
113
+ // Let the subquery's pruneUnusedJoins know about the top-level WHERE clauses
114
+ topLevelCondition: opts.topLevelCondition ?? cb,
115
+ outerLateralJoins: [{ alias, select: selects, outerCb: cb }, ...(opts.outerLateralJoins ?? [])],
116
+ });
117
+ subQuery.orderBys = [];
118
+ if (joinTable)
119
+ subQuery.tables.unshift(joinTable);
120
+ const join = { join: "lateral", table: meta.tableName, query: subQuery, alias, fromAlias };
121
+ (orderByTables ?? tables).push(join);
122
+ // Look for complex conditions...
123
+ const topLevelAlias = opts.outerLateralJoins?.[opts.outerLateralJoins?.length - 1]?.alias;
124
+ const complexConditions = (opts.topLevelCondition ?? cb).findAndRewrite(topLevelAlias ?? alias, alias);
125
+ for (const cc of complexConditions) {
126
+ if (cc.cond.kind === "column" && cc.cond.column === "$count") {
127
+ subQuery.selects.push(buildCountStar(cc));
128
+ }
129
+ else {
130
+ const [sql, bindings] = (0, buildUtils_1.buildCondition)(cc.cond);
131
+ subQuery.selects.push({
132
+ sql: `BOOL_OR(${sql}) as ${cc.as}`,
133
+ aliases: [cc.cond.alias],
134
+ bindings,
135
+ });
136
+ }
137
+ }
138
+ // If there are complex conditions looking at our data, we don't want a "make sure at least one matched"
139
+ const usedByComplexCondition = selects.some((s) => typeof s === "object" && "aliases" in s && s.aliases.includes(alias)) ||
140
+ // If we're the very 1st addLateralJoin, we don't push our selects into the next-up
141
+ // lateral join, so instead look through the top-level condition
142
+ (!opts.topLevelCondition &&
143
+ deepFindConditions(cb.expressions[0], true).some((c) => {
144
+ return c.kind === "raw" && c.aliases.includes(alias);
145
+ }));
146
+ // If there are literally no conditions on this child relation, don't add the "make sure at least one matched"
147
+ const hasAnyFilter = deepFindConditions(subQuery.condition, true).length > 0 || count !== undefined;
148
+ if (!usedByComplexCondition && hasAnyFilter) {
52
149
  cb.addSimpleCondition({
53
150
  kind: "column",
54
151
  alias,
55
- column: column.columnName,
56
- dbType: column.dbType,
57
- cond: { kind: "is-null" },
58
- pruneable: true,
152
+ column: "_",
153
+ dbType: "int",
154
+ cond: count ?? { kind: "gt", value: 0 },
155
+ // Don't let this condition pin the join, unless the user asked for a specific count
156
+ // (or deepFindConditions finds a real condition from the above filter).
157
+ pruneable: count === undefined,
158
+ });
159
+ // Go up the tree and make sure any parent lateral joins have a "at least 1 match"
160
+ opts.outerLateralJoins?.forEach(({ alias, outerCb }) => {
161
+ outerCb.addSimpleCondition({
162
+ kind: "column",
163
+ alias,
164
+ column: "_",
165
+ dbType: "int",
166
+ cond: { kind: "gt", value: 0 },
167
+ pruneable: true,
168
+ });
59
169
  });
60
170
  }
171
+ return join;
61
172
  }
173
+ /** Adds `meta` to the query, i.e. for m2o/o2o joins into parents. */
62
174
  function addTable(meta, alias, join, col1, col2, filter) {
63
175
  // look at filter, is it `{ book: "b2" }` or `{ book: { ... } }`
64
176
  const ef = parseEntityFilter(meta, filter);
65
- if (!ef && join !== "primary" && !(0, Aliases_1.isAlias)(filter)) {
177
+ // Maybe skip
178
+ if (!ef && join !== "primary" && !(0, Aliases_1.isAlias)(filter))
66
179
  return;
67
- }
68
180
  if (join === "primary") {
69
181
  tables.push({ alias, table: meta.tableName, join });
70
182
  }
183
+ else if (join === "lateral") {
184
+ (0, utils_1.fail)("Unexpected lateral join");
185
+ }
71
186
  else {
72
187
  tables.push({ alias, table: meta.tableName, join, col1, col2 });
73
188
  }
74
189
  // Maybe only do this if we're the primary, or have a field that needs it?
75
- addTablePerClassJoinsAndClassTag(query, meta, alias, join === "primary");
190
+ addTablePerClassJoinsAndClassTag(query, meta, alias,
191
+ // Use opts.topLevelCondition to tell we're in a `addLateralJoin` and don't want the `CASE ... END as __class`
192
+ // select clause, which won't work because it's not an aggregate
193
+ join === "primary" && !opts.topLevelCondition);
76
194
  if (needsStiDiscriminator(meta)) {
77
195
  addStiSubtypeFilter(cb, meta, alias);
78
196
  }
79
- maybeAddNotSoftDeleted(meta, alias);
80
- // The user's locally declared aliases, i.e. `const [a, b] = aliases(Author, Book)`,
81
- // aren't guaranteed to line up with the aliases we've assigned internally, like `a`
82
- // might actually be `a1` if there are two `authors` tables in the query, so push the
83
- // canonical alias value for the current clause into the Alias.
84
- if (filter && typeof filter === "object" && "as" in filter && (0, Aliases_1.isAlias)(filter.as)) {
85
- filter.as[Aliases_1.aliasMgmt].setAlias(meta, alias);
86
- }
87
- else if ((0, Aliases_1.isAlias)(filter)) {
88
- filter[Aliases_1.aliasMgmt].setAlias(meta, alias);
197
+ maybeAddNotSoftDeleted(cb, softDeletes, meta, alias);
198
+ bindAlias(filter, meta, alias);
199
+ // If we're inside a lateral join, look for top-level conditions that need to be rewritten as `BOOL_OR(...)`
200
+ // I.e. we might be a regular m2o join, but we just came from a lateral join, so any complex conditions
201
+ // like `ourTable.column.eq(...)` need to be:
202
+ // a) injected as a `BOOL_OR(ourTable.column.eq(...)) as _b_column_0` select to surface outside the lateral join, and
203
+ // b) rewritten in the top-level query to be just `_b_column_0`
204
+ if (opts.topLevelCondition) {
205
+ const topLevelAlias = opts.outerLateralJoins?.[opts.outerLateralJoins?.length - 1]?.alias;
206
+ const complexConditions = opts.topLevelCondition.findAndRewrite(topLevelAlias ?? alias, alias);
207
+ for (const cc of complexConditions) {
208
+ if (cc.cond.kind === "column" && cc.cond.column === "$count") {
209
+ selects.push(buildCountStar(cc));
210
+ }
211
+ else {
212
+ const [sql, bindings] = (0, buildUtils_1.buildCondition)(cc.cond);
213
+ selects.push({ sql: `BOOL_OR(${sql}) as ${cc.as}`, aliases: [cc.cond.alias], bindings });
214
+ }
215
+ // Expose the `_b_column_0` through `SELECT`s all the up the tree
216
+ // (...until the top-level query, which doesn't need to SELECT it, only WHERE against it)
217
+ opts.outerLateralJoins?.forEach(({ alias, select }, i) => {
218
+ const isTopLevel = i === opts.outerLateralJoins.length - 1;
219
+ if (!isTopLevel) {
220
+ select.push({ sql: `BOOL_OR(${alias}.${cc.as}) as ${cc.as}`, bindings: [], aliases: [alias] });
221
+ }
222
+ });
223
+ }
224
+ // prune needs to see the immediate, not necessarily the whole conditions...
225
+ // only rewriting needs to see the whole thing
89
226
  }
227
+ // See if the clause says we must do a join into the relation
90
228
  if (ef && ef.kind === "join") {
91
229
  // subFilter really means we're matching against the entity columns/further joins
92
230
  Object.keys(ef.subFilter).forEach((key) => {
93
231
  // Skip the `{ as: ... }` alias binding
94
- if (key === "as")
232
+ if (key === "as" || key === "$count")
95
233
  return;
96
234
  const field = meta.allFields[key] ??
97
235
  meta.polyComponentFields?.[key] ??
@@ -209,55 +347,22 @@ function parseFindQuery(meta, filter, opts = {}) {
209
347
  (0, utils_1.fail)(`No poly component found for ${otherField.fieldName}`);
210
348
  otherColumn = otherComponent.columnName;
211
349
  }
212
- addTable(field.otherMetadata(), a, "outer", (0, keywords_1.kqDot)(alias, "id"), (0, keywords_1.kqDot)(a, otherColumn), ef.subFilter[key]);
350
+ const condition = `${(0, keywords_1.kqDot)(alias, "id")} = ${(0, keywords_1.kqDot)(a + otherField.aliasSuffix, otherColumn)}`;
351
+ addLateralJoin(field.otherMetadata(), alias, a, ef.subFilter[key], { kind: "raw", aliases: [a, alias], condition, pruneable: true, bindings: [] }, undefined);
213
352
  }
214
353
  else if (field.kind === "m2m") {
215
354
  // Always join into the m2m table
216
355
  const ja = getAlias(field.joinTableName);
217
- tables.push({
356
+ const jt = {
218
357
  alias: ja,
219
- join: "outer",
358
+ join: "inner",
220
359
  table: field.joinTableName,
221
360
  col1: (0, keywords_1.kqDot)(alias, "id"),
222
361
  col2: (0, keywords_1.kqDot)(ja, field.columnNames[0]),
223
- });
224
- // But conditionally join into the alias table
225
- const sub = ef.subFilter[key];
226
- if ((0, Aliases_1.isAlias)(sub)) {
227
- const a = getAlias(field.otherMetadata().tableName);
228
- addTable(field.otherMetadata(), a, "outer", (0, keywords_1.kqDot)(ja, field.columnNames[1]), (0, keywords_1.kqDot)(a, "id"), sub);
229
- }
230
- const f = parseEntityFilter(field.otherMetadata(), sub);
231
- // Probe the filter and see if it's just an id, if so we can avoid the join
232
- if (!f) {
233
- // skip
234
- }
235
- else if (f.kind === "join" || filterSoftDeletes(field.otherMetadata(), softDeletes)) {
236
- const a = getAlias(field.otherMetadata().tableName);
237
- addTable(field.otherMetadata(), a, "outer", (0, keywords_1.kqDot)(ja, field.columnNames[1]), (0, keywords_1.kqDot)(a, "id"), ef.subFilter[key]);
238
- }
239
- else {
240
- const meta = field.otherMetadata();
241
- // We normally don't have `columns` for m2m fields, b/c they don't go through normal serde
242
- // codepaths, so make one up to leverage the existing `mapToDb` function.
243
- const column = {
244
- columnName: field.columnNames[1],
245
- dbType: meta.idDbType,
246
- mapToDb(value) {
247
- // Check for `typeof value === number` in case this is a new entity, and we've been given the nilIdValue
248
- return value === null || isNilIdValue(value)
249
- ? value
250
- : (0, index_1.keyToNumber)(meta, (0, index_1.maybeResolveReferenceToId)(value));
251
- },
252
- };
253
- cb.addSimpleCondition({
254
- kind: "column",
255
- alias: ja,
256
- column: field.columnNames[1],
257
- dbType: meta.idDbType,
258
- cond: mapToDb(column, f),
259
- });
260
- }
362
+ };
363
+ const a = getAlias(field.otherMetadata().tableName);
364
+ const condition = `${(0, keywords_1.kqDot)(ja, field.columnNames[1])} = ${(0, keywords_1.kqDot)(a, "id")}`;
365
+ addLateralJoin(field.otherMetadata(), alias, a, ef.subFilter[key], { kind: "raw", aliases: [ja, a], condition, bindings: [], pruneable: true }, jt);
261
366
  }
262
367
  else {
263
368
  throw new Error(`Unsupported field ${key}`);
@@ -269,8 +374,10 @@ function parseFindQuery(meta, filter, opts = {}) {
269
374
  cb.addValueFilter(alias, column, ef);
270
375
  }
271
376
  }
272
- function addOrderBy(meta, alias, orderBy) {
377
+ function addOrderBy(meta, alias, orderBy, lateralJoins) {
273
378
  const entries = Object.entries(orderBy);
379
+ // If we're recursing for lateral joins, look in the local query's tables,
380
+ const tables = (lateralJoins[lateralJoins.length - 1]?.query ?? query).tables;
274
381
  if (entries.length === 0)
275
382
  return;
276
383
  for (const [key, value] of entries) {
@@ -279,33 +386,73 @@ function parseFindQuery(meta, filter, opts = {}) {
279
386
  const field = meta.allFields[key] ?? (0, utils_1.fail)(`${key} not found on ${meta.tableName}`);
280
387
  if (field.kind === "primitive" || field.kind === "primaryKey" || field.kind === "enum") {
281
388
  const column = field.serde.columns[0];
282
- orderBys.push({
283
- alias: `${alias}${field.aliasSuffix ?? ""}`,
284
- column: column.columnName,
285
- order: value,
286
- });
389
+ if (lateralJoins.length === 0) {
390
+ // We can add the orderBy directly against the column
391
+ orderBys.push({
392
+ alias: `${alias}${field.aliasSuffix}`,
393
+ column: column.columnName,
394
+ order: value,
395
+ });
396
+ }
397
+ else {
398
+ // We need to orderBy the summed column
399
+ const [topJoin, ...rest] = lateralJoins;
400
+ const lastJoin = rest[rest.length - 1] ?? topJoin;
401
+ const as = `_${alias}_${column.columnName}_sum`;
402
+ lastJoin.query.selects.push({
403
+ sql: `SUM(${alias}${field.aliasSuffix}.${column.columnName}) AS ${as}`,
404
+ aliases: [alias],
405
+ bindings: [],
406
+ });
407
+ for (const join of lateralJoins) {
408
+ if (join !== lastJoin) {
409
+ join.query.selects.push({
410
+ sql: `SUM(${alias}.${as}) AS ${as}`,
411
+ aliases: [join.alias],
412
+ bindings: [],
413
+ });
414
+ }
415
+ }
416
+ orderBys.push({ alias: `${topJoin.alias}`, column: as, order: value });
417
+ }
287
418
  }
288
419
  else if (field.kind === "m2o") {
289
420
  // Do we already this table joined in?
290
421
  let table = tables.find((t) => t.table === field.otherMetadata().tableName);
291
- if (table) {
292
- addOrderBy(field.otherMetadata(), table.alias, value);
293
- }
294
- else {
295
- const table = field.otherMetadata().tableName;
296
- const a = getAlias(table);
422
+ if (!table) {
423
+ const { tableName } = field.otherMetadata();
424
+ const a = getAlias(tableName);
297
425
  const column = field.serde.columns[0].columnName;
298
- // If we don't have a join, don't force this to be an inner join
299
- tables.push({
426
+ table = {
300
427
  alias: a,
301
- table,
302
- join: "outer",
428
+ table: tableName,
429
+ // If we don't have a join, don't force this to be an inner join
430
+ join: "outer", // don't drop the entity just b/c of a missing order by
303
431
  col1: (0, keywords_1.kqDot)(alias, column),
304
432
  col2: (0, keywords_1.kqDot)(a, "id"),
305
- distinct: false,
306
- });
307
- addOrderBy(field.otherMetadata(), a, value);
433
+ };
434
+ tables.push(table);
435
+ }
436
+ addOrderBy(field.otherMetadata(), table.alias, value, lateralJoins);
437
+ }
438
+ else if (field.kind === "o2m") {
439
+ let table = tables.filter((t) => t.join === "lateral").find((t) => t.table === field.otherMetadata().tableName);
440
+ if (!table) {
441
+ // ...big copy/paste from up above...
442
+ const a = getAlias(field.otherMetadata().tableName);
443
+ const otherField = field.otherMetadata().allFields[field.otherFieldName];
444
+ let otherColumn = otherField.serde.columns[0].columnName;
445
+ // If the other field is a poly, we need to find the right column
446
+ if (otherField.kind === "poly") {
447
+ // For a subcomponent that matches field's metadata
448
+ const otherComponent = otherField.components.find((c) => c.otherMetadata() === meta) ??
449
+ (0, utils_1.fail)(`No poly component found for ${otherField.fieldName}`);
450
+ otherColumn = otherComponent.columnName;
451
+ }
452
+ const condition = `${(0, keywords_1.kqDot)(alias, "id")} = ${(0, keywords_1.kqDot)(a + otherField.aliasSuffix, otherColumn)}`;
453
+ table = addLateralJoin(field.otherMetadata(), alias, a, undefined, { kind: "raw", aliases: [a, alias], condition, pruneable: true, bindings: [] }, undefined, tables);
308
454
  }
455
+ addOrderBy(field.otherMetadata(), table.alias, value, [...lateralJoins, table]);
309
456
  }
310
457
  else {
311
458
  throw new Error(`Unsupported field ${key}`);
@@ -313,13 +460,14 @@ function parseFindQuery(meta, filter, opts = {}) {
313
460
  }
314
461
  }
315
462
  // always add the main table
316
- const alias = getAlias(meta.tableName);
317
- selects.push(`${(0, keywords_1.kq)(alias)}.*`);
318
- addTable(meta, alias, "primary", "n/a", "n/a", filter);
319
- // If they passed extra `conditions: ...`, parse that
320
- if (optsExpression) {
321
- cb.maybeAddExpression(optsExpression);
463
+ const alias = opts.alias ?? getAlias(meta.tableName);
464
+ if (opts.topLevelCondition === undefined) {
465
+ selects.push(`${(0, keywords_1.kq)(alias)}.*`);
466
+ }
467
+ else {
468
+ selects.push("count(*) as _");
322
469
  }
470
+ addTable(meta, alias, "primary", "n/a", "n/a", filter);
323
471
  Object.assign(query, {
324
472
  condition: cb.toExpressionFilter(),
325
473
  });
@@ -329,10 +477,10 @@ function parseFindQuery(meta, filter, opts = {}) {
329
477
  if (orderBy) {
330
478
  if (Array.isArray(orderBy)) {
331
479
  for (const ob of orderBy)
332
- addOrderBy(meta, alias, ob);
480
+ addOrderBy(meta, alias, ob, []);
333
481
  }
334
482
  else {
335
- addOrderBy(meta, alias, orderBy);
483
+ addOrderBy(meta, alias, orderBy, []);
336
484
  }
337
485
  }
338
486
  maybeAddOrderBy(query, meta, alias);
@@ -384,48 +532,62 @@ function maybeAddIdNotNulls(query) {
384
532
  }
385
533
  // Remove any joins that are not used in the select or conditions
386
534
  function pruneUnusedJoins(parsed, keepAliases) {
535
+ const dt = new DependencyTracker();
536
+ // First setup the alias -> alias dependencies...
537
+ const todo = [...parsed.tables];
538
+ while (todo.length > 0) {
539
+ const t = todo.pop();
540
+ if (t.join === "lateral") {
541
+ dt.addAlias(t.alias, [t.fromAlias]);
542
+ // Recurse into lateral joins...
543
+ todo.push(...t.query.tables);
544
+ }
545
+ else if (t.join === "cross") {
546
+ // Doesn't have any conditions
547
+ }
548
+ else if (t.join !== "primary") {
549
+ dt.addAlias(t.alias, [parseAlias(t.col1)]);
550
+ }
551
+ }
387
552
  // Mark all terminal usages
388
- const used = new Set();
389
- parsed.selects.forEach((s) => used.add(parseAlias(s)));
390
- parsed.orderBys.forEach((o) => used.add(o.alias));
391
- keepAliases.forEach((a) => used.add(a));
392
- deepFindConditions(parsed.condition)
393
- .filter((c) => !c.pruneable)
394
- .forEach((c) => {
395
- switch (c.kind) {
396
- case "column":
397
- used.add(c.alias);
398
- break;
399
- case "raw":
400
- for (const alias of c.aliases) {
401
- used.add(alias);
402
- }
403
- break;
404
- default:
405
- (0, utils_1.assertNever)(c);
553
+ parsed.selects.forEach((s) => {
554
+ if (typeof s === "string") {
555
+ if (!s.includes("count("))
556
+ dt.markRequired(parseAlias(s));
557
+ }
558
+ else {
559
+ for (const a of s.aliases)
560
+ dt.markRequired(a);
406
561
  }
407
562
  });
408
- // Mark all usages via joins
409
- for (let i = 0; i < parsed.tables.length; i++) {
410
- const t = parsed.tables[i];
411
- if (t.join !== "primary") {
412
- // If alias (col2) is required, ensure the col1 alias is also required
413
- const a2 = t.alias;
414
- const a1 = parseAlias(t.col1);
415
- if (used.has(a2) && !used.has(a1)) {
416
- used.add(a1);
417
- // Restart at zero to find dependencies before us
418
- i = 0;
563
+ parsed.orderBys.forEach((o) => dt.markRequired(o.alias));
564
+ keepAliases.forEach((a) => dt.markRequired(a));
565
+ // Look recursively into lateral join conditions
566
+ const todo2 = [parsed];
567
+ while (todo2.length > 0) {
568
+ const query = todo2.pop();
569
+ deepFindConditions(query.condition, true).forEach((c) => {
570
+ if (c.kind === "column") {
571
+ dt.markRequired(c.alias);
419
572
  }
420
- }
573
+ else if (c.kind === "raw") {
574
+ for (const alias of c.aliases)
575
+ dt.markRequired(alias);
576
+ }
577
+ else {
578
+ (0, utils_1.assertNever)(c);
579
+ }
580
+ });
581
+ todo2.push(...query.tables.filter((t) => t.join === "lateral").map((t) => t.query));
421
582
  }
422
583
  // Now remove any unused joins
423
- parsed.tables = parsed.tables.filter((t) => used.has(t.alias));
584
+ parsed.tables = parsed.tables.filter((t) => dt.required.has(t.alias));
424
585
  // And then remove any inline soft-delete conditions we don't need anymore
425
586
  if (parsed.condition && parsed.condition.op === "and") {
426
587
  parsed.condition.conditions = parsed.condition.conditions.filter((c) => {
427
588
  if (c.kind === "column") {
428
- const prune = c.pruneable && !parsed.tables.some((t) => t.alias === c.alias);
589
+ const prune = c.pruneable && !dt.required.has(c.alias);
590
+ // if (prune) console.log(`DROPPING`, c);
429
591
  return !prune;
430
592
  }
431
593
  else {
@@ -433,9 +595,13 @@ function pruneUnusedJoins(parsed, keepAliases) {
433
595
  }
434
596
  });
435
597
  }
598
+ // Remove any `{ and: [...] }`s that are empty; we should probably do this deeply?
599
+ if (parsed.condition && parsed.condition.conditions.length === 0) {
600
+ parsed.condition = undefined;
601
+ }
436
602
  }
437
603
  /** Pulls out a flat list of all `ColumnCondition`s from a `ParsedExpressionFilter` tree. */
438
- function deepFindConditions(condition) {
604
+ function deepFindConditions(condition, filterPruneable) {
439
605
  const todo = condition ? [condition] : [];
440
606
  const result = [];
441
607
  while (todo.length !== 0) {
@@ -444,8 +610,12 @@ function deepFindConditions(condition) {
444
610
  if (c.kind === "exp") {
445
611
  todo.push(c);
446
612
  }
613
+ else if (c.kind === "column" || c.kind === "raw") {
614
+ if (!filterPruneable || !c.pruneable)
615
+ result.push(c);
616
+ }
447
617
  else {
448
- result.push(c);
618
+ (0, utils_1.assertNever)(c);
449
619
  }
450
620
  }
451
621
  }
@@ -681,6 +851,14 @@ class ConditionBuilder {
681
851
  addSimpleCondition(condition) {
682
852
  this.conditions.push(condition);
683
853
  }
854
+ /** Adds an already-db-level condition to the simple conditions list. */
855
+ addRawCondition(condition) {
856
+ this.conditions.push({
857
+ kind: "raw",
858
+ bindings: [],
859
+ ...condition,
860
+ });
861
+ }
684
862
  /** Adds an already-db-level expression to the expressions list. */
685
863
  addParsedExpression(parsed) {
686
864
  this.expressions.push(parsed);
@@ -733,12 +911,83 @@ class ConditionBuilder {
733
911
  // If no inline conditions, and just 1 opt expression, just use that
734
912
  return expressions[0];
735
913
  }
914
+ else if (expressions.length === 1 && expressions[0].op === "and") {
915
+ // Merge the 1 `AND` expression with the other simple conditions
916
+ return { kind: "exp", op: "and", conditions: [...conditions, ...expressions[0].conditions] };
917
+ }
736
918
  else if (conditions.length > 0 || expressions.length > 0) {
737
919
  // Combine the conditions within the `em.find` join literal & the `conditions` as ANDs
738
920
  return { kind: "exp", op: "and", conditions: [...conditions, ...expressions] };
739
921
  }
740
922
  return undefined;
741
923
  }
924
+ /**
925
+ * Finds `child.column.eq(...)` complex conditions that need pushed down into each lateral join.
926
+ *
927
+ * Once we find something like `{ column: "first_name", cond: { eq: "a1" } }`, we return it to the
928
+ * caller (for injection into the lateral join's `SELECT` clause), and replace it with a boolean
929
+ * expression that is basically "did any of my children match this condition?".
930
+ *
931
+ * We also assume that `findAndRewrite` is only called on the top-level/user-facing `ParsedFindQuery`,
932
+ * and not any intermediate `LateralJoinTable` queries (which are allowed to have their own
933
+ * `ConditionBuilder`s for building their internal query, but it's not exposed to the user,
934
+ * so won't have any truly-complex conditions that should need rewritten).
935
+ *
936
+ * @param topLevelLateralJoin the outermost lateral join alias, as that will be the only alias
937
+ * that is visible to the rewritten condition, i.e. `_alias._whatever_condition_`.
938
+ * @param alias the alias being "hidden" in a lateral join, and so its columns/data won't be
939
+ * available for the top-level condition to directly AND/OR against.
940
+ */
941
+ findAndRewrite(topLevelLateralJoin, alias) {
942
+ let j = 0;
943
+ const found = [];
944
+ const todo = [this.conditions];
945
+ for (const exp of this.expressions)
946
+ todo.push(exp.conditions);
947
+ while (todo.length > 0) {
948
+ const array = todo.pop();
949
+ array.forEach((cond, i) => {
950
+ if (cond.kind === "column") {
951
+ // Use startsWith to look for `_b0` / `_s0` base/subtype conditions
952
+ if (cond.alias === alias || cond.alias.startsWith(`${alias}_`)) {
953
+ if (cond.column === "_")
954
+ return; // Hack to skip rewriting `alias._ > 0`
955
+ const as = `_${alias}_${cond.column}_${j++}`;
956
+ array[i] = {
957
+ kind: "raw",
958
+ aliases: [topLevelLateralJoin],
959
+ condition: `${topLevelLateralJoin}.${as}`,
960
+ bindings: [],
961
+ pruneable: false,
962
+ ...{ rewritten: true },
963
+ };
964
+ found.push({ cond, as });
965
+ }
966
+ }
967
+ else if (cond.kind === "exp") {
968
+ todo.push(cond.conditions);
969
+ }
970
+ else if (cond.kind === "raw") {
971
+ // what would we do here?
972
+ if (cond.aliases.includes(alias)) {
973
+ // Look for a hacky hint that this is our own already-rewritten query; this is likely
974
+ // because `findAndRewrite` is mutating condition expressions that get passed into
975
+ // `parseFindQuery` multiple times, i.e. while batching/dataloading.
976
+ //
977
+ // ...although in theory parseExpression should already be making a copy of any user-facing
978
+ // `em.find` conditions. :thinking:
979
+ if ("rewritten" in cond)
980
+ return;
981
+ throw new Error("Joist doesn't support raw conditions in lateral joins yet");
982
+ }
983
+ }
984
+ else {
985
+ (0, utils_1.assertNever)(cond);
986
+ }
987
+ });
988
+ }
989
+ return found;
990
+ }
742
991
  }
743
992
  exports.ConditionBuilder = ConditionBuilder;
744
993
  /** Converts domain-level values like string ids/enums into their db equivalent. */
@@ -835,7 +1084,6 @@ function addTablePerClassJoinsAndClassTag(query, meta, alias, isPrimary) {
835
1084
  join: "outer",
836
1085
  col1: (0, keywords_1.kqDot)(alias, "id"),
837
1086
  col2: `${alias}_b${i}.id`,
838
- distinct: false,
839
1087
  });
840
1088
  });
841
1089
  // We always join in the base table in case a query happens to use
@@ -855,7 +1103,6 @@ function addTablePerClassJoinsAndClassTag(query, meta, alias, isPrimary) {
855
1103
  join: "outer",
856
1104
  col1: (0, keywords_1.kqDot)(alias, "id"),
857
1105
  col2: `${alias}_s${i}.id`,
858
- distinct: false,
859
1106
  });
860
1107
  for (const field of Object.values(st.fields)) {
861
1108
  if (field.fieldName !== "id" && field.serde) {
@@ -881,16 +1128,25 @@ function addTablePerClassJoinsAndClassTag(query, meta, alias, isPrimary) {
881
1128
  }
882
1129
  }
883
1130
  }
884
- function maybeAddNotSoftDeleted(conditions, meta, alias, softDeletes) {
1131
+ function maybeAddNotSoftDeleted(
1132
+ // Within this file we pass ConditionBuilder, but findByUniqueDataLoader passes ColumnCondition[]
1133
+ cb, softDeletes, meta, alias) {
885
1134
  if (filterSoftDeletes(meta, softDeletes)) {
886
1135
  const column = meta.allFields[(0, EntityMetadata_1.getBaseMeta)(meta).timestampFields.deletedAt].serde?.columns[0];
887
- conditions.push({
1136
+ const condition = {
888
1137
  kind: "column",
889
1138
  alias,
890
1139
  column: column.columnName,
891
1140
  dbType: column.dbType,
892
1141
  cond: { kind: "is-null" },
893
- });
1142
+ pruneable: true,
1143
+ };
1144
+ if (cb instanceof ConditionBuilder) {
1145
+ cb.addSimpleCondition(condition);
1146
+ }
1147
+ else {
1148
+ cb.push(condition);
1149
+ }
894
1150
  }
895
1151
  }
896
1152
  function filterSoftDeletes(meta, softDeletes) {
@@ -899,12 +1155,15 @@ function filterSoftDeletes(meta, softDeletes) {
899
1155
  // We don't support CTI subtype soft-delete filtering yet
900
1156
  (meta.inheritanceType !== "cti" || meta.baseTypes.length === 0));
901
1157
  }
1158
+ /** Parses user-facing `{ and: ... }` or `{ or: ... }` into a `ParsedExpressionFilter`. */
902
1159
  function parseExpression(expression) {
1160
+ // Look for `{ and: [...] }` or `{ or: [...] }`
903
1161
  const [op, expressions] = "and" in expression && expression.and
904
1162
  ? ["and", expression.and]
905
1163
  : "or" in expression && expression.or
906
1164
  ? ["or", expression.or]
907
1165
  : (0, utils_1.fail)(`Invalid expression ${expression}`);
1166
+ // Potentially recurse into nested expressions
908
1167
  const conditions = expressions.map((exp) => (exp && ("and" in exp || "or" in exp) ? parseExpression(exp) : exp));
909
1168
  const [skip, valid] = (0, utils_1.partition)(conditions, (cond) => cond === undefined || cond === exports.skipCondition);
910
1169
  if ((skip.length > 0 && expression.pruneIfUndefined === "any") || valid.length === 0) {
@@ -915,24 +1174,23 @@ function parseExpression(expression) {
915
1174
  function getTables(query) {
916
1175
  let primary;
917
1176
  const joins = [];
1177
+ const laterals = [];
1178
+ const crosses = [];
918
1179
  for (const table of query.tables) {
919
1180
  if (table.join === "primary") {
920
1181
  primary = table;
921
1182
  }
1183
+ else if (table.join === "lateral") {
1184
+ laterals.push(table);
1185
+ }
1186
+ else if (table.join === "cross") {
1187
+ crosses.push(table);
1188
+ }
922
1189
  else {
923
1190
  joins.push(table);
924
1191
  }
925
1192
  }
926
- return [primary, joins];
927
- }
928
- function joinKeywords(join) {
929
- return join.join === "inner" ? "JOIN" : "LEFT OUTER JOIN";
930
- }
931
- function joinClause(join) {
932
- return `${joinKeywords(join)} ${(0, keywords_1.kq)(join.table)} ${(0, keywords_1.kq)(join.alias)} ON ${join.col1} = ${join.col2}`;
933
- }
934
- function joinClauses(joins) {
935
- return joins.map((t) => (t.join !== "primary" ? joinClause(t) : ""));
1193
+ return [primary, joins, laterals, crosses];
936
1194
  }
937
1195
  function needsClassPerTableJoins(meta) {
938
1196
  return meta.inheritanceType === "cti" && (meta.subTypes.length > 0 || meta.baseTypes.length > 0);
@@ -951,8 +1209,60 @@ function addStiSubtypeFilter(cb, subtypeMeta, alias) {
951
1209
  cond: { kind: "eq", value: subtypeMeta.stiDiscriminatorValue },
952
1210
  });
953
1211
  }
1212
+ /**
1213
+ * Given a filter that might be an `alias(Author)` placeholder, or have an `as: author`
1214
+ * binding, tells the `Alias` its canonical meta/alias.
1215
+ *
1216
+ * That way, when we later walk `conditions` and build the `AND/OR` tree, each condition
1217
+ * will know the canonical alias to output into the SQL clause.
1218
+ */
1219
+ function bindAlias(filter, meta, alias) {
1220
+ // The user's locally declared aliases, i.e. `const [a, b] = aliases(Author, Book)`,
1221
+ // aren't guaranteed to line up with the aliases we've assigned internally, like `a`
1222
+ // might actually be `a1` if there are two `authors` tables in the query, so push the
1223
+ // canonical alias value for the current clause into the Alias.
1224
+ if (filter && typeof filter === "object" && "as" in filter && (0, Aliases_1.isAlias)(filter.as)) {
1225
+ filter.as[Aliases_1.aliasMgmt].setAlias(meta, alias);
1226
+ }
1227
+ else if ((0, Aliases_1.isAlias)(filter)) {
1228
+ filter[Aliases_1.aliasMgmt].setAlias(meta, alias);
1229
+ }
1230
+ }
954
1231
  /** Converts a search term like `foo bar` into a SQL `like` pattern like `%foo%bar%`. */
955
1232
  function makeLike(search) {
956
1233
  return search ? `%${search.replace(/\s+/g, "%")}%` : undefined;
957
1234
  }
1235
+ /** Track join dependencies for `pruneUnusedJoins`. */
1236
+ class DependencyTracker {
1237
+ nodes = new Map();
1238
+ required = new Set();
1239
+ addAlias(alias, dependencies = []) {
1240
+ this.nodes.set(alias, dependencies);
1241
+ }
1242
+ markRequired(alias) {
1243
+ const marked = new Set();
1244
+ const todo = [alias];
1245
+ while (todo.length > 0) {
1246
+ const alias = todo.pop();
1247
+ if (!marked.has(alias)) {
1248
+ marked.add(alias);
1249
+ todo.push(...(this.nodes.get(alias) || []));
1250
+ }
1251
+ }
1252
+ this.required = new Set([...this.required, ...marked]);
1253
+ }
1254
+ }
1255
+ /** Takes a `{ column: "$count", kind: eq/gt/etc }` and turns it into a ParsedSelect. */
1256
+ function buildCountStar(cc) {
1257
+ // Reuse buildCondition to get the et/gt/etc --> operator
1258
+ const [op, bindings] = (0, buildUtils_1.buildCondition)(cc.cond);
1259
+ // But swap the dummy column name with `count(*)`
1260
+ const parts = op.split(" ");
1261
+ parts[0] = "count(*)";
1262
+ return {
1263
+ sql: `${parts.join(" ")} as ${cc.as}`,
1264
+ aliases: [cc.cond.alias],
1265
+ bindings,
1266
+ };
1267
+ }
958
1268
  //# sourceMappingURL=QueryParser.js.map