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