linkgress-orm 0.4.60 → 0.4.62

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.
@@ -710,6 +710,16 @@ class SelectQueryBuilder {
710
710
  }
711
711
  return this;
712
712
  }
713
+ /**
714
+ * The CTEs attached to this query through `.with(...)` (or auto-attached by
715
+ * `joinFilter(cte, ...)`). Read by {@link UnionQueryBuilder.buildSql}, which
716
+ * hoists them to statement level so a CTE declared on one union leg is
717
+ * visible to all of them.
718
+ * @internal
719
+ */
720
+ _getAttachedCtes() {
721
+ return this.ctes;
722
+ }
713
723
  /**
714
724
  * Limit results
715
725
  */
@@ -1345,6 +1355,9 @@ class SelectQueryBuilder {
1345
1355
  allParams: context.params,
1346
1356
  collectionStrategy: this.collectionStrategy,
1347
1357
  executor: this.executor,
1358
+ // CTEs the union already declared ahead of the whole statement — this leg
1359
+ // must not re-declare them (nor re-push their params).
1360
+ hoistedCteNames: context.hoistedCteNames,
1348
1361
  };
1349
1362
  const mockRow = this._createMockRow();
1350
1363
  const selectionResult = this.selector(mockRow);
@@ -4185,7 +4198,7 @@ ${joinClauses.join('\n')}`;
4185
4198
  let whereClause = '';
4186
4199
  if (this.whereCond) {
4187
4200
  const condBuilder = new conditions_1.ConditionBuilder();
4188
- const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
4201
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders, context.hoistedCteNames);
4189
4202
  whereClause = `WHERE ${sql}`;
4190
4203
  context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
4191
4204
  context.allParams.push(...params);
@@ -4321,8 +4334,14 @@ ${joinClauses.join('\n')}`;
4321
4334
  * @internal
4322
4335
  */
