joist-core 2.3.0-next.76 → 2.3.0-next.78

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/build/query.js CHANGED
@@ -58,6 +58,46 @@ function query(q) {
58
58
  else return newSubqueryProxy(handle);
59
59
  }
60
60
  /**
61
+ * Declares a `WITH RECURSIVE` CTE from its two terms, and returns the CTE as a readable value.
62
+ *
63
+ * `base` is the non-recursive term, which seeds the rows and, as in PostgreSQL, supplies the CTE's
64
+ * columns. `step` is the recursive term: it receives the CTE itself, so it can join back to the rows
65
+ * found so far. The two terms are combined with UNION ALL, or UNION when `union: "distinct"` drops
66
+ * duplicates, which is how a cyclic graph is kept from looping forever.
67
+ *
68
+ * ```ts
69
+ * const [a] = tables(Author);
70
+ * const tree = recursiveQuery(
71
+ * "tree",
72
+ * { from: a, where: a.mentor_id.isNull(), select: { id: a.id, mentorId: a.mentor_id } },
73
+ * (self) => ({
74
+ * from: a,
75
+ * join: [{ inner: self, on: a.mentor_id.eq(self.id) }],
76
+ * select: { id: a.id, mentorId: a.mentor_id },
77
+ * }),
78
+ * );
79
+ * const rows = await em.query({ with: tree, from: tree, select: tree });
80
+ * ```
81
+ *
82
+ * Unlike `query()`, the name is required, because the step term must name it.
83
+ */
84
+ function recursiveQuery(name, base, step, opts = {}) {
85
+ if (typeof name !== "string" || name === "") fail("A recursive CTE needs a name");
86
+ if (opts.union !== void 0 && opts.union !== "all" && opts.union !== "distinct") fail("A recursive CTE's union must be 'all' or 'distinct'");
87
+ const handle = new SubqueryHandle(toQuery(base), true);
88
+ if (handle.output().kind !== "pojo") fail("A recursive CTE's base term needs a named projection");
89
+ const self = newSubqueryProxy(handle);
90
+ const operands = [base, step(self)];
91
+ handle.setBody(opts.union === "distinct" ? {
92
+ union: operands,
93
+ as: name
94
+ } : {
95
+ unionAll: operands,
96
+ as: name
97
+ });
98
+ return self;
99
+ }
100
+ /**
61
101
  * Builds a SQL expression from a tagged template.
62
102
  *
63
103
  * For an Author alias `a` assigned the SQL alias `a1`:
@@ -146,9 +186,23 @@ function projectionToSql(select, ctx) {
146
186
  }
147
187
  /** The runtime identity of a `query(...)` value; `Ctx.aliasFor` keys on it, like a table's `TableMgmt`. */
