linkgress-orm 0.4.44 → 0.4.46

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.
@@ -142,8 +142,18 @@ const NUMERIC_REGEX = /^-?\d+(\.\d+)?$/;
142
142
  /**
143
143
  * Query builder for a table
144
144
  */
145
+ /**
146
+ * Monotonic sequence for query-chain identities. Every root builder gets a
147
+ * fresh id; derived builders inherit it. Field refs bake the id so a
148
+ * standalone subquery can detect same-table refs leaking in from an OUTER
149
+ * chain (alias collision → the correlation would silently bind to the inner
150
+ * row — see extractOuterFieldRefs).
151
+ */
152
+ let chainIdSeq = 0;
145
153
  class QueryBuilder {
146
154
  constructor(schema, client, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, collectionStrategy, schemaRegistry) {
155
+ /** @internal Chain identity — see chainIdSeq. */
156
+ this.chainId = ++chainIdSeq;
147
157
  this.orderByFields = [];
148
158
  this.manualJoins = [];
149
159
  this.joinCounter = 0;
@@ -200,7 +210,7 @@ class QueryBuilder {
200
210
  return new SelectQueryBuilder(this.schema, this.client, selector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, // isDistinct defaults to false
201
211
  this.schemaRegistry, // Pass schema registry for nested navigation resolution
202
212
  [], // ctes - start with empty array
203
- this.collectionStrategy);
213
+ this.collectionStrategy, this.chainId);
204
214
  }
205
215
  /**
206
216
  * Add WHERE condition
@@ -222,7 +232,7 @@ class QueryBuilder {
222
232
  */
223
233
  with(...ctes) {
224
234
  return new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, this.schemaRegistry, // Pass schema registry for nested navigation resolution
225
- ctes, this.collectionStrategy);
235
+ ctes, this.collectionStrategy, this.chainId);
226
236
  }
227
237
  /**
228
238
  * Create mock row for analysis
@@ -235,6 +245,7 @@ class QueryBuilder {
235
245
  }
236
246
  const mock = {};
237
247
  const tableAlias = this.schema.name;
248
+ const chainId = this.chainId;
238
249
  // Performance: Use pre-computed column name map if available
239
250
  const columnNameMap = getColumnNameMapForSchema(this.schema);
240
251
  // Performance: Lazy-cache FieldRef objects - only create when first accessed
@@ -258,6 +269,7 @@ class QueryBuilder {
258
269
  __fieldName: colName,
259
270
  __dbColumnName: dbColumnName,
260
271
  __tableAlias: tableAlias,
272
+ __chainId: chainId,
261
273
  // Include mapper for toDriver transformation in conditions
262
274
  __mapper: mapper,
263
275
  };
@@ -324,7 +336,7 @@ class QueryBuilder {
324
336
  const qb = new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, // isDistinct defaults to false
325
337
  undefined, // schemaRegistry
326
338
  [], // ctes
327
- this.collectionStrategy);
339
+ this.collectionStrategy, this.chainId);
328
340
  return qb.leftJoinSubquery(rightTable, alias, condition, selector);
329
341
  }
330
342
  const rightSchema = rightTable._getSchema();
@@ -357,7 +369,7 @@ class QueryBuilder {
357
369
  return new SelectQueryBuilder(this.schema, this.client, wrappedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, false, // isDistinct defaults to false
358
370
  undefined, // schemaRegistry
359
371
  [], // ctes
360
- this.collectionStrategy);
372
+ this.collectionStrategy, this.chainId);
361
373
  }
362
374
  /**
363
375
  * Add an INNER JOIN to the query with a selector (supports both tables and subqueries)
@@ -373,7 +385,7 @@ class QueryBuilder {
373
385
  const qb = new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, // isDistinct defaults to false
374
386
  undefined, // schemaRegistry
375
387
  [], // ctes
376
- this.collectionStrategy);
388
+ this.collectionStrategy, this.chainId);
377
389
  return qb.innerJoinSubquery(rightTable, alias, condition, selector);
378
390
  }
379
391
  const rightSchema = rightTable._getSchema();
@@ -406,7 +418,7 @@ class QueryBuilder {
406
418
  return new SelectQueryBuilder(this.schema, this.client, wrappedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, false, // isDistinct defaults to false
407
419
  undefined, // schemaRegistry
408
420
  [], // ctes
409
- this.collectionStrategy);
421
+ this.collectionStrategy, this.chainId);
410
422
  }
411
423
  /**
412
424
  * Create mock row for a specific table/alias (for joins)
@@ -503,7 +515,7 @@ class SelectQueryBuilder {
503
515
  getQualifiedTableName(tableName, schema) {
504
516
  return schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
505
517
  }
506
- constructor(schema, client, selector, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, isDistinct, schemaRegistry, ctes, collectionStrategy) {
518
+ constructor(schema, client, selector, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, isDistinct, schemaRegistry, ctes, collectionStrategy, chainId) {
507
519
  this.orderByFields = [];
508
520
  this.manualJoins = [];
509
521
  this.joinCounter = 0;
@@ -513,6 +525,7 @@ class SelectQueryBuilder {
513
525
  this.schema = schema;
514
526
  this.client = client;
515
527
  this.selector = selector;
528
+ this.chainId = chainId ?? ++chainIdSeq;
516
529
  this.whereCond = whereCond;
517
530
  this.limitValue = limit;
518
531
  this.offsetValue = offset;
@@ -559,13 +572,61 @@ class SelectQueryBuilder {
559
572
  const firstResult = this.selector(row);
560
573
  return selector(firstResult);
561
574
  };
562
- return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy);
575
+ return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy, this.chainId);
563
576
  }
564
577
  /**
565
578
  * Add WHERE condition
566
579
  * Multiple where() calls are chained with AND logic
567
580
  * Note: The row parameter represents the selected shape (after select())
568
581
  */