4323
4336
  buildQueryCore(selection, context, includeOrderLimitOffset = true) {
4324
- // Handle user-defined CTEs first - their params need to come before main query params
4337
+ // Handle user-defined CTEs first - their params need to come before main query params.
4338
+ // A CTE the enclosing builder already hoisted to statement level (UNION) has had both
4339
+ // its params and its WITH entry contributed there; pushing them again would duplicate
4340
+ // the params and shift every placeholder in this leg.
4325
4341
  for (const cte of this.ctes) {
4342
+ if (context.hoistedCteNames?.has(cte.name)) {
4343
+ continue;
4344
+ }
4326
4345
  context.allParams.push(...cte.params);
4327
4346
  context.paramCounter += cte.params.length;
4328
4347
  }
@@ -4532,7 +4551,7 @@ ${joinClauses.join('\n')}`;
4532
4551
  let whereClause = '';
4533
4552
  if (this.whereCond) {
4534
4553
  const condBuilder = new conditions_1.ConditionBuilder();
4535
- const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
4554
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders, context.hoistedCteNames);
4536
4555
  whereClause = `WHERE ${sql}`;
4537
4556
  context.paramCounter = newParamCounter;
4538
4557
  context.allParams.push(...params);
@@ -4569,6 +4588,10 @@ ${joinClauses.join('\n')}`;
4569
4588
  let finalQuery = '';
4570
4589
  const allCtes = [];
4571
4590
  for (const cte of this.ctes) {
4591
+ // Hoisted to statement level by the enclosing UNION - declared there, not here.
4592
+ if (context.hoistedCteNames?.has(cte.name)) {
4593
+ continue;
4594
+ }
4572
4595
  allCtes.push(`"${cte.name}" AS ${cte.materialized ? 'MATERIALIZED ' : ''}(${cte.query})`);
4573
4596
  }
4574
4597
  if (context.ctes.size > 0) {
@@ -5194,7 +5217,7 @@ ${joinClauses.join('\n')}`;
5194
5217
  let whereClause = '';
5195
5218
  if (this.whereCond) {
5196
5219
  const condBuilder = new conditions_1.ConditionBuilder();
5197
- const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
5220
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders, context.hoistedCteNames);
5198
5221
  whereClause = `WHERE ${sql}`;
5199
5222
  context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
5200
5223
  context.allParams.push(...params);
@@ -5251,6 +5274,18 @@ ${joinClauses.join('\n')}`;
5251
5274
  * Build aggregate query (count or exists)
5252
5275
  */
5253
5276
  buildAggregateQuery(context, type) {
5277
+ // User-defined CTEs first, exactly as buildQuery does: their params occupy the
5278
+ // opening slots of the statement because DbCte bodies carry placeholders numbered
5279
+ // from $1. Without this, `.with(cte).count()` / `.exists()` dropped the WITH clause
5280
+ // entirely and PostgreSQL answered `relation "<cte>" does not exist` (42P01) for any
5281
+ // query whose WHERE referenced the CTE.
5282
+ for (const cte of this.ctes) {
5283
+ if (context.hoistedCteNames?.has(cte.name)) {
5284
+ continue;
5285
+ }
5286
+ context.allParams.push(...cte.params);
5287
+ context.paramCounter += cte.params.length;
5288
+ }
5254
5289
  // Detect navigation property joins from WHERE condition
5255
5290
  const joins = [];
5256
5291
  this.detectAndAddJoinsFromCondition(this.whereCond, joins);
@@ -5258,7 +5293,7 @@ ${joinClauses.join('\n')}`;
5258
5293
  let whereClause = '';
5259
5294
  if (this.whereCond) {
5260
5295
  const condBuilder = new conditions_1.ConditionBuilder();
5261
- const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
5296
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders, context.hoistedCteNames);
5262
5297
  whereClause = `WHERE ${sql}`;
5263
5298
  context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
5264
5299
  context.allParams.push(...params);
@@ -5314,9 +5349,24 @@ ${joinClauses.join('\n')}`;
5314
5349
  const selectClause = type === 'count'
5315
5350
  ? 'SELECT COUNT(*) as count'
5316
5351
  : 'SELECT EXISTS(SELECT 1';
5352
+ // WITH clause - same assembly as buildQuery, so an aggregate can reference the
5353
+ // CTEs attached to the query it is aggregating.
5354
+ const allCtes = [];
5355
+ for (const cte of this.ctes) {
5356
+ if (context.hoistedCteNames?.has(cte.name)) {
5357
+ continue;
5358
+ }
5359
+ allCtes.push(`"${cte.name}" AS ${cte.materialized ? 'MATERIALIZED ' : ''}(${cte.query})`);
5360
+ }
5361
+ if (context.ctes.size > 0) {
5362
+ for (const [cteName, { sql: cteSql }] of context.ctes.entries()) {
5363
+ allCtes.push(`"${cteName}" AS (${cteSql})`);
5364
+ }
5365
+ }
5366
+ const withClause = allCtes.length > 0 ? `WITH ${allCtes.join(', ')}\n` : '';
5317
5367
  const sql = type === 'count'
5318
- ? `${selectClause}\n${fromClause}\n${whereClause}`.trim()
5319
- : `${selectClause}\n${fromClause}\n${whereClause})`.trim();
5368
+ ? `${withClause}${selectClause}\n${fromClause}\n${whereClause}`.trim()
5369
+ : `${withClause}${selectClause}\n${fromClause}\n${whereClause})`.trim();
5320
5370
  return {
5321
5371
  sql,
5322
5372
  params: context.allParams,
@@ -5355,6 +5405,11 @@ ${joinClauses.join('\n')}`;
5355
5405
  paramCounter: outerContext.paramCounter,
5356
5406
  allParams: outerContext.params,
5357
5407
  placeholders: outerContext.placeholders, // Pass placeholders through for prepared statements
5408
+ // CTEs an enclosing builder already declared at statement level stay
5409
+ // declared for anything NESTED in this subquery too. Without this the
5410
+ // signal dies at the subquery boundary and a CTE-rooted subquery one
5411
+ // level deeper re-declares (and re-materializes) a CTE it was handed.
5412
+ hoistedCteNames: outerContext.hoistedCteNames,
5358
5413
  executor: this.executor,
5359
5414
  };
5360
5415
  // Analyze the selector to extract nested queries
@@ -6089,7 +6144,7 @@ class CollectionQueryBuilder {
6089
6144
  }
6090
6145
  if (this.whereCond) {
6091
6146
  const condBuilder = new conditions_1.ConditionBuilder();
6092
- const { sql: condSql, params, placeholders, paramCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
6147
+ const { sql: condSql, params, placeholders, paramCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders, context.hoistedCteNames);
6093
6148
  context.paramCounter = paramCounter;
6094
6149
  context.params.push(...params);
6095
6150
  if (placeholders) {
@@ -6610,7 +6665,7 @@ class CollectionQueryBuilder {
6610
6665
  let whereParams;
6611
6666
  if (this.whereCond) {
6612
6667
  const condBuilder = new conditions_1.ConditionBuilder();
6613
- const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
6668
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders, context.hoistedCteNames);
6614
6669
  whereClause = sql;
6615
6670
  whereParams = params;
6616
6671
  context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)