linkgress-orm 0.4.76 → 0.4.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/dist/config/linkgress-config.d.ts +23 -0
- package/dist/config/linkgress-config.d.ts.map +1 -1
- package/dist/config/linkgress-config.js +29 -0
- package/dist/config/linkgress-config.js.map +1 -1
- package/dist/entity/db-context.d.ts +9 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +76 -65
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +14 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/conditions.d.ts +36 -0
- package/dist/query/conditions.d.ts.map +1 -1
- package/dist/query/conditions.js +78 -10
- package/dist/query/conditions.js.map +1 -1
- package/dist/query/lateral-sql-cache.d.ts +53 -0
- package/dist/query/lateral-sql-cache.d.ts.map +1 -0
- package/dist/query/lateral-sql-cache.js +63 -0
- package/dist/query/lateral-sql-cache.js.map +1 -0
- package/dist/query/query-builder.d.ts +18 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +203 -59
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +28 -3
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +161 -180
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/package.json +80 -80
|
@@ -4,6 +4,7 @@ exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQ
|
|
|
4
4
|
exports.getColumnNameMapForSchema = getColumnNameMapForSchema;
|
|
5
5
|
exports.getRelationEntriesForSchema = getRelationEntriesForSchema;
|
|
6
6
|
exports.getSchemaColumnMeta = getSchemaColumnMeta;
|
|
7
|
+
exports.getDbToPropertyMapForSchema = getDbToPropertyMapForSchema;
|
|
7
8
|
exports.getTargetSchemaForRelation = getTargetSchemaForRelation;
|
|
8
9
|
exports.createNestedFieldRefProxy = createNestedFieldRefProxy;
|
|
9
10
|
const conditions_1 = require("./conditions");
|
|
@@ -81,6 +82,23 @@ function getSchemaColumnMeta(schema) {
|
|
|
81
82
|
}
|
|
82
83
|
return meta;
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Reverse of the column-name map — db column name → property name — cached per schema.
|
|
87
|
+
* An ordered collection needs it on every build to render the alias form of its ORDER BY;
|
|
88
|
+
* rebuilding it walked every column of the schema per build.
|
|
89
|
+
*/
|
|
90
|
+
const schemaDbToPropertyMapCache = new WeakMap();
|
|
91
|
+
function getDbToPropertyMapForSchema(schema) {
|
|
92
|
+
let map = schemaDbToPropertyMapCache.get(schema);
|
|
93
|
+
if (map == null) {
|
|
94
|
+
map = new Map();
|
|
95
|
+
for (const [propName, meta] of getSchemaColumnMeta(schema)) {
|
|
96
|
+
map.set(meta.name, propName);
|
|
97
|
+
}
|
|
98
|
+
schemaDbToPropertyMapCache.set(schema, map);
|
|
99
|
+
}
|
|
100
|
+
return map;
|
|
101
|
+
}
|
|
84
102
|
/**
|
|
85
103
|
* Mock-row descriptor cache for {@link ReferenceQueryBuilder.createMockTargetRow}.
|
|
86
104
|
*
|
|
@@ -103,6 +121,13 @@ const MOCK_ROW_CHAIN_ID = Symbol('linkgressMockChainId');
|
|
|
103
121
|
const navigationPathSignature = (path) => path
|
|
104
122
|
.map(step => `${step.alias}:${step.targetTable}:${(step.foreignKeys ?? []).join('+')}:${(step.matches ?? []).join('+')}:${step.isMandatory ? 1 : 0}:${step.sourceAlias ?? ''}`)
|
|
105
123
|
.join('>');
|
|
124
|
+
/** A reference mock row: `Object.create(prototype)` plus its two own state slots. */
|
|
125
|
+
const mintReferenceMockRow = (prototype) => {
|
|
126
|
+
const mock = Object.create(prototype);
|
|
127
|
+
mock[MOCK_ROW_FIELD_REFS] = {};
|
|
128
|
+
mock[MOCK_ROW_NAV_CACHE] = {};
|
|
129
|
+
return mock;
|
|
130
|
+
};
|
|
106
131
|
/**
|
|
107
132
|
* Whether `value` is a mock row minted by the query builders (`createMockTargetRow`,
|
|
108
133
|
* `SelectQueryBuilder._createMockRow`, `CollectionQueryBuilder.createMockItem`). Every
|
|
@@ -470,13 +495,25 @@ class QueryBuilder {
|
|
|
470
495
|
// Single reference navigation (many-to-one, one-to-one)
|
|
471
496
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
|
|
472
497
|
// with circular relations like User->Posts->User)
|
|
498
|
+
const holder = {};
|
|
473
499
|
descriptors[relName] = {
|
|
474
|
-
get
|
|
500
|
+
get() {
|
|
501
|
+
// One mock target row per row and relation (a selector reading `p.user.*` several
|
|
502
|
+
// times used to mint a builder and a row per access)
|
|
503
|
+
const slots = this;
|
|
504
|
+
const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
|
|
505
|
+
const cachedRow = navCache[relName];
|
|
506
|
+
if (cachedRow !== undefined) {
|
|
507
|
+
return cachedRow;
|
|
508
|
+
}
|
|
509
|
+
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
510
|
+
return (navCache[relName] = mintReferenceMockRow(holder.prototype));
|
|
511
|
+
}
|
|
475
512
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, schemaRegistry, // Pass schema registry for nested navigation resolution
|
|
476
513
|
[], // Empty navigation path for first level navigation
|
|
477
514
|
sourceTableName // Pass source table name for lateral join correlation
|
|
478
515
|
);
|
|
479
|
-
return refBuilder.createMockTargetRow();
|
|
516
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
480
517
|
},
|
|
481
518
|
enumerable: false,
|
|
482
519
|
configurable: true,
|
|
@@ -632,12 +669,20 @@ class QueryBuilder {
|
|
|
632
669
|
// Single reference navigation
|
|
633
670
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
634
671
|
Object.defineProperty(mock, relName, {
|
|
635
|
-
get
|
|
672
|
+
get() {
|
|
673
|
+
// One mock target row per row and relation (a selector reading `p.user.*` several
|
|
674
|
+
// times used to mint a builder and a row per access)
|
|
675
|
+
const slots = this;
|
|
676
|
+
const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
|
|
677
|
+
const cachedRow = navCache[relName];
|
|
678
|
+
if (cachedRow !== undefined) {
|
|
679
|
+
return cachedRow;
|
|
680
|
+
}
|
|
636
681
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, this.schemaRegistry, // Pass schema registry for nested navigation resolution
|
|
637
682
|
[], // Empty navigation path for first level navigation
|
|
638
683
|
schema.name // Pass source table name for lateral join correlation
|
|
639
684
|
);
|
|
640
|
-
return refBuilder.createMockTargetRow();
|
|
685
|
+
return (navCache[relName] = refBuilder.createMockTargetRow());
|
|
641
686
|
},
|
|
642
687
|
enumerable: false,
|
|
643
688
|
configurable: true,
|
|
@@ -1188,12 +1233,20 @@ class SelectQueryBuilder {
|
|
|
1188
1233
|
// Single reference navigation
|
|
1189
1234
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
1190
1235
|
Object.defineProperty(mock, relName, {
|
|
1191
|
-
get
|
|
1236
|
+
get() {
|
|
1237
|
+
// One mock target row per row and relation (a selector reading `p.user.*` several
|
|
1238
|
+
// times used to mint a builder and a row per access)
|
|
1239
|
+
const slots = this;
|
|
1240
|
+
const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
|
|
1241
|
+
const cachedRow = navCache[relName];
|
|
1242
|
+
if (cachedRow !== undefined) {
|
|
1243
|
+
return cachedRow;
|
|
1244
|
+
}
|
|
1192
1245
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, this.schemaRegistry, // Pass schema registry for nested navigation resolution
|
|
1193
1246
|
[], // Empty navigation path for first level navigation
|
|
1194
1247
|
schema.name // Pass source table name for lateral join correlation
|
|
1195
1248
|
);
|
|
1196
|
-
return refBuilder.createMockTargetRow();
|
|
1249
|
+
return (navCache[relName] = refBuilder.createMockTargetRow());
|
|
1197
1250
|
},
|
|
1198
1251
|
enumerable: false,
|
|
1199
1252
|
configurable: true,
|
|
@@ -3646,15 +3699,27 @@ ${joinClauses.join('\n')}`;
|
|
|
3646
3699
|
else {
|
|
3647
3700
|
// For single reference (many-to-one), create a ReferenceQueryBuilder
|
|
3648
3701
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
3702
|
+
const holder = {};
|
|
3649
3703
|
descriptors[relName] = {
|
|
3650
|
-
get
|
|
3704
|
+
get() {
|
|
3705
|
+
// One mock target row per row and relation (a selector reading `p.user.*` several
|
|
3706
|
+
// times used to mint a builder and a row per access)
|
|
3707
|
+
const slots = this;
|
|
3708
|
+
const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
|
|
3709
|
+
const cachedRow = navCache[relName];
|
|
3710
|
+
if (cachedRow !== undefined) {
|
|
3711
|
+
return cachedRow;
|
|
3712
|
+
}
|
|
3713
|
+
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
3714
|
+
return (navCache[relName] = mintReferenceMockRow(holder.prototype));
|
|
3715
|
+
}
|
|
3651
3716
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
|
|
3652
3717
|
schemaRegistry, // Pass schema registry for nested resolution
|
|
3653
3718
|
[], // Empty navigation path for first level navigation
|
|
3654
3719
|
sourceTableName // Pass source table name for lateral join correlation
|
|
3655
3720
|
);
|
|
3656
3721
|
// Return a mock object that exposes the target table's columns
|
|
3657
|
-
return refBuilder.createMockTargetRow();
|
|
3722
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
3658
3723
|
},
|
|
3659
3724
|
enumerable: false,
|
|
3660
3725
|
configurable: true,
|
|
@@ -4204,7 +4269,11 @@ ${joinClauses.join('\n')}`;
|
|
|
4204
4269
|
}
|
|
4205
4270
|
else {
|
|
4206
4271
|
// Process selection object properties
|
|
4207
|
-
for (const
|
|
4272
|
+
for (const key in selection) {
|
|
4273
|
+
if (!Object.prototype.hasOwnProperty.call(selection, key)) {
|
|
4274
|
+
continue;
|
|
4275
|
+
}
|
|
4276
|
+
const value = selection[key];
|
|
4208
4277
|
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
4209
4278
|
// Handle collection - delegate to strategy pattern via buildCTE
|
|
4210
4279
|
// The strategy handles CTE/LATERAL specifics and returns necessary info
|
|
@@ -4678,7 +4747,11 @@ ${joinClauses.join('\n')}`;
|
|
|
4678
4747
|
// - collections hit an explicit `continue` and were never emitted.
|
|
4679
4748
|
// Both regressions are now covered by union-nested-select.test.ts and
|
|
4680
4749
|
// union-collection-nav.test.ts.
|
|
4681
|
-
for (const
|
|
4750
|
+
for (const key in selection) {
|
|
4751
|
+
if (!Object.prototype.hasOwnProperty.call(selection, key)) {
|
|
4752
|
+
continue;
|
|
4753
|
+
}
|
|
4754
|
+
const value = selection[key];
|
|
4682
4755
|
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
4683
4756
|
// Collection projection inside a UNION leg — delegate to the
|
|
4684
4757
|
// collection strategy (LATERAL by default) and emit per-row
|
|
@@ -5828,7 +5901,7 @@ class ReferenceQueryBuilder {
|
|
|
5828
5901
|
* Create a mock object that exposes the target table's columns
|
|
5829
5902
|
* This allows accessing related fields like: p.user.username
|
|
5830
5903
|
*/
|
|
5831
|
-
createMockTargetRow() {
|
|
5904
|
+
createMockTargetRow(holder) {
|
|
5832
5905
|
if (this.targetTableSchema) {
|
|
5833
5906
|
// Prototype-level cache — see MockRowCache's doc. Everything the getters close over
|
|
5834
5907
|
// is fully determined by (target schema object identity, relationName, sourceAlias,
|
|
@@ -5844,11 +5917,18 @@ class ReferenceQueryBuilder {
|
|
|
5844
5917
|
// Consumers must not probe these rows with OWN-property APIs (`Object.keys`,
|
|
5845
5918
|
// `getOwnPropertyNames`, `getOwnPropertyDescriptor` on the row itself) — the getters
|
|
5846
5919
|
// are inherited. Use `isReferenceMockRow` / `findFirstGetterKey` instead.
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5920
|
+
//
|
|
5921
|
+
// The getter that minted this builder may hand in its MockPrototypeHolder: a filled holder
|
|
5922
|
+
// short-circuits the key build and the lookup, and a miss fills it (switch on only).
|
|
5923
|
+
const enabled = mock_row_cache_1.MockRowCache.isEnabled();
|
|
5924
|
+
let prototype = enabled ? holder?.prototype : undefined;
|
|
5925
|
+
if (prototype === undefined) {
|
|
5926
|
+
prototype = mock_row_cache_1.MockRowCache.getOrBuild(enabled ? `${this.targetTable}|${this.relationName}|${this.sourceAlias ?? ''}|${navigationPathSignature(this.navigationPath)}` : '', () => Object.defineProperties({}, this.buildMockRowDescriptors()));
|
|
5927
|
+
if (enabled && holder !== undefined) {
|
|
5928
|
+
holder.prototype = prototype;
|
|
5929
|
+
}
|
|
5930
|
+
}
|
|
5931
|
+
return mintReferenceMockRow(prototype);
|
|
5852
5932
|
}
|
|
5853
5933
|
else {
|
|
5854
5934
|
// Fallback: use the shared nested proxy that supports deep property access
|
|
@@ -5967,18 +6047,24 @@ class ReferenceQueryBuilder {
|
|
|
5967
6047
|
// Reference navigation
|
|
5968
6048
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
|
|
5969
6049
|
// with circular relations like User->Posts->User)
|
|
6050
|
+
const holder = {};
|
|
5970
6051
|
descriptors[relName] = {
|
|
5971
6052
|
get() {
|
|
5972
6053
|
const slots = this;
|
|
5973
6054
|
const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
|
|
5974
6055
|
let cached = navCache[relName];
|
|
5975
6056
|
if (cached === undefined) {
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
6057
|
+
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
6058
|
+
cached = navCache[relName] = mintReferenceMockRow(holder.prototype);
|
|
6059
|
+
}
|
|
6060
|
+
else {
|
|
6061
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
|
|
6062
|
+
schemaRegistry, // Pass schema registry for nested resolution
|
|
6063
|
+
extendedNavPath, // Pass navigation path for nested collections
|
|
6064
|
+
parentSourceAlias ? tableAlias : '' // Only set source if tracking path
|
|
6065
|
+
);
|
|
6066
|
+
cached = navCache[relName] = refBuilder.createMockTargetRow(holder);
|
|
6067
|
+
}
|
|
5982
6068
|
}
|
|
5983
6069
|
return cached;
|
|
5984
6070
|
},
|
|
@@ -6158,16 +6244,28 @@ class CollectionQueryBuilder {
|
|
|
6158
6244
|
else {
|
|
6159
6245
|
// Reference navigation
|
|
6160
6246
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
6247
|
+
const holder = {};
|
|
6161
6248
|
descriptors[relName] = {
|
|
6162
|
-
get
|
|
6249
|
+
get() {
|
|
6163
6250
|
// Don't call build() - it returns schema without relations
|
|
6164
6251
|
// Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
|
|
6252
|
+
// One mock target row per ITEM row and relation: a selector that reads
|
|
6253
|
+
// `it.product.*` fifteen times used to mint fifteen builders and rows.
|
|
6254
|
+
const slots = this;
|
|
6255
|
+
const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
|
|
6256
|
+
const cachedRow = navCache[relName];
|
|
6257
|
+
if (cachedRow !== undefined) {
|
|
6258
|
+
return cachedRow;
|
|
6259
|
+
}
|
|
6260
|
+
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
6261
|
+
return (navCache[relName] = mintReferenceMockRow(holder.prototype));
|
|
6262
|
+
}
|
|
6165
6263
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
|
|
6166
6264
|
schemaRegistry, // Pass schema registry for nested resolution
|
|
6167
6265
|
[], // Empty navigation path - this is the first reference in the chain
|
|
6168
6266
|
targetTable // Source alias is this collection's target table
|
|
6169
6267
|
);
|
|
6170
|
-
return refBuilder.createMockTargetRow();
|
|
6268
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
6171
6269
|
},
|
|
6172
6270
|
enumerable: false,
|
|
6173
6271
|
configurable: true,
|
|
@@ -6529,6 +6627,12 @@ class CollectionQueryBuilder {
|
|
|
6529
6627
|
}
|
|
6530
6628
|
// Collect all table aliases referenced in the selection
|
|
6531
6629
|
const allTableAliases = new Set();
|
|
6630
|
+
// Aliases already joined — kept in step with `joins` so membership is O(1) instead of a
|
|
6631
|
+
// `joins.some(...)` scan per field ref (a wide projection asks this for every field)
|
|
6632
|
+
const joinedAliases = new Set();
|
|
6633
|
+
for (const join of joins) {
|
|
6634
|
+
joinedAliases.add(join.alias);
|
|
6635
|
+
}
|
|
6532
6636
|
// Helper to collect from a single selection
|
|
6533
6637
|
const collectFromSelection = (sel) => {
|
|
6534
6638
|
if (!sel || typeof sel !== 'object') {
|
|
@@ -6536,20 +6640,26 @@ class CollectionQueryBuilder {
|
|
|
6536
6640
|
}
|
|
6537
6641
|
// Handle single FieldRef
|
|
6538
6642
|
if ('__tableAlias' in sel && '__dbColumnName' in sel) {
|
|
6539
|
-
this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
6643
|
+
this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
|
|
6540
6644
|
return;
|
|
6541
6645
|
}
|
|
6542
6646
|
// Handle object with multiple fields
|
|
6543
|
-
|
|
6647
|
+
// Own enumerable keys only (the set Object.entries walks), without allocating the pairs:
|
|
6648
|
+
// a wide collection projection runs this for every field on every build.
|
|
6649
|
+
for (const key in sel) {
|
|
6650
|
+
if (!Object.prototype.hasOwnProperty.call(sel, key)) {
|
|
6651
|
+
continue;
|
|
6652
|
+
}
|
|
6653
|
+
const value = sel[key];
|
|
6544
6654
|
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
6545
6655
|
// This is a FieldRef with a table alias
|
|
6546
|
-
this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
6656
|
+
this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
|
|
6547
6657
|
}
|
|
6548
6658
|
else if (value instanceof conditions_1.SqlFragment) {
|
|
6549
6659
|
// SqlFragment may contain navigation property references
|
|
6550
6660
|
const fieldRefs = value.getFieldRefs();
|
|
6551
6661
|
for (const fieldRef of fieldRefs) {
|
|
6552
|
-
this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
6662
|
+
this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
|
|
6553
6663
|
}
|
|
6554
6664
|
}
|
|
6555
6665
|
else if (value instanceof CollectionQueryBuilder) {
|
|
@@ -6560,8 +6670,9 @@ class CollectionQueryBuilder {
|
|
|
6560
6670
|
// in its own FROM for the correlation to resolve.
|
|
6561
6671
|
const nestedPath = value.getNavigationPath();
|
|
6562
6672
|
for (const step of nestedPath) {
|
|
6563
|
-
if (!
|
|
6673
|
+
if (!joinedAliases.has(step.alias)) {
|
|
6564
6674
|
joins.push(step);
|
|
6675
|
+
joinedAliases.add(step.alias);
|
|
6565
6676
|
}
|
|
6566
6677
|
}
|
|
6567
6678
|
}
|
|
@@ -6575,14 +6686,14 @@ class CollectionQueryBuilder {
|
|
|
6575
6686
|
collectFromSelection(selection);
|
|
6576
6687
|
// Second pass: resolve all navigation joins by finding the correct path through schemas
|
|
6577
6688
|
if (allTableAliases.size > 0) {
|
|
6578
|
-
this.resolveNavigationJoins(allTableAliases, joins, currentSchema);
|
|
6689
|
+
this.resolveNavigationJoins(allTableAliases, joins, currentSchema, joinedAliases);
|
|
6579
6690
|
}
|
|
6580
6691
|
}
|
|
6581
6692
|
/**
|
|
6582
6693
|
* Add a navigation JOIN for a FieldRef if it references a related table
|
|
6583
6694
|
* Handles multi-level navigation by recursively resolving the join chain
|
|
6584
6695
|
*/
|
|
6585
|
-
addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases) {
|
|
6696
|
+
addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases, joinedAliases) {
|
|
6586
6697
|
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
|
|
6587
6698
|
return;
|
|
6588
6699
|
}
|
|
@@ -6611,23 +6722,24 @@ class CollectionQueryBuilder {
|
|
|
6611
6722
|
// Collect this table alias for later resolution
|
|
6612
6723
|
allTableAliases.add(tableAlias);
|
|
6613
6724
|
// Check if we already have this join
|
|
6614
|
-
if (joins.some(j => j.alias === tableAlias)) {
|
|
6725
|
+
if (joinedAliases !== undefined ? joinedAliases.has(tableAlias) : joins.some(j => j.alias === tableAlias)) {
|
|
6615
6726
|
return;
|
|
6616
6727
|
}
|
|
6617
6728
|
// Find the relation in the current schema
|
|
6618
6729
|
const relation = sourceSchema.relations?.[tableAlias];
|
|
6619
6730
|
if (relation && relation.type === 'one') {
|
|
6620
|
-
this.addNavigationJoin(tableAlias, relation, joins, sourceAlias);
|
|
6731
|
+
this.addNavigationJoin(tableAlias, relation, joins, sourceAlias, joinedAliases);
|
|
6621
6732
|
}
|
|
6622
6733
|
}
|
|
6623
6734
|
/**
|
|
6624
6735
|
* Add a navigation join and return the target schema
|
|
6625
6736
|
*/
|
|
6626
|
-
addNavigationJoin(alias, relation, joins, sourceAlias) {
|
|
6737
|
+
addNavigationJoin(alias, relation, joins, sourceAlias, joinedAliases) {
|
|
6627
6738
|
// Check if already added
|
|
6628
|
-
if (joins.some(j => j.alias === alias)) {
|
|
6739
|
+
if (joinedAliases !== undefined ? joinedAliases.has(alias) : joins.some(j => j.alias === alias)) {
|
|
6629
6740
|
return undefined;
|
|
6630
6741
|
}
|
|
6742
|
+
joinedAliases?.add(alias);
|
|
6631
6743
|
// Get the target table schema
|
|
6632
6744
|
let targetSchema;
|
|
6633
6745
|
let targetSchemaName;
|
|
@@ -6657,8 +6769,28 @@ class CollectionQueryBuilder {
|
|
|
6657
6769
|
* Resolve all navigation joins by finding the correct path through the schema graph
|
|
6658
6770
|
* This handles multi-level navigation like task.level.createdBy
|
|
6659
6771
|
*/
|
|
6660
|
-
resolveNavigationJoins(allTableAliases, joins, startSchema) {
|
|
6772
|
+
resolveNavigationJoins(allTableAliases, joins, startSchema, joinedAliases) {
|
|
6773
|
+
// Aliases already joined, kept in step with `joins` (O(1) membership; the caller may hand
|
|
6774
|
+
// in the set it maintained while collecting the field refs)
|
|
6775
|
+
if (joinedAliases === undefined) {
|
|
6776
|
+
joinedAliases = new Set();
|
|
6777
|
+
for (const join of joins) {
|
|
6778
|
+
joinedAliases.add(join.alias);
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6661
6781
|
// Keep resolving until we've resolved all aliases or can't make progress
|
|
6782
|
+
// Fast path: every referenced alias is already joined (direct relations of the target table
|
|
6783
|
+
// were added while the field refs were collected) — the loop below would only mark them.
|
|
6784
|
+
let allJoined = true;
|
|
6785
|
+
for (const alias of allTableAliases) {
|
|
6786
|
+
if (!joinedAliases.has(alias)) {
|
|
6787
|
+
allJoined = false;
|
|
6788
|
+
break;
|
|
6789
|
+
}
|
|
6790
|
+
}
|
|
6791
|
+
if (allJoined) {
|
|
6792
|
+
return;
|
|
6793
|
+
}
|
|
6662
6794
|
let resolved = new Set();
|
|
6663
6795
|
let lastResolvedCount = -1;
|
|
6664
6796
|
let maxIterations = 100; // Prevent infinite loops
|
|
@@ -6696,7 +6828,7 @@ class CollectionQueryBuilder {
|
|
|
6696
6828
|
if (resolved.has(alias)) {
|
|
6697
6829
|
continue;
|
|
6698
6830
|
}
|
|
6699
|
-
if (
|
|
6831
|
+
if (joinedAliases.has(alias)) {
|
|
6700
6832
|
resolved.add(alias);
|
|
6701
6833
|
continue;
|
|
6702
6834
|
}
|
|
@@ -6706,7 +6838,7 @@ class CollectionQueryBuilder {
|
|
|
6706
6838
|
if (!relation || relation.type !== 'one') {
|
|
6707
6839
|
continue;
|
|
6708
6840
|
}
|
|
6709
|
-
const targetSchema = this.addNavigationJoin(alias, relation, joins, schemaAlias);
|
|
6841
|
+
const targetSchema = this.addNavigationJoin(alias, relation, joins, schemaAlias, joinedAliases);
|
|
6710
6842
|
if (targetSchema) {
|
|
6711
6843
|
// Keep the anchor map current so the alias we just joined can serve as the
|
|
6712
6844
|
// source for further direct lookups in the next fixpoint round
|
|
@@ -6731,8 +6863,8 @@ class CollectionQueryBuilder {
|
|
|
6731
6863
|
}
|
|
6732
6864
|
// Add all intermediate joins
|
|
6733
6865
|
for (const step of path) {
|
|
6734
|
-
if (!
|
|
6735
|
-
const stepSchema = this.addNavigationJoin(step.alias, step.relation, joins, step.sourceAlias);
|
|
6866
|
+
if (!joinedAliases.has(step.alias)) {
|
|
6867
|
+
const stepSchema = this.addNavigationJoin(step.alias, step.relation, joins, step.sourceAlias, joinedAliases);
|
|
6736
6868
|
if (stepSchema) {
|
|
6737
6869
|
joinedSchemas.set(step.alias, stepSchema);
|
|
6738
6870
|
}
|
|
@@ -6753,9 +6885,12 @@ class CollectionQueryBuilder {
|
|
|
6753
6885
|
// Memoised per registry — the BFS below depends only on the target alias and the ORDERED
|
|
6754
6886
|
// joined alias→table pairs (order decides which of several equal-length paths wins), so
|
|
6755
6887
|
// the same query shape resolves to the same path on every build. See NavigationPathCache.
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6888
|
+
// Built by concatenation over the map (no spread / map / join allocations — this runs on every
|
|
6889
|
+
// build of every projection that reaches an alias through a second hop)
|
|
6890
|
+
let signature = targetAlias + '|';
|
|
6891
|
+
for (const [alias, schema] of joinedSchemas) {
|
|
6892
|
+
signature += alias + ':' + (schema.schema ?? '') + '.' + schema.name + ',';
|
|
6893
|
+
}
|
|
6759
6894
|
return navigation_path_cache_1.NavigationPathCache.getOrBuild(this.schemaRegistry, signature, () => this.computeNavigationPath(targetAlias, joinedSchemas));
|
|
6760
6895
|
}
|
|
6761
6896
|
/** The uncached BFS behind {@link findNavigationPath}. */
|
|
@@ -6842,6 +6977,7 @@ class CollectionQueryBuilder {
|
|
|
6842
6977
|
val.constructor === Object;
|
|
6843
6978
|
};
|
|
6844
6979
|
// Helper function to recursively process fields and build SelectedField structures
|
|
6980
|
+
const collectionMarkerAlias = `__collection_${this.targetTable}__`;
|
|
6845
6981
|
const processField = (alias, field) => {
|
|
6846
6982
|
if (field instanceof conditions_1.SqlFragment) {
|
|
6847
6983
|
// SQL Fragment - build the SQL expression
|
|
@@ -6909,6 +7045,7 @@ class CollectionQueryBuilder {
|
|
|
6909
7045
|
nestedCteJoin: {
|
|
6910
7046
|
cteName: nestedResult.tableName,
|
|
6911
7047
|
joinClause: nestedJoinClause,
|
|
7048
|
+
memoId: nestedResult.memoId,
|
|
6912
7049
|
},
|
|
6913
7050
|
// Store nested collection info for recursive mapper transformation
|
|
6914
7051
|
nestedCollectionInfo: {
|
|
@@ -6951,11 +7088,10 @@ class CollectionQueryBuilder {
|
|
|
6951
7088
|
const sourceTable = field.__sourceTable; // Actual table name for schema lookup
|
|
6952
7089
|
// If tableAlias differs from the target table (or its collection marker), it's a navigation property reference
|
|
6953
7090
|
// The collection marker is `__collection_tableName__` and should be treated as the target table
|
|
6954
|
-
|
|
6955
|
-
if (tableAlias && tableAlias !== this.targetTable && tableAlias !== collectionMarker) {
|
|
7091
|
+
if (tableAlias && tableAlias !== this.targetTable && tableAlias !== collectionMarkerAlias) {
|
|
6956
7092
|
return { alias, expression: `"${tableAlias}"."${dbColumnName}"`, propertyName: fieldName, sourceTable };
|
|
6957
7093
|
}
|
|
6958
|
-
return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName };
|
|
7094
|
+
return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName, isColumn: true };
|
|
6959
7095
|
}
|
|
6960
7096
|
else if (typeof field === 'string') {
|
|
6961
7097
|
// Simple string reference (for backward compatibility)
|
|
@@ -6964,8 +7100,10 @@ class CollectionQueryBuilder {
|
|
|
6964
7100
|
else if (isPlainObject(field)) {
|
|
6965
7101
|
// Nested object - recursively process its fields
|
|
6966
7102
|
const nestedFields = [];
|
|
6967
|
-
for (const
|
|
6968
|
-
|
|
7103
|
+
for (const nestedAlias in field) {
|
|
7104
|
+
if (Object.prototype.hasOwnProperty.call(field, nestedAlias)) {
|
|
7105
|
+
nestedFields.push(processField(nestedAlias, field[nestedAlias]));
|
|
7106
|
+
}
|
|
6969
7107
|
}
|
|
6970
7108
|
return { alias, nested: nestedFields };
|
|
6971
7109
|
}
|
|
@@ -6995,6 +7133,7 @@ class CollectionQueryBuilder {
|
|
|
6995
7133
|
alias: dbColumnName,
|
|
6996
7134
|
expression: `"${dbColumnName}"`,
|
|
6997
7135
|
propertyName: fieldName,
|
|
7136
|
+
isColumn: true,
|
|
6998
7137
|
});
|
|
6999
7138
|
}
|
|
7000
7139
|
else if (selectedFields instanceof CollectionQueryBuilder || selectedFields instanceof conditions_1.SqlFragment) {
|
|
@@ -7003,8 +7142,11 @@ class CollectionQueryBuilder {
|
|
|
7003
7142
|
}
|
|
7004
7143
|
else {
|
|
7005
7144
|
// Object selection - extract each field (with support for nested objects)
|
|
7006
|
-
|
|
7007
|
-
|
|
7145
|
+
// Own enumerable keys, without allocating the entry pairs (this runs per field per build)
|
|
7146
|
+
for (const alias in selectedFields) {
|
|
7147
|
+
if (Object.prototype.hasOwnProperty.call(selectedFields, alias)) {
|
|
7148
|
+
selectedFieldConfigs.push(processField(alias, selectedFields[alias]));
|
|
7149
|
+
}
|
|
7008
7150
|
}
|
|
7009
7151
|
}
|
|
7010
7152
|
}
|
|
@@ -7018,6 +7160,7 @@ class CollectionQueryBuilder {
|
|
|
7018
7160
|
alias: colName,
|
|
7019
7161
|
expression: `"${dbColumnName}"`,
|
|
7020
7162
|
propertyName: colName, // Same as alias when selecting all fields
|
|
7163
|
+
isColumn: true,
|
|
7021
7164
|
});
|
|
7022
7165
|
}
|
|
7023
7166
|
}
|
|
@@ -7055,13 +7198,7 @@ class CollectionQueryBuilder {
|
|
|
7055
7198
|
let orderByClauseAlias;
|
|
7056
7199
|
if (this.orderByFields.length > 0) {
|
|
7057
7200
|
// Build reverse lookup: db column name -> property name
|
|
7058
|
-
|
|
7059
|
-
if (this.targetTableSchema) {
|
|
7060
|
-
dbToPropertyMap = new Map();
|
|
7061
|
-
for (const [propName, meta] of getSchemaColumnMeta(this.targetTableSchema)) {
|
|
7062
|
-
dbToPropertyMap.set(meta.name, propName);
|
|
7063
|
-
}
|
|
7064
|
-
}
|
|
7201
|
+
const dbToPropertyMap = this.targetTableSchema ? getDbToPropertyMapForSchema(this.targetTableSchema) : null;
|
|
7065
7202
|
const orderPartsDb = this.orderByFields.map(({ field, direction }) => {
|
|
7066
7203
|
// field is already the database column name
|
|
7067
7204
|
return `"${field}" ${direction}`;
|
|
@@ -7149,8 +7286,14 @@ class CollectionQueryBuilder {
|
|
|
7149
7286
|
// which must be included in the lateral subquery for correlation
|
|
7150
7287
|
// Include selectMany joins in both all and selector navigation joins
|
|
7151
7288
|
// selectMany joins are structural (from flattening) and needed by both CTE and LATERAL
|
|
7152
|
-
|
|
7153
|
-
|
|
7289
|
+
// (the common case — no navigation path, no selectMany — reuses the detected array instead of
|
|
7290
|
+
// spreading it twice; strategies only read these lists)
|
|
7291
|
+
const allNavigationJoins = this.navigationPath.length === 0 && this.selectManyJoins.length === 0
|
|
7292
|
+
? navigationJoins
|
|
7293
|
+
: [...this.navigationPath, ...this.selectManyJoins, ...navigationJoins];
|
|
7294
|
+
const allSelectorJoins = this.selectManyJoins.length === 0
|
|
7295
|
+
? navigationJoins
|
|
7296
|
+
: [...this.selectManyJoins, ...navigationJoins];
|
|
7154
7297
|
// Step 6: Build CollectionAggregationConfig object
|
|
7155
7298
|
const config = {
|
|
7156
7299
|
relationName: this.relationName,
|
|
@@ -7215,6 +7358,7 @@ class CollectionQueryBuilder {
|
|
|
7215
7358
|
joinClause: result.joinClause,
|
|
7216
7359
|
selectExpression: result.selectExpression,
|
|
7217
7360
|
tableName: result.tableName,
|
|
7361
|
+
memoId: result.memoId,
|
|
7218
7362
|
};
|
|
7219
7363
|
}
|
|
7220
7364
|
}
|