582
+ /**
583
+ * INNER JOIN used purely as a row FILTER — the selection shape is preserved
584
+ * (unlike innerJoin, no selector is required and the builder stays chainable
585
+ * as the same entity/selection type). The ON condition receives (left, right)
586
+ * mocks; an optional third callback contributes an extra WHERE predicate with
587
+ * the same (left, right) arguments.
588
+ *
589
+ * Intended for scope-style predicates that hop across an N:1 FK (e.g.
590
+ * order_item → invoicing_partner_data): a JOIN against the "one" side never
591
+ * duplicates left rows. Joining a 1:N side WILL duplicate rows — callers own
592
+ * that trade-off (use exists()/inSubquery for semi-join semantics instead).
593
+ */
594
+ joinFilter(rightTable, on, filter) {
595
+ return this.addFilterJoin('INNER', rightTable, on, filter);
596
+ }
597
+ /**
598
+ * LEFT JOIN used purely as a row FILTER — see joinFilter. Useful with an
599
+ * IS NULL predicate in the filter callback for anti-join shapes
600
+ * ("rows with NO matching right row").
601
+ */
602
+ leftJoinFilter(rightTable, on, filter) {
603
+ return this.addFilterJoin('LEFT', rightTable, on, filter);
604
+ }
605
+ addFilterJoin(type, rightTable, on, filter) {
606
+ const rightSchema = rightTable._getSchema();
607
+ const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
608
+ this.joinCounter = this.joinCounter + 1;
609
+ const mockRow = this._createMockRow();
610
+ const selectedMock = this.selector(mockRow);
611
+ const leftMock = this.createFieldRefProxy(selectedMock, true);
612
+ const rightMock = this.createMockRowForTable(rightSchema, rightAlias);
613
+ const onCondition = on(leftMock, rightMock);
614
+ this.manualJoins = [
615
+ ...this.manualJoins,
616
+ {
617
+ type,
618
+ table: rightSchema.name,
619
+ alias: rightAlias,
620
+ schema: rightSchema,
621
+ condition: onCondition,
622
+ },
623
+ ];
624
+ if (filter) {
625
+ const filterCondition = filter(leftMock, rightMock);
626
+ this.whereCond = this.whereCond ? (0, conditions_1.and)(this.whereCond, filterCondition) : filterCondition;
627
+ }
628
+ return this;
629
+ }
569
630
  where(condition) {
570
631
  const mockRow = this._createMockRow();
571
632
  // Apply the selector to get the selected shape that the user sees in the WHERE condition
@@ -670,7 +731,7 @@ class SelectQueryBuilder {
670
731
  const freshMockRight = this.createMockRowForSubquery(alias, subquery);
671
732
  return selector(leftResult, freshMockRight);
672
733
  };
673
- return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy);
734
+ return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy, this.chainId);
674
735
  }
675
736
  // Implementation
