linkgress-orm 0.4.75 → 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/entity/entity-builder.d.ts.map +1 -1
- package/dist/entity/entity-builder.js +12 -1
- package/dist/entity/entity-builder.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 +209 -64
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-utils.d.ts +1 -0
- package/dist/query/query-utils.d.ts.map +1 -1
- package/dist/query/query-utils.js +20 -0
- package/dist/query/query-utils.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +105 -104
- package/dist/query/strategies/cte-collection-strategy.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 +227 -230
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +85 -84
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/package.json +80 -80
|
@@ -4,12 +4,14 @@ 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");
|
|
11
|
+
const query_utils_1 = require("./query-utils");
|
|
10
12
|
const prepared_query_1 = require("./prepared-query");
|
|
11
13
|
const db_context_1 = require("../entity/db-context");
|
|
12
|
-
const
|
|
14
|
+
const query_utils_2 = require("./query-utils");
|
|
13
15
|
const subquery_1 = require("./subquery");
|
|
14
16
|
const grouped_query_1 = require("./grouped-query");
|
|
15
17
|
const cte_builder_1 = require("./cte-builder");
|
|
@@ -80,6 +82,23 @@ function getSchemaColumnMeta(schema) {
|
|
|
80
82
|
}
|
|
81
83
|
return meta;
|
|
82
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
|
+
}
|
|
83
102
|
/**
|
|
84
103
|
* Mock-row descriptor cache for {@link ReferenceQueryBuilder.createMockTargetRow}.
|
|
85
104
|
*
|
|
@@ -102,6 +121,13 @@ const MOCK_ROW_CHAIN_ID = Symbol('linkgressMockChainId');
|
|
|
102
121
|
const navigationPathSignature = (path) => path
|
|
103
122
|
.map(step => `${step.alias}:${step.targetTable}:${(step.foreignKeys ?? []).join('+')}:${(step.matches ?? []).join('+')}:${step.isMandatory ? 1 : 0}:${step.sourceAlias ?? ''}`)
|
|
104
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
|
+
};
|
|
105
131
|
/**
|
|
106
132
|
* Whether `value` is a mock row minted by the query builders (`createMockTargetRow`,
|
|
107
133
|
* `SelectQueryBuilder._createMockRow`, `CollectionQueryBuilder.createMockItem`). Every
|
|
@@ -469,13 +495,25 @@ class QueryBuilder {
|
|
|
469
495
|
// Single reference navigation (many-to-one, one-to-one)
|
|
470
496
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
|
|
471
497
|
// with circular relations like User->Posts->User)
|
|
498
|
+
const holder = {};
|
|
472
499
|
descriptors[relName] = {
|
|
473
|
-
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
|
+
}
|
|
474
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
|
|
475
513
|
[], // Empty navigation path for first level navigation
|
|
476
514
|
sourceTableName // Pass source table name for lateral join correlation
|
|
477
515
|
);
|
|
478
|
-
return refBuilder.createMockTargetRow();
|
|
516
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
479
517
|
},
|
|
480
518
|
enumerable: false,
|
|
481
519
|
configurable: true,
|
|
@@ -631,12 +669,20 @@ class QueryBuilder {
|
|
|
631
669
|
// Single reference navigation
|
|
632
670
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
633
671
|
Object.defineProperty(mock, relName, {
|
|
634
|
-
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
|
+
}
|
|
635
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
|
|
636
682
|
[], // Empty navigation path for first level navigation
|
|
637
683
|
schema.name // Pass source table name for lateral join correlation
|
|
638
684
|
);
|
|
639
|
-
return refBuilder.createMockTargetRow();
|
|
685
|
+
return (navCache[relName] = refBuilder.createMockTargetRow());
|
|
640
686
|
},
|
|
641
687
|
enumerable: false,
|
|
642
688
|
configurable: true,
|
|
@@ -662,7 +708,7 @@ class QueryBuilder {
|
|
|
662
708
|
orderBy(selector) {
|
|
663
709
|
const mockRow = this._createMockRow();
|
|
664
710
|
const result = (0, exports.materializeMockSelection)(selector(mockRow));
|
|
665
|
-
(0,
|
|
711
|
+
(0, query_utils_2.parseOrderBy)(result, this.orderByFields);
|
|
666
712
|
return this;
|
|
667
713
|
}
|
|
668
714
|
}
|
|
@@ -941,7 +987,7 @@ class SelectQueryBuilder {
|
|
|
941
987
|
const result = selector(fieldRefProxy);
|
|
942
988
|
// Clear previous orderBy - last one takes precedence
|
|
943
989
|
this.orderByFields = [];
|
|
944
|
-
(0,
|
|
990
|
+
(0, query_utils_2.parseOrderBy)(result, this.orderByFields);
|
|
945
991
|
return this;
|
|
946
992
|
}
|
|
947
993
|
/**
|
|
@@ -1187,12 +1233,20 @@ class SelectQueryBuilder {
|
|
|
1187
1233
|
// Single reference navigation
|
|
1188
1234
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
1189
1235
|
Object.defineProperty(mock, relName, {
|
|
1190
|
-
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
|
+
}
|
|
1191
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
|
|
1192
1246
|
[], // Empty navigation path for first level navigation
|
|
1193
1247
|
schema.name // Pass source table name for lateral join correlation
|
|
1194
1248
|
);
|
|
1195
|
-
return refBuilder.createMockTargetRow();
|
|
1249
|
+
return (navCache[relName] = refBuilder.createMockTargetRow());
|
|
1196
1250
|
},
|
|
1197
1251
|
enumerable: false,
|
|
1198
1252
|
configurable: true,
|
|
@@ -3645,15 +3699,27 @@ ${joinClauses.join('\n')}`;
|
|
|
3645
3699
|
else {
|
|
3646
3700
|
// For single reference (many-to-one), create a ReferenceQueryBuilder
|
|
3647
3701
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
3702
|
+
const holder = {};
|
|
3648
3703
|
descriptors[relName] = {
|
|
3649
|
-
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
|
+
}
|
|
3650
3716
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
|
|
3651
3717
|
schemaRegistry, // Pass schema registry for nested resolution
|
|
3652
3718
|
[], // Empty navigation path for first level navigation
|
|
3653
3719
|
sourceTableName // Pass source table name for lateral join correlation
|
|
3654
3720
|
);
|
|
3655
3721
|
// Return a mock object that exposes the target table's columns
|
|
3656
|
-
return refBuilder.createMockTargetRow();
|
|
3722
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
3657
3723
|
},
|
|
3658
3724
|
enumerable: false,
|
|
3659
3725
|
configurable: true,
|
|
@@ -4203,7 +4269,11 @@ ${joinClauses.join('\n')}`;
|
|
|
4203
4269
|
}
|
|
4204
4270
|
else {
|
|
4205
4271
|
// Process selection object properties
|
|
4206
|
-
for (const
|
|
4272
|
+
for (const key in selection) {
|
|
4273
|
+
if (!Object.prototype.hasOwnProperty.call(selection, key)) {
|
|
4274
|
+
continue;
|
|
4275
|
+
}
|
|
4276
|
+
const value = selection[key];
|
|
4207
4277
|
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
4208
4278
|
// Handle collection - delegate to strategy pattern via buildCTE
|
|
4209
4279
|
// The strategy handles CTE/LATERAL specifics and returns necessary info
|
|
@@ -4677,7 +4747,11 @@ ${joinClauses.join('\n')}`;
|
|
|
4677
4747
|
// - collections hit an explicit `continue` and were never emitted.
|
|
4678
4748
|
// Both regressions are now covered by union-nested-select.test.ts and
|
|
4679
4749
|
// union-collection-nav.test.ts.
|
|
4680
|
-
for (const
|
|
4750
|
+
for (const key in selection) {
|
|
4751
|
+
if (!Object.prototype.hasOwnProperty.call(selection, key)) {
|
|
4752
|
+
continue;
|
|
4753
|
+
}
|
|
4754
|
+
const value = selection[key];
|
|
4681
4755
|
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
4682
4756
|
// Collection projection inside a UNION leg — delegate to the
|
|
4683
4757
|
// collection strategy (LATERAL by default) and emit per-row
|
|
@@ -5827,7 +5901,7 @@ class ReferenceQueryBuilder {
|
|
|
5827
5901
|
* Create a mock object that exposes the target table's columns
|
|
5828
5902
|
* This allows accessing related fields like: p.user.username
|
|
5829
5903
|
*/
|
|
5830
|
-
createMockTargetRow() {
|
|
5904
|
+
createMockTargetRow(holder) {
|
|
5831
5905
|
if (this.targetTableSchema) {
|
|
5832
5906
|
// Prototype-level cache — see MockRowCache's doc. Everything the getters close over
|
|
5833
5907
|
// is fully determined by (target schema object identity, relationName, sourceAlias,
|
|
@@ -5843,11 +5917,18 @@ class ReferenceQueryBuilder {
|
|
|
5843
5917
|
// Consumers must not probe these rows with OWN-property APIs (`Object.keys`,
|
|
5844
5918
|
// `getOwnPropertyNames`, `getOwnPropertyDescriptor` on the row itself) — the getters
|
|
5845
5919
|
// are inherited. Use `isReferenceMockRow` / `findFirstGetterKey` instead.
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
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);
|
|
5851
5932
|
}
|
|
5852
5933
|
else {
|
|
5853
5934
|
// Fallback: use the shared nested proxy that supports deep property access
|
|
@@ -5966,18 +6047,24 @@ class ReferenceQueryBuilder {
|
|
|
5966
6047
|
// Reference navigation
|
|
5967
6048
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
|
|
5968
6049
|
// with circular relations like User->Posts->User)
|
|
6050
|
+
const holder = {};
|
|
5969
6051
|
descriptors[relName] = {
|
|
5970
6052
|
get() {
|
|
5971
6053
|
const slots = this;
|
|
5972
6054
|
const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
|
|
5973
6055
|
let cached = navCache[relName];
|
|
5974
6056
|
if (cached === undefined) {
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
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
|
+
}
|
|
5981
6068
|
}
|
|
5982
6069
|
return cached;
|
|
5983
6070
|
},
|
|
@@ -6157,16 +6244,28 @@ class CollectionQueryBuilder {
|
|
|
6157
6244
|
else {
|
|
6158
6245
|
// Reference navigation
|
|
6159
6246
|
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
6247
|
+
const holder = {};
|
|
6160
6248
|
descriptors[relName] = {
|
|
6161
|
-
get
|
|
6249
|
+
get() {
|
|
6162
6250
|
// Don't call build() - it returns schema without relations
|
|
6163
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
|
+
}
|
|
6164
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
|
|
6165
6264
|
schemaRegistry, // Pass schema registry for nested resolution
|
|
6166
6265
|
[], // Empty navigation path - this is the first reference in the chain
|
|
6167
6266
|
targetTable // Source alias is this collection's target table
|
|
6168
6267
|
);
|
|
6169
|
-
return refBuilder.createMockTargetRow();
|
|
6268
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
6170
6269
|
},
|
|
6171
6270
|
enumerable: false,
|
|
6172
6271
|
configurable: true,
|
|
@@ -6193,7 +6292,7 @@ class CollectionQueryBuilder {
|
|
|
6193
6292
|
orderBy(selector) {
|
|
6194
6293
|
const mockItem = this.createMockItem();
|
|
6195
6294
|
const result = selector(mockItem);
|
|
6196
|
-
(0,
|
|
6295
|
+
(0, query_utils_2.parseOrderBy)(result, this.orderByFields);
|
|
6197
6296
|
return this;
|
|
6198
6297
|
}
|
|
6199
6298
|
/**
|
|
@@ -6508,7 +6607,7 @@ class CollectionQueryBuilder {
|
|
|
6508
6607
|
context.placeholders = placeholders;
|
|
6509
6608
|
}
|
|
6510
6609
|
// Rewrite collection marker aliases to actual table names
|
|
6511
|
-
const markerPattern =
|
|
6610
|
+
const markerPattern = (0, query_utils_1.collectionMarkerPattern)(targetTable, false);
|
|
6512
6611
|
const rewrittenCondSql = condSql.replace(markerPattern, `"${targetTable}"`);
|
|
6513
6612
|
whereSQL += ` AND ${rewrittenCondSql}`;
|
|
6514
6613
|
}
|
|
@@ -6528,6 +6627,12 @@ class CollectionQueryBuilder {
|
|
|
6528
6627
|
}
|
|
6529
6628
|
// Collect all table aliases referenced in the selection
|
|
6530
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
|
+
}
|
|
6531
6636
|
// Helper to collect from a single selection
|
|
6532
6637
|
const collectFromSelection = (sel) => {
|
|
6533
6638
|
if (!sel || typeof sel !== 'object') {
|
|
@@ -6535,20 +6640,26 @@ class CollectionQueryBuilder {
|
|
|
6535
6640
|
}
|
|
6536
6641
|
// Handle single FieldRef
|
|
6537
6642
|
if ('__tableAlias' in sel && '__dbColumnName' in sel) {
|
|
6538
|
-
this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
6643
|
+
this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
|
|
6539
6644
|
return;
|
|
6540
6645
|
}
|
|
6541
6646
|
// Handle object with multiple fields
|
|
6542
|
-
|
|
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];
|
|
6543
6654
|
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
6544
6655
|
// This is a FieldRef with a table alias
|
|
6545
|
-
this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
6656
|
+
this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
|
|
6546
6657
|
}
|
|
6547
6658
|
else if (value instanceof conditions_1.SqlFragment) {
|
|
6548
6659
|
// SqlFragment may contain navigation property references
|
|
6549
6660
|
const fieldRefs = value.getFieldRefs();
|
|
6550
6661
|
for (const fieldRef of fieldRefs) {
|
|
6551
|
-
this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases);
|
|
6662
|
+
this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
|
|
6552
6663
|
}
|
|
6553
6664
|
}
|
|
6554
6665
|
else if (value instanceof CollectionQueryBuilder) {
|
|
@@ -6559,8 +6670,9 @@ class CollectionQueryBuilder {
|
|
|
6559
6670
|
// in its own FROM for the correlation to resolve.
|
|
6560
6671
|
const nestedPath = value.getNavigationPath();
|
|
6561
6672
|
for (const step of nestedPath) {
|
|
6562
|
-
if (!
|
|
6673
|
+
if (!joinedAliases.has(step.alias)) {
|
|
6563
6674
|
joins.push(step);
|
|
6675
|
+
joinedAliases.add(step.alias);
|
|
6564
6676
|
}
|
|
6565
6677
|
}
|
|
6566
6678
|
}
|
|
@@ -6574,14 +6686,14 @@ class CollectionQueryBuilder {
|
|
|
6574
6686
|
collectFromSelection(selection);
|
|
6575
6687
|
// Second pass: resolve all navigation joins by finding the correct path through schemas
|
|
6576
6688
|
if (allTableAliases.size > 0) {
|
|
6577
|
-
this.resolveNavigationJoins(allTableAliases, joins, currentSchema);
|
|
6689
|
+
this.resolveNavigationJoins(allTableAliases, joins, currentSchema, joinedAliases);
|
|
6578
6690
|
}
|
|
6579
6691
|
}
|
|
6580
6692
|
/**
|
|
6581
6693
|
* Add a navigation JOIN for a FieldRef if it references a related table
|
|
6582
6694
|
* Handles multi-level navigation by recursively resolving the join chain
|
|
6583
6695
|
*/
|
|
6584
|
-
addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases) {
|
|
6696
|
+
addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases, joinedAliases) {
|
|
6585
6697
|
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
|
|
6586
6698
|
return;
|
|
6587
6699
|
}
|
|
@@ -6610,23 +6722,24 @@ class CollectionQueryBuilder {
|
|
|
6610
6722
|
// Collect this table alias for later resolution
|
|
6611
6723
|
allTableAliases.add(tableAlias);
|
|
6612
6724
|
// Check if we already have this join
|
|
6613
|
-
if (joins.some(j => j.alias === tableAlias)) {
|
|
6725
|
+
if (joinedAliases !== undefined ? joinedAliases.has(tableAlias) : joins.some(j => j.alias === tableAlias)) {
|
|
6614
6726
|
return;
|
|
6615
6727
|
}
|
|
6616
6728
|
// Find the relation in the current schema
|
|
6617
6729
|
const relation = sourceSchema.relations?.[tableAlias];
|
|
6618
6730
|
if (relation && relation.type === 'one') {
|
|
6619
|
-
this.addNavigationJoin(tableAlias, relation, joins, sourceAlias);
|
|
6731
|
+
this.addNavigationJoin(tableAlias, relation, joins, sourceAlias, joinedAliases);
|
|
6620
6732
|
}
|
|
6621
6733
|
}
|
|
6622
6734
|
/**
|
|
6623
6735
|
* Add a navigation join and return the target schema
|
|
6624
6736
|
*/
|
|
6625
|
-
addNavigationJoin(alias, relation, joins, sourceAlias) {
|
|
6737
|
+
addNavigationJoin(alias, relation, joins, sourceAlias, joinedAliases) {
|
|
6626
6738
|
// Check if already added
|
|
6627
|
-
if (joins.some(j => j.alias === alias)) {
|
|
6739
|
+
if (joinedAliases !== undefined ? joinedAliases.has(alias) : joins.some(j => j.alias === alias)) {
|
|
6628
6740
|
return undefined;
|
|
6629
6741
|
}
|
|
6742
|
+
joinedAliases?.add(alias);
|
|
6630
6743
|
// Get the target table schema
|
|
6631
6744
|
let targetSchema;
|
|
6632
6745
|
let targetSchemaName;
|
|
@@ -6656,8 +6769,28 @@ class CollectionQueryBuilder {
|
|
|
6656
6769
|
* Resolve all navigation joins by finding the correct path through the schema graph
|
|
6657
6770
|
* This handles multi-level navigation like task.level.createdBy
|
|
6658
6771
|
*/
|
|
6659
|
-
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
|
+
}
|
|
6660
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
|
+
}
|
|
6661
6794
|
let resolved = new Set();
|
|
6662
6795
|
let lastResolvedCount = -1;
|
|
6663
6796
|
let maxIterations = 100; // Prevent infinite loops
|
|
@@ -6695,7 +6828,7 @@ class CollectionQueryBuilder {
|
|
|
6695
6828
|
if (resolved.has(alias)) {
|
|
6696
6829
|
continue;
|
|
6697
6830
|
}
|
|
6698
|
-
if (
|
|
6831
|
+
if (joinedAliases.has(alias)) {
|
|
6699
6832
|
resolved.add(alias);
|
|
6700
6833
|
continue;
|
|
6701
6834
|
}
|
|
@@ -6705,7 +6838,7 @@ class CollectionQueryBuilder {
|
|
|
6705
6838
|
if (!relation || relation.type !== 'one') {
|
|
6706
6839
|
continue;
|
|
6707
6840
|
}
|
|
6708
|
-
const targetSchema = this.addNavigationJoin(alias, relation, joins, schemaAlias);
|
|
6841
|
+
const targetSchema = this.addNavigationJoin(alias, relation, joins, schemaAlias, joinedAliases);
|
|
6709
6842
|
if (targetSchema) {
|
|
6710
6843
|
// Keep the anchor map current so the alias we just joined can serve as the
|
|
6711
6844
|
// source for further direct lookups in the next fixpoint round
|
|
@@ -6730,8 +6863,8 @@ class CollectionQueryBuilder {
|
|
|
6730
6863
|
}
|
|
6731
6864
|
// Add all intermediate joins
|
|
6732
6865
|
for (const step of path) {
|
|
6733
|
-
if (!
|
|
6734
|
-
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);
|
|
6735
6868
|
if (stepSchema) {
|
|
6736
6869
|
joinedSchemas.set(step.alias, stepSchema);
|
|
6737
6870
|
}
|
|
@@ -6752,9 +6885,12 @@ class CollectionQueryBuilder {
|
|
|
6752
6885
|
// Memoised per registry — the BFS below depends only on the target alias and the ORDERED
|
|
6753
6886
|
// joined alias→table pairs (order decides which of several equal-length paths wins), so
|
|
6754
6887
|
// the same query shape resolves to the same path on every build. See NavigationPathCache.
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
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
|
+
}
|
|
6758
6894
|
return navigation_path_cache_1.NavigationPathCache.getOrBuild(this.schemaRegistry, signature, () => this.computeNavigationPath(targetAlias, joinedSchemas));
|
|
6759
6895
|
}
|
|
6760
6896
|
/** The uncached BFS behind {@link findNavigationPath}. */
|
|
@@ -6841,6 +6977,7 @@ class CollectionQueryBuilder {
|
|
|
6841
6977
|
val.constructor === Object;
|
|
6842
6978
|
};
|
|
6843
6979
|
// Helper function to recursively process fields and build SelectedField structures
|
|
6980
|
+
const collectionMarkerAlias = `__collection_${this.targetTable}__`;
|
|
6844
6981
|
const processField = (alias, field) => {
|
|
6845
6982
|
if (field instanceof conditions_1.SqlFragment) {
|
|
6846
6983
|
// SQL Fragment - build the SQL expression
|
|
@@ -6908,6 +7045,7 @@ class CollectionQueryBuilder {
|
|
|
6908
7045
|
nestedCteJoin: {
|
|
6909
7046
|
cteName: nestedResult.tableName,
|
|
6910
7047
|
joinClause: nestedJoinClause,
|
|
7048
|
+
memoId: nestedResult.memoId,
|
|
6911
7049
|
},
|
|
6912
7050
|
// Store nested collection info for recursive mapper transformation
|
|
6913
7051
|
nestedCollectionInfo: {
|
|
@@ -6950,11 +7088,10 @@ class CollectionQueryBuilder {
|
|
|
6950
7088
|
const sourceTable = field.__sourceTable; // Actual table name for schema lookup
|
|
6951
7089
|
// If tableAlias differs from the target table (or its collection marker), it's a navigation property reference
|
|
6952
7090
|
// The collection marker is `__collection_tableName__` and should be treated as the target table
|
|
6953
|
-
|
|
6954
|
-
if (tableAlias && tableAlias !== this.targetTable && tableAlias !== collectionMarker) {
|
|
7091
|
+
if (tableAlias && tableAlias !== this.targetTable && tableAlias !== collectionMarkerAlias) {
|
|
6955
7092
|
return { alias, expression: `"${tableAlias}"."${dbColumnName}"`, propertyName: fieldName, sourceTable };
|
|
6956
7093
|
}
|
|
6957
|
-
return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName };
|
|
7094
|
+
return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName, isColumn: true };
|
|
6958
7095
|
}
|
|
6959
7096
|
else if (typeof field === 'string') {
|
|
6960
7097
|
// Simple string reference (for backward compatibility)
|
|
@@ -6963,8 +7100,10 @@ class CollectionQueryBuilder {
|
|
|
6963
7100
|
else if (isPlainObject(field)) {
|
|
6964
7101
|
// Nested object - recursively process its fields
|
|
6965
7102
|
const nestedFields = [];
|
|
6966
|
-
for (const
|
|
6967
|
-
|
|
7103
|
+
for (const nestedAlias in field) {
|
|
7104
|
+
if (Object.prototype.hasOwnProperty.call(field, nestedAlias)) {
|
|
7105
|
+
nestedFields.push(processField(nestedAlias, field[nestedAlias]));
|
|
7106
|
+
}
|
|
6968
7107
|
}
|
|
6969
7108
|
return { alias, nested: nestedFields };
|
|
6970
7109
|
}
|
|
@@ -6994,6 +7133,7 @@ class CollectionQueryBuilder {
|
|
|
6994
7133
|
alias: dbColumnName,
|
|
6995
7134
|
expression: `"${dbColumnName}"`,
|
|
6996
7135
|
propertyName: fieldName,
|
|
7136
|
+
isColumn: true,
|
|
6997
7137
|
});
|
|
6998
7138
|
}
|
|
6999
7139
|
else if (selectedFields instanceof CollectionQueryBuilder || selectedFields instanceof conditions_1.SqlFragment) {
|
|
@@ -7002,8 +7142,11 @@ class CollectionQueryBuilder {
|
|
|
7002
7142
|
}
|
|
7003
7143
|
else {
|
|
7004
7144
|
// Object selection - extract each field (with support for nested objects)
|
|
7005
|
-
|
|
7006
|
-
|
|
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
|
+
}
|
|
7007
7150
|
}
|
|
7008
7151
|
}
|
|
7009
7152
|
}
|
|
@@ -7017,6 +7160,7 @@ class CollectionQueryBuilder {
|
|
|
7017
7160
|
alias: colName,
|
|
7018
7161
|
expression: `"${dbColumnName}"`,
|
|
7019
7162
|
propertyName: colName, // Same as alias when selecting all fields
|
|
7163
|
+
isColumn: true,
|
|
7020
7164
|
});
|
|
7021
7165
|
}
|
|
7022
7166
|
}
|
|
@@ -7054,13 +7198,7 @@ class CollectionQueryBuilder {
|
|
|
7054
7198
|
let orderByClauseAlias;
|
|
7055
7199
|
if (this.orderByFields.length > 0) {
|
|
7056
7200
|
// Build reverse lookup: db column name -> property name
|
|
7057
|
-
|
|
7058
|
-
if (this.targetTableSchema) {
|
|
7059
|
-
dbToPropertyMap = new Map();
|
|
7060
|
-
for (const [propName, meta] of getSchemaColumnMeta(this.targetTableSchema)) {
|
|
7061
|
-
dbToPropertyMap.set(meta.name, propName);
|
|
7062
|
-
}
|
|
7063
|
-
}
|
|
7201
|
+
const dbToPropertyMap = this.targetTableSchema ? getDbToPropertyMapForSchema(this.targetTableSchema) : null;
|
|
7064
7202
|
const orderPartsDb = this.orderByFields.map(({ field, direction }) => {
|
|
7065
7203
|
// field is already the database column name
|
|
7066
7204
|
return `"${field}" ${direction}`;
|
|
@@ -7148,8 +7286,14 @@ class CollectionQueryBuilder {
|
|
|
7148
7286
|
// which must be included in the lateral subquery for correlation
|
|
7149
7287
|
// Include selectMany joins in both all and selector navigation joins
|
|
7150
7288
|
// selectMany joins are structural (from flattening) and needed by both CTE and LATERAL
|
|
7151
|
-
|
|
7152
|
-
|
|
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];
|
|
7153
7297
|
// Step 6: Build CollectionAggregationConfig object
|
|
7154
7298
|
const config = {
|
|
7155
7299
|
relationName: this.relationName,
|
|
@@ -7214,6 +7358,7 @@ class CollectionQueryBuilder {
|
|
|
7214
7358
|
joinClause: result.joinClause,
|
|
7215
7359
|
selectExpression: result.selectExpression,
|
|
7216
7360
|
tableName: result.tableName,
|
|
7361
|
+
memoId: result.memoId,
|
|
7217
7362
|
};
|
|
7218
7363
|
}
|
|
7219
7364
|
}
|