148
188
  var SubqueryHandle = class {
149
- q;
150
- constructor(q) {
151
- this.q = q;
189
+ recursive;
190
+ #q;
191
+ constructor(q, recursive = false) {
192
+ this.recursive = recursive;
193
+ this.#q = q;
194
+ }
195
+ get q() {
196
+ return this.#q;
197
+ }
198
+ /**
199
+ * Gives a recursive CTE its finished body. `recursiveQuery` starts the handle on its base term, so
200
+ * the step term can read the CTE's columns while the body that will hold that step is still being
201
+ * built. The base term supplies the CTE's columns either way, PostgreSQL's rule for a recursive WITH.
202
+ */
203
+ setBody(q) {
204
+ if (!this.recursive) fail("Only a recursive CTE replaces its body");
205
+ this.#q = q;
152
206
  }
153
207
  get name() {
154
208
  return this.q.as;
@@ -312,6 +366,7 @@ const MUTATION_KEYS = [
312
366
  "allowAll"
313
367
  ];
314
368
  const READ_KEYS = [
369
+ "with",
315
370
  "from",
316
371
  "join",
317
372
  "where",
@@ -368,6 +423,7 @@ function setOperands(q) {
368
423
  if (keys.length !== 1) fail("A set query requires exactly one operation key");
369
424
  validateQueryKeys(q, [
370
425
  keys[0],
426
+ "with",
371
427
  "orderBy",
372
428
  "limit",
373
429
  "offset",
@@ -491,10 +547,13 @@ var OutputExpr = class extends BaseExpr {
491
547
  * output positions without moving DISTINCT/order/pagination or repeating volatile selected expressions.
492
548
  * Parenthesizing each accumulated left side preserves array association and explicit nested grouping.
493
549
  */
494
- function parseSetQuery(q, parent, assigner) {
550
+ function parseSetQuery(q, parent, assigner, recursiveSelf) {
495
551
  const [operation, operands] = setOperands(q);
496
552
  const output = queryOutput(q);
497
- const plans = operands.map((operand) => parseQuery(toQuery(operand), parent, assigner));
553
+ const ctx = new Ctx(assigner, parent);
554
+ if (recursiveSelf) ctx.setRecursiveSelf(recursiveSelf);
555
+ const ctes = registerCtes(q, ctx, assigner);
556
+ const plans = operands.map((operand) => parseQuery(toQuery(operand), ctx, assigner));
498
557
  let sql = "";
499
558
  for (const plan of plans) {
500
559
  let branch = plan.sql;
@@ -515,10 +574,25 @@ function parseSetQuery(q, parent, assigner) {
515
574
  sql += " OFFSET ?";
516
575
  bindings.push(q.offset);
517
576
  }
577
+ const referenced = new Set(plans.flatMap((plan) => plan.outerRefs));
578
+ const keptCtes = [];
579
+ for (let i = ctes.length - 1; i >= 0; i--) {
580
+ const cte = ctes[i];
581
+ if (!referenced.has(cte.alias)) continue;
582
+ for (const ref of cte.plan.outerRefs) referenced.add(ref);
583
+ keptCtes.unshift(cte);
584
+ }
585
+ if (keptCtes.length > 0) {
586
+ const clause = withFragment(keptCtes);
587
+ sql = clause.sql + sql;
588
+ bindings.unshift(...clause.bindings);
589
+ }
590
+ const cteAliases = new Set(ctes.map((cte) => cte.alias));
591
+ const outerRefs = [...ctx.outerRefs, ...plans.flatMap((plan) => plan.outerRefs)];
518
592
  return {
519
593
  sql,
520
594
  bindings,
521
- outerRefs: [...new Set(plans.flatMap((plan) => plan.outerRefs))],
595
+ outerRefs: [...new Set(outerRefs.filter((ref) => !cteAliases.has(ref)))],
522
596
  output,
523
597
  decodeRows: plans[0].decodeRows
524
598
  };
@@ -549,14 +623,84 @@ var Ctx = class {
549
623
  assigner;
550
624
  parent;
551
625
  aliases = /* @__PURE__ */ new Map();
626
+ /**
627
+ * The subset of `aliases` that are CTE names. A `from`/`join` on one of these emits just the name,
628
+ * i.e. `FROM book_stats`, where an ordinary `query(...)` value emits its whole body inline, i.e.
629
+ * `FROM (SELECT ...) AS sq`.
630
+ */
631
+ ctes = /* @__PURE__ */ new Map();
632
+ /**
633
+ * `with` entries named but not yet turned into SQL. `registerCtes` names every entry first, then
634
+ * generates the bodies one at a time, so while one body is being generated the entries after it sit
635
+ * here. Reading one of those is a forward reference.
636
+ */
637
+ pendingCtes = /* @__PURE__ */ new Map();
638
+ /** CTEs already read by this query's `from`/`join`, so a second read fails instead of colliding. */
639
+ usedCtes = /* @__PURE__ */ new Set();
640
+ /** The recursive CTE whose body this query is part of, if any; see `recursiveSelf`. */
641
+ ownRecursiveSelf;
552
642
  outerRefs = /* @__PURE__ */ new Set();
553
643
  constructor(assigner, parent) {
554
644
  this.assigner = assigner;
555
645
  this.parent = parent;
556
646
  }
647
+ /**
648
+ * One handle can hold only one alias per query, so a value used twice must be told apart, not
649
+ * silently collapsed into the second registration's alias.
650
+ */
557
651
  register(handle, alias) {
652
+ if (this.aliases.has(handle)) fail(`${describeHandle(handle)} is already in this query's \`with\`/\`from\`/\`join\`; use a separate table(...)/query(...) value for each use`);
558
653
  this.aliases.set(handle, alias);
559
654
  }
655
+ /** Names every `with` entry up front, so a CTE reading a later sibling is reported, not inlined. */
656
+ declareCte(handle, alias) {
657
+ this.pendingCtes.set(handle, alias);
658
+ }
659
+ /** Brings a pending CTE into scope, for the sources and columns that may now read it. */
660
+ promoteCte(handle) {
661
+ const alias = this.pendingCtes.get(handle) ?? fail("CTE was not declared");
662
+ this.pendingCtes.delete(handle);
663
+ this.ctes.set(handle, alias);
664
+ this.register(handle, alias);
665
+ }
666
+ /**
667
+ * The CTE name for `handle`, looking in the enclosing queries too, because a CTE is in scope for the
668
+ * whole statement.
669
+ *
670
+ * Unlike `aliasFor`, a hit in an enclosing query is not added to `outerRefs`: reading a CTE by name
671
+ * is not a correlated reference, so it must not keep an enclosing join alive.
672
+ */
673
+ cteAliasFor(handle) {
674
+ return this.ctes.get(handle) ?? this.parent?.cteAliasFor(handle);
675
+ }
676
+ /**
677
+ * The recursive CTE this query's terms belong to, inherited from the enclosing query.
678
+ *
679
+ * PostgreSQL requires a recursive term to reference its own CTE, so that reference is not optional
680
+ * and must survive pruning, unlike an ordinary explicit join that nothing else reads.
681
+ */
682
+ get recursiveSelf() {
683
+ return this.ownRecursiveSelf ?? this.parent?.recursiveSelf;
684
+ }
685
+ setRecursiveSelf(handle) {
686
+ this.ownRecursiveSelf = handle;
687
+ }
688
+ /** Whether `handle` is a CTE with no SQL yet, i.e. itself or a later `with` entry. */
689
+ isPendingCte(handle) {
690
+ return this.pendingCtes.has(handle) || (this.parent?.isPendingCte(handle) ?? false);
691
+ }
692
+ /**
693
+ * Fails if this query already reads `handle`. A `Ctx` maps each handle to a single alias, so two
694
+ * reads of one CTE value would render as the same name, i.e. `FROM tree JOIN tree`, and neither the
695
+ * SQL nor a column expression could say which one it meant. Each read needs its own `query(...)`.
696
+ *
697
+ * Only this query is checked, not the enclosing ones: a nested subquery has its own FROM, so reading
698
+ * the same CTE in there is fine.
699
+ */
700
+ useCte(handle) {
701
+ if (this.usedCtes.has(handle)) fail(`${describeHandle(handle)} is already in this query's \`from\`/\`join\`; use a separate query(...) value for each use`);
702
+ this.usedCtes.add(handle);
703
+ }
560
704
  aliasFor(handle) {
561
705
  const local = this.aliases.get(handle);
562
706
  if (local) return local;
@@ -586,11 +730,13 @@ function describeHandle(handle) {
586
730
  * 3. Prune: drop joins nothing references (see below), then reject a kept join whose ON collapsed.
587
731
  * 4. Assemble the SQL from the kept fragments, so pruned bindings disappear with their SQL.
588
732
  */
589
- function parseQuery(q, parent, assigner) {
733
+ function parseQuery(q, parent, assigner, recursiveSelf) {
590
734
  validateReadQuery(q);
591
- if (isSetQuery(q)) return parseSetQuery(q, parent, assigner);
735
+ if (isSetQuery(q)) return parseSetQuery(q, parent, assigner, recursiveSelf);
592
736
  const ctx = new Ctx(assigner, parent);
737
+ if (recursiveSelf) ctx.setRecursiveSelf(recursiveSelf);
593
738
  const joinEntries = [...q.join ?? []].filter(isDefined);
739
+ const ctes = registerCtes(q, ctx, assigner);
594
740
  const parseFrom = registerSource(q.from, ctx, assigner);
595
741
  const pendingJoins = joinEntries.flatMap((j) => {
596
742
  const kind = "inner" in j && j.inner ? "inner" : "left";
@@ -635,7 +781,7 @@ function parseQuery(q, parent, assigner) {
635
781
  const having = conditionToSql(q.having, ctx, true);
636
782
  const groupBys = (q.groupBy ?? []).map((g) => asExpr(g, "groupBy").toSql(ctx));
637
783
  const orderBys = orderBysToSql(q, ctx);
638
- const kept = pruneJoins(q, from, joins, [
784
+ const { joins: kept, ctes: keptCtes } = pruneJoins(q, ctx, from, joins, ctes, [
639
785
  ...selects,
640
786
  ...groupBys,
641
787
  ...orderBys,
@@ -650,6 +796,7 @@ function parseQuery(q, parent, assigner) {
650
796
  if (forward) fail(`Join ${describeHandle(j.source.handle)} references '${forward}', which is joined later; move that join earlier in the join array`);
651
797
  }
652
798
  const out = [];
799
+ if (keptCtes.length > 0) out.push(withFragment(keptCtes));
653
800
  out.push({
654
801
  sql: `SELECT ${q.distinct ? "DISTINCT " : ""}`,
655
802
  bindings: [],
@@ -713,6 +860,20 @@ function parseQuery(q, parent, assigner) {
713
860
  */
714
861
  function registerSource(source, ctx, assigner) {
715
862
  const handle = handleOf(source);
863
+ const cteAlias = ctx.cteAliasFor(handle);
864
+ if (cteAlias) {
865
+ ctx.useCte(handle);
866
+ return () => ({
867
+ handle,
868
+ alias: cteAlias,
869
+ sql: safeKq(cteAlias),
870
+ bindings: [],
871
+ refs: [],
872
+ entitySelects: [],
873
+ meta: void 0
874
+ });
875
+ }
876
+ if (ctx.isPendingCte(handle)) fail(`${describeHandle(handle)} is declared later in this query's \`with\`; a CTE can only read earlier ones, and cannot read itself`);
716
877
  if (handle instanceof SubqueryHandle) {
717
878
  const alias = handle.name ? assigner.getLiteralAlias(handle.name) : assigner.getLiteralAlias("sq");
718
879
  ctx.register(handle, alias);
@@ -1057,14 +1218,28 @@ function refsOf(parsed) {
1057
1218
  * explicit `{ inner: b, on }` here does filter rows, so pruning it when unreferenced drops that filter;
1058
1219
  * that matches `{ books: { title: undefined } }` in em.find and is deliberate. `keep: true` pins it, and
1059
1220
  * a pure existence filter is better written as `a.id.in(query({ ... }))`, which is never `undefined`.
1221
+ *
1222
+ * CTEs prune on the same rule and through the same dependency map: a `with` entry nothing reads
1223
+ * anymore drops with the join that read it, and a CTE read only by another CTE survives with it.
1224
+ * A CTE joined into the query shares its alias with that join, so their dependencies are merged.
1225
+ *
1226
+ * The one join that never prunes is a recursive term's reference to its own CTE: PostgreSQL requires
1227
+ * it, so it is not the caller's optional filter (see `Ctx.recursiveSelf`).
1060
1228
  */
1061
- function pruneJoins(q, from, joins, used) {
1062
- if (q.pruneJoins === false) return joins;
1229
+ function pruneJoins(q, ctx, from, joins, ctes, used) {
1230
+ if (q.pruneJoins === false) return {
1231
+ joins,
1232
+ ctes
1233
+ };
1063
1234
  const deps = /* @__PURE__ */ new Map();
1064
- for (const j of joins) {
1065
- const refs = [...j.userOn?.refs ?? [], ...j.source.refs].filter((r) => r !== j.source.alias);
1066
- deps.set(j.source.alias, refs);
1235
+ function addDeps(alias, refs) {
1236
+ const own = refs.filter((r) => r !== alias);
1237
+ const existing = deps.get(alias);
1238
+ if (existing) existing.push(...own);
1239
+ else deps.set(alias, own);
1067
1240
  }
1241
+ for (const j of joins) addDeps(j.source.alias, [...j.userOn?.refs ?? [], ...j.source.refs]);
1242
+ for (const c of ctes) addDeps(c.alias, c.plan.outerRefs);
1068
1243
  const required = /* @__PURE__ */ new Set();
1069
1244
  function markRequired(alias) {
1070
1245
  if (required.has(alias)) return;
@@ -1074,7 +1249,53 @@ function pruneJoins(q, from, joins, used) {
1074
1249
  markRequired(from.alias);
1075
1250
  for (const r of used.flatMap((u) => u.refs)) markRequired(r);
1076
1251
  for (const j of joins) if (j.keep) markRequired(j.source.alias);
1077
- return joins.filter((j) => required.has(j.source.alias));
1252
+ for (const j of joins) if (j.source.handle === ctx.recursiveSelf) markRequired(j.source.alias);
1253
+ return {
1254
+ joins: joins.filter((j) => required.has(j.source.alias)),
1255
+ ctes: ctes.filter((c) => required.has(c.alias))
1256
+ };
1257
+ }
1258
+ /**
1259
+ * Names and parses each `with` entry, in declaration order.
1260
+ *
1261
+ * Each entry is in scope before the next is parsed, so a CTE can read an *earlier* sibling by name,
1262
+ * PostgreSQL's rule for a non-recursive WITH. A forward reference, or a CTE reading the query's own
1263
+ * `from`, fails as "not in this query's from/join", the same error any out-of-scope source gets.
1264
+ *
1265
+ * I.e. `with: [totals, ranked]` parses `totals` first, so `ranked` can join it, but not the reverse.
1266
+ */
1267
+ function registerCtes(q, ctx, assigner) {
1268
+ const handles = (q.with === void 0 ? [] : Array.isArray(q.with) ? q.with : [q.with]).filter(isDefined).map(withEntryHandle);
1269
+ const aliases = handles.map((handle) => assigner.getLiteralAlias(handle.name ?? "cte"));
1270
+ handles.forEach((handle, i) => ctx.declareCte(handle, aliases[i]));
1271
+ return handles.map((handle, i) => {
1272
+ if (handle.recursive) ctx.promoteCte(handle);
1273
+ const plan = parseQuery(handle.q, ctx, assigner, handle.recursive ? handle : void 0);
1274
+ if (!handle.recursive) ctx.promoteCte(handle);
1275
+ return {
1276
+ alias: aliases[i],
1277
+ plan,
1278
+ recursive: handle.recursive
1279
+ };
1280
+ });
1281
+ }
1282
+ /** A CTE must be a table shape, so entity-mode and scalar values, which have no columns, are out. */
1283
+ function withEntryHandle(entry) {
1284
+ if (!isSubqueryValue(entry)) fail(entry instanceof SubqueryExpr || isEntityQueryValue(entry) ? "A `with` entry needs named columns; entity and scalar query(...) values have none" : "A `with` entry must be a query(...) value");
1285
+ return readValueHandle(entry);
1286
+ }
1287
+ /**
1288
+ * Renders `WITH a AS (...), b AS (...) `, whose bindings lead the statement, as their SQL does.
1289
+ *
1290
+ * One recursive CTE makes the whole clause `WITH RECURSIVE`, PostgreSQL's rule: the keyword is on the
1291
+ * clause, not on the entry that needs it, and it does not force the other entries to be recursive.
1292
+ */
1293
+ function withFragment(ctes) {
1294
+ return {
1295
+ sql: `WITH${ctes.some((c) => c.recursive) ? " RECURSIVE" : ""} ${ctes.map((c) => `${safeKq(c.alias)} AS (${c.plan.sql})`).join(", ")} `,
1296
+ bindings: ctes.flatMap((c) => c.plan.bindings),
1297
+ refs: []
1298
+ };
1078
1299
  }
1079
1300
  function asExpr(value, where) {
1080
1301
  if (value instanceof BaseExpr) return value;
@@ -1091,6 +1312,6 @@ function isDefined(value) {
1091
1312
  return value !== void 0;
1092
1313
  }
1093
1314
  //#endregion
1094
- export { Ctx, SubqueryHandle, conditionToSql, entityQueryBrand, injectedConditions, isReadQueryValue, parseUserQuery, projectionToSql, query, scalarQueryBrand, sql, subqueryBrand };
1315
+ export { Ctx, SubqueryHandle, conditionToSql, entityQueryBrand, injectedConditions, isReadQueryValue, parseUserQuery, projectionToSql, query, recursiveQuery, scalarQueryBrand, sql, subqueryBrand };
1095
1316
 
1096
1317
  //# sourceMappingURL=query.js.map