676
737
  leftJoin(rightTable, condition, selector, alias) {
@@ -714,7 +775,7 @@ class SelectQueryBuilder {
714
775
  const freshMockRight = this.createMockRowForTable(rightSchema, rightAlias);
715
776
  return selector(leftResult, freshMockRight);
716
777
  };
717
- return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy);
778
+ return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy, this.chainId);
718
779
  }
719
780
  /**
720
781
  * Add a LEFT JOIN with a CTE
@@ -743,7 +804,7 @@ class SelectQueryBuilder {
743
804
  const freshMockRight = this.createMockRowForCte(cte);
744
805
  return selector(leftResult, freshMockRight);
745
806
  };
746
- return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy);
807
+ return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy, this.chainId);
747
808
  }
748
809
  /**
749
810
  * Add an INNER JOIN with a subquery
@@ -777,7 +838,7 @@ class SelectQueryBuilder {
777
838
  const freshMockRight = this.createMockRowForSubquery(alias, subquery);
778
839
  return selector(leftResult, freshMockRight);
779
840
  };
780
- return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy);
841
+ return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy, this.chainId);
781
842
  }
782
843
  /**
783
844
  * Add an INNER JOIN to the query with a selector
@@ -817,7 +878,7 @@ class SelectQueryBuilder {
817
878
  const freshMockRight = this.createMockRowForTable(rightSchema, rightAlias);
818
879
  return selector(leftResult, freshMockRight);
819
880
  };
820
- return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy);
881
+ return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy, this.chainId);
821
882
  }
822
883
  /**
823
884
  * Create mock row for a specific table/alias (for joins)
@@ -996,7 +1057,7 @@ class SelectQueryBuilder {
996
1057
  return selector(firstResult);
997
1058
  };
998
1059
  return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, true, // Set isDistinct to true
999
- this.schemaRegistry, this.ctes, this.collectionStrategy);
1060
+ this.schemaRegistry, this.ctes, this.collectionStrategy, this.chainId);
1000
1061
  }
1001
1062
  /**
1002
1063
  * Get minimum value from the query
@@ -2857,6 +2918,7 @@ ${joinClauses.join('\n')}`;
2857
2918
  _createMockRow() {
2858
2919
  const mock = {};
2859
2920
  const tableAlias = this.schema.name;
2921
+ const chainId = this.chainId;
2860
2922
  // Performance: Use pre-computed column name map if available
2861
2923
  const columnNameMap = getColumnNameMapForSchema(this.schema);
2862
2924
  // Performance: Lazy-cache FieldRef objects
@@ -2880,6 +2942,7 @@ ${joinClauses.join('\n')}`;
2880
2942
  __fieldName: colName,
2881
2943
  __dbColumnName: dbColumnName,
2882
2944
  __tableAlias: tableAlias,
2945
+ __chainId: chainId,
2883
2946
  // Include mapper for toDriver transformation in conditions
2884
2947
  __mapper: mapper,
2885
2948
  };
@@ -5007,8 +5070,23 @@ ${joinClauses.join('\n')}`;
5007
5070
  const tableAlias = ref.__tableAlias;
5008
5071
  // If the table alias doesn't match our current schema, it's from an outer query
5009
5072
  // Also check if it's not a navigation property of this table (which would be in schema.relations)
5073
+ // and not one of our own manual-join aliases (filter-joins qualify refs by join alias).
5010
5074
  if (tableAlias !== currentTableName && !this.schema.relations[tableAlias]) {
5011
- outerRefs.push(ref);
5075
+ if (!this.manualJoins.some(j => j.alias === tableAlias)) {
5076
+ outerRefs.push(ref);
5077
+ }
5078
+ }
5079
+ else if (tableAlias === currentTableName
5080
+ && ref.__chainId != null
5081
+ && this.chainId != null
5082
+ && ref.__chainId !== this.chainId) {
5083
+ // A ref over OUR table name that was created by a DIFFERENT query chain:
5084
+ // this is a same-table correlated standalone subquery. Inner and outer
5085
+ // FROM would share the alias, so the "correlation" would silently bind
5086
+ // to the INNER row and return wrong results — refuse loudly instead.
5087
+ throw new Error(`Correlated standalone subquery over table "${currentTableName}" references the same table from the outer query. `
5088
+ + `Both sides would share the alias "${currentTableName}" and the correlation would silently compare the inner row with itself. `
5089
+ + `Correlate through a different table or key column, or materialize the outer value before building the subquery.`);
5012
5090
  }
5013
5091
  }
5014
5092
  }