linkgress-orm 0.1.0 → 0.1.2
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/LICENSE +21 -21
- package/README.md +196 -196
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +7 -5
- package/dist/entity/db-context.js.map +1 -1
- package/dist/migration/db-schema-manager.d.ts +5 -0
- package/dist/migration/db-schema-manager.d.ts.map +1 -1
- package/dist/migration/db-schema-manager.js +147 -79
- package/dist/migration/db-schema-manager.js.map +1 -1
- package/dist/migration/enum-migrator.js +6 -6
- package/dist/query/collection-strategy.interface.d.ts +38 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/cte-builder.d.ts +18 -1
- package/dist/query/cte-builder.d.ts.map +1 -1
- package/dist/query/cte-builder.js +102 -11
- package/dist/query/cte-builder.js.map +1 -1
- package/dist/query/grouped-query.d.ts +24 -1
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +260 -71
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/join-builder.d.ts.map +1 -1
- package/dist/query/join-builder.js +10 -14
- package/dist/query/join-builder.js.map +1 -1
- package/dist/query/query-builder.d.ts +65 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +566 -167
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts +4 -0
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +169 -78
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +4 -0
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +86 -28
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +97 -97
- package/dist/schema/table-builder.d.ts +16 -0
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +23 -1
- package/dist/schema/table-builder.js.map +1 -1
- package/package.json +1 -1
|
@@ -83,9 +83,9 @@ class GroupedQueryBuilder {
|
|
|
83
83
|
*/
|
|
84
84
|
createMockRow() {
|
|
85
85
|
const mock = {};
|
|
86
|
-
// Add columns as FieldRef objects
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
// Add columns as FieldRef objects - use pre-computed column name map if available
|
|
87
|
+
const columnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(this.schema);
|
|
88
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
89
89
|
Object.defineProperty(mock, colName, {
|
|
90
90
|
get: () => ({
|
|
91
91
|
__fieldName: colName,
|
|
@@ -97,15 +97,11 @@ class GroupedQueryBuilder {
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
// Add navigation properties (collections and single references)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
|
|
100
|
+
// Performance: Use pre-computed relation entries and cached schemas
|
|
101
|
+
const relationEntries = (0, query_builder_1.getRelationEntriesForSchema)(this.schema);
|
|
102
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
103
|
+
const targetSchema = (0, query_builder_1.getTargetSchemaForRelation)(this.schema, relName, relConfig);
|
|
107
104
|
if (relConfig.type === 'many') {
|
|
108
|
-
const targetSchema = relationSchemas.get(relName);
|
|
109
105
|
Object.defineProperty(mock, relName, {
|
|
110
106
|
get: () => {
|
|
111
107
|
return new query_builder_1.CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
|
|
@@ -115,7 +111,6 @@ class GroupedQueryBuilder {
|
|
|
115
111
|
});
|
|
116
112
|
}
|
|
117
113
|
else {
|
|
118
|
-
const targetSchema = relationSchemas.get(relName);
|
|
119
114
|
Object.defineProperty(mock, relName, {
|
|
120
115
|
get: () => {
|
|
121
116
|
const refBuilder = new query_builder_1.ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
@@ -131,8 +126,8 @@ class GroupedQueryBuilder {
|
|
|
131
126
|
if (join.isSubquery || !join.schema) {
|
|
132
127
|
continue;
|
|
133
128
|
}
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
|
|
130
|
+
for (const [colName, dbColumnName] of joinColumnNameMap) {
|
|
136
131
|
if (!mock[join.alias]) {
|
|
137
132
|
mock[join.alias] = {};
|
|
138
133
|
}
|
|
@@ -221,12 +216,52 @@ class GroupedSelectQueryBuilder {
|
|
|
221
216
|
return this.transformResults(result.rows);
|
|
222
217
|
}
|
|
223
218
|
/**
|
|
224
|
-
* Transform database results - convert aggregate values
|
|
219
|
+
* Transform database results - convert aggregate values and apply mappers
|
|
225
220
|
*/
|
|
226
221
|
transformResults(rows) {
|
|
227
|
-
// Get the mock result to identify which fields are aggregates
|
|
222
|
+
// Get the mock result to identify which fields are aggregates and have mappers
|
|
228
223
|
const mockGroup = this.createMockGroupedItem();
|
|
229
224
|
const mockResult = this.resultSelector(mockGroup);
|
|
225
|
+
// Also get the original selection to track field origins for mapper lookup
|
|
226
|
+
const mockRow = this.createMockRow();
|
|
227
|
+
const mockOriginalSelection = this.originalSelector(mockRow);
|
|
228
|
+
// Build column metadata cache from schema for mapper lookup
|
|
229
|
+
const columnMetadataCache = {};
|
|
230
|
+
for (const [key, mockValue] of Object.entries(mockResult)) {
|
|
231
|
+
// Check if mockValue has getMapper (SqlFragment or aliased field with mapper)
|
|
232
|
+
if (typeof mockValue === 'object' && mockValue !== null && typeof mockValue.getMapper === 'function') {
|
|
233
|
+
const mapper = mockValue.getMapper();
|
|
234
|
+
if (mapper) {
|
|
235
|
+
columnMetadataCache[key] = { hasMapper: true, mapper };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Check if this is a FieldRef from schema column
|
|
239
|
+
else if (typeof mockValue === 'object' && mockValue !== null && '__fieldName' in mockValue) {
|
|
240
|
+
const fieldName = mockValue.__fieldName;
|
|
241
|
+
// Look up in schema
|
|
242
|
+
const column = this.schema.columns[fieldName];
|
|
243
|
+
if (column) {
|
|
244
|
+
const config = column.build();
|
|
245
|
+
if (config.mapper) {
|
|
246
|
+
columnMetadataCache[key] = { hasMapper: true, mapper: config.mapper };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Also check original selection for mapper (for aliased fields like p.key.distinctDay)
|
|
250
|
+
else if (mockOriginalSelection && fieldName in mockOriginalSelection) {
|
|
251
|
+
const origValue = mockOriginalSelection[fieldName];
|
|
252
|
+
if (typeof origValue === 'object' && origValue !== null && '__fieldName' in origValue) {
|
|
253
|
+
const origFieldName = origValue.__fieldName;
|
|
254
|
+
const origColumn = this.schema.columns[origFieldName];
|
|
255
|
+
if (origColumn) {
|
|
256
|
+
const config = origColumn.build();
|
|
257
|
+
if (config.mapper) {
|
|
258
|
+
columnMetadataCache[key] = { hasMapper: true, mapper: config.mapper };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
230
265
|
return rows.map(row => {
|
|
231
266
|
const transformed = {};
|
|
232
267
|
for (const [key, value] of Object.entries(row)) {
|
|
@@ -242,14 +277,81 @@ class GroupedSelectQueryBuilder {
|
|
|
242
277
|
transformed[key] = value;
|
|
243
278
|
}
|
|
244
279
|
}
|
|
280
|
+
// Check if this field has a mapper
|
|
281
|
+
else if (columnMetadataCache[key]?.hasMapper) {
|
|
282
|
+
const mapper = columnMetadataCache[key].mapper;
|
|
283
|
+
if (value === null || value === undefined) {
|
|
284
|
+
transformed[key] = null;
|
|
285
|
+
}
|
|
286
|
+
else if (typeof mapper.fromDriver === 'function') {
|
|
287
|
+
transformed[key] = mapper.fromDriver(value);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
transformed[key] = value;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
245
293
|
else {
|
|
246
|
-
// Non-aggregate field - keep as is
|
|
294
|
+
// Non-aggregate field without mapper - keep as is
|
|
247
295
|
transformed[key] = value;
|
|
248
296
|
}
|
|
249
297
|
}
|
|
250
298
|
return transformed;
|
|
251
299
|
});
|
|
252
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Get selection metadata for mapper preservation in CTEs
|
|
303
|
+
* Enhances the selection result with mapper info from original schema columns
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
getSelectionMetadata() {
|
|
307
|
+
const mockGroup = this.createMockGroupedItem();
|
|
308
|
+
const mockResult = this.resultSelector(mockGroup);
|
|
309
|
+
// Get original selection to map field names back to schema columns
|
|
310
|
+
const mockRow = this.createMockRow();
|
|
311
|
+
const mockOriginalSelection = this.originalSelector(mockRow);
|
|
312
|
+
// Build enhanced metadata with mappers
|
|
313
|
+
const enhancedMetadata = {};
|
|
314
|
+
for (const [key, value] of Object.entries(mockResult)) {
|
|
315
|
+
// Check if it's a FieldRef
|
|
316
|
+
if (typeof value === 'object' && value !== null && '__fieldName' in value) {
|
|
317
|
+
const fieldName = value.__fieldName;
|
|
318
|
+
// First check if schema has mapper for this field
|
|
319
|
+
const column = this.schema.columns[fieldName];
|
|
320
|
+
if (column) {
|
|
321
|
+
const config = column.build();
|
|
322
|
+
if (config.mapper) {
|
|
323
|
+
// Add mapper info to the metadata
|
|
324
|
+
enhancedMetadata[key] = {
|
|
325
|
+
...value,
|
|
326
|
+
getMapper: () => config.mapper,
|
|
327
|
+
};
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Check original selection for mapper (for aliased fields like p.key.distinctDay)
|
|
332
|
+
if (mockOriginalSelection && fieldName in mockOriginalSelection) {
|
|
333
|
+
const origValue = mockOriginalSelection[fieldName];
|
|
334
|
+
if (typeof origValue === 'object' && origValue !== null && '__fieldName' in origValue) {
|
|
335
|
+
const origFieldName = origValue.__fieldName;
|
|
336
|
+
const origColumn = this.schema.columns[origFieldName];
|
|
337
|
+
if (origColumn) {
|
|
338
|
+
const config = origColumn.build();
|
|
339
|
+
if (config.mapper) {
|
|
340
|
+
enhancedMetadata[key] = {
|
|
341
|
+
...value,
|
|
342
|
+
getMapper: () => config.mapper,
|
|
343
|
+
};
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// No mapper found, use original value
|
|
351
|
+
enhancedMetadata[key] = value;
|
|
352
|
+
}
|
|
353
|
+
return enhancedMetadata;
|
|
354
|
+
}
|
|
253
355
|
/**
|
|
254
356
|
* Execute query and return first result or null
|
|
255
357
|
*/
|
|
@@ -282,7 +384,9 @@ class GroupedSelectQueryBuilder {
|
|
|
282
384
|
outerContext.paramCounter = context.paramCounter;
|
|
283
385
|
return sql;
|
|
284
386
|
};
|
|
285
|
-
|
|
387
|
+
// Get selection metadata with mappers for table subqueries
|
|
388
|
+
const selectionMetadata = mode === 'table' ? this.getSelectionMetadata() : undefined;
|
|
389
|
+
return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata);
|
|
286
390
|
}
|
|
287
391
|
/**
|
|
288
392
|
* Build SQL for use in CTEs - public interface for CTE builder
|
|
@@ -453,12 +557,34 @@ class GroupedSelectQueryBuilder {
|
|
|
453
557
|
const mockRow = this.createMockRow();
|
|
454
558
|
const mockOriginalSelection = this.originalSelector(mockRow);
|
|
455
559
|
const mockGroupingKey = this.groupingKeySelector(mockOriginalSelection);
|
|
456
|
-
|
|
560
|
+
// Create mock grouped item using the SAME grouping key (not a fresh one)
|
|
561
|
+
// This ensures SqlFragment instances are shared between GROUP BY and SELECT
|
|
562
|
+
const mockGroup = {
|
|
563
|
+
key: mockGroupingKey,
|
|
564
|
+
count: () => createAggregateFieldRef('COUNT'),
|
|
565
|
+
sum: (selector) => createAggregateFieldRef('SUM', selector),
|
|
566
|
+
min: (selector) => createAggregateFieldRef('MIN', selector),
|
|
567
|
+
max: (selector) => createAggregateFieldRef('MAX', selector),
|
|
568
|
+
avg: (selector) => createAggregateFieldRef('AVG', selector),
|
|
569
|
+
};
|
|
457
570
|
const mockResult = this.resultSelector(mockGroup);
|
|
458
571
|
// Extract GROUP BY fields from the grouping key
|
|
572
|
+
// We build these first and cache the SQL for SqlFragments so they can be reused in SELECT
|
|
459
573
|
const groupByFields = [];
|
|
574
|
+
const sqlFragmentCache = new Map(); // Cache built SQL for reuse in SELECT
|
|
460
575
|
for (const [key, value] of Object.entries(mockGroupingKey)) {
|
|
461
|
-
if (
|
|
576
|
+
if (value instanceof conditions_1.SqlFragment) {
|
|
577
|
+
// SqlFragment in GROUP BY - build the SQL expression and cache it
|
|
578
|
+
const sqlBuildContext = {
|
|
579
|
+
paramCounter: context.paramCounter,
|
|
580
|
+
params: context.allParams,
|
|
581
|
+
};
|
|
582
|
+
const fragmentSql = value.buildSql(sqlBuildContext);
|
|
583
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
584
|
+
groupByFields.push(fragmentSql);
|
|
585
|
+
sqlFragmentCache.set(value, fragmentSql);
|
|
586
|
+
}
|
|
587
|
+
else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
|
|
462
588
|
const field = value;
|
|
463
589
|
const tableAlias = field.__tableAlias || this.schema.name;
|
|
464
590
|
groupByFields.push(`"${tableAlias}"."${field.__dbColumnName}"`);
|
|
@@ -477,8 +603,9 @@ class GroupedSelectQueryBuilder {
|
|
|
477
603
|
}
|
|
478
604
|
else if (aggField.__aggregateSelector) {
|
|
479
605
|
// SUM, MIN, MAX, AVG with selector
|
|
480
|
-
|
|
481
|
-
|
|
606
|
+
// Note: The selector references fields from the ORIGINAL SELECTION (after first .select()),
|
|
607
|
+
// not the raw table row. So we need to use mockOriginalSelection, not a fresh mock row.
|
|
608
|
+
const field = aggField.__aggregateSelector(mockOriginalSelection);
|
|
482
609
|
if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
483
610
|
const fieldRef = field;
|
|
484
611
|
const tableAlias = fieldRef.__tableAlias || this.schema.name;
|
|
@@ -505,8 +632,8 @@ class GroupedSelectQueryBuilder {
|
|
|
505
632
|
selectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
|
|
506
633
|
}
|
|
507
634
|
else if (agg.__selector) {
|
|
508
|
-
|
|
509
|
-
const field = agg.__selector(
|
|
635
|
+
// Use mockOriginalSelection - the selector references fields from the first .select()
|
|
636
|
+
const field = agg.__selector(mockOriginalSelection);
|
|
510
637
|
if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
511
638
|
const fieldRef = field;
|
|
512
639
|
const tableAlias = fieldRef.__tableAlias || this.schema.name;
|
|
@@ -526,14 +653,22 @@ class GroupedSelectQueryBuilder {
|
|
|
526
653
|
selectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${alias}"`);
|
|
527
654
|
}
|
|
528
655
|
else if (value instanceof conditions_1.SqlFragment) {
|
|
529
|
-
// SQL fragment
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
656
|
+
// SQL fragment - check if we already built this for GROUP BY
|
|
657
|
+
const cachedSql = sqlFragmentCache.get(value);
|
|
658
|
+
if (cachedSql) {
|
|
659
|
+
// Reuse the cached SQL to ensure same parameter numbers
|
|
660
|
+
selectParts.push(`${cachedSql} as "${alias}"`);
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
// Build new SQL for this fragment
|
|
664
|
+
const sqlBuildContext = {
|
|
665
|
+
paramCounter: context.paramCounter,
|
|
666
|
+
params: context.allParams,
|
|
667
|
+
};
|
|
668
|
+
const fragmentSql = value.buildSql(sqlBuildContext);
|
|
669
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
670
|
+
selectParts.push(`${fragmentSql} as "${alias}"`);
|
|
671
|
+
}
|
|
537
672
|
}
|
|
538
673
|
}
|
|
539
674
|
// Detect navigation property references in WHERE and add JOINs
|
|
@@ -621,9 +756,9 @@ class GroupedSelectQueryBuilder {
|
|
|
621
756
|
*/
|
|
622
757
|
createMockRow() {
|
|
623
758
|
const mock = {};
|
|
624
|
-
// Add columns as FieldRef objects
|
|
625
|
-
|
|
626
|
-
|
|
759
|
+
// Add columns as FieldRef objects - use pre-computed column name map if available
|
|
760
|
+
const columnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(this.schema);
|
|
761
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
627
762
|
Object.defineProperty(mock, colName, {
|
|
628
763
|
get: () => ({
|
|
629
764
|
__fieldName: colName,
|
|
@@ -635,15 +770,11 @@ class GroupedSelectQueryBuilder {
|
|
|
635
770
|
});
|
|
636
771
|
}
|
|
637
772
|
// Add navigation properties (collections and single references)
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
|
|
773
|
+
// Performance: Use pre-computed relation entries and cached schemas
|
|
774
|
+
const relationEntries = (0, query_builder_1.getRelationEntriesForSchema)(this.schema);
|
|
775
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
776
|
+
const targetSchema = (0, query_builder_1.getTargetSchemaForRelation)(this.schema, relName, relConfig);
|
|
645
777
|
if (relConfig.type === 'many') {
|
|
646
|
-
const targetSchema = relationSchemas.get(relName);
|
|
647
778
|
Object.defineProperty(mock, relName, {
|
|
648
779
|
get: () => {
|
|
649
780
|
return new query_builder_1.CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
|
|
@@ -653,7 +784,6 @@ class GroupedSelectQueryBuilder {
|
|
|
653
784
|
});
|
|
654
785
|
}
|
|
655
786
|
else {
|
|
656
|
-
const targetSchema = relationSchemas.get(relName);
|
|
657
787
|
Object.defineProperty(mock, relName, {
|
|
658
788
|
get: () => {
|
|
659
789
|
const refBuilder = new query_builder_1.ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
@@ -669,8 +799,8 @@ class GroupedSelectQueryBuilder {
|
|
|
669
799
|
if (join.isSubquery || !join.schema) {
|
|
670
800
|
continue;
|
|
671
801
|
}
|
|
672
|
-
|
|
673
|
-
|
|
802
|
+
const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
|
|
803
|
+
for (const [colName, dbColumnName] of joinColumnNameMap) {
|
|
674
804
|
if (!mock[join.alias]) {
|
|
675
805
|
mock[join.alias] = {};
|
|
676
806
|
}
|
|
@@ -760,27 +890,13 @@ class GroupedSelectQueryBuilder {
|
|
|
760
890
|
for (const [, value] of Object.entries(selection)) {
|
|
761
891
|
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
762
892
|
// This is a FieldRef with a table alias - check if it's from a related table
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if (relation.targetTableBuilder) {
|
|
771
|
-
const targetTableSchema = relation.targetTableBuilder.build();
|
|
772
|
-
targetSchema = targetTableSchema.schema;
|
|
773
|
-
}
|
|
774
|
-
// Add a JOIN for this reference
|
|
775
|
-
joins.push({
|
|
776
|
-
alias: tableAlias,
|
|
777
|
-
targetTable: relation.targetTable,
|
|
778
|
-
targetSchema,
|
|
779
|
-
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
780
|
-
matches: relation.matches || [],
|
|
781
|
-
isMandatory: relation.isMandatory ?? false,
|
|
782
|
-
});
|
|
783
|
-
}
|
|
893
|
+
this.addJoinForFieldRef(value, joins);
|
|
894
|
+
}
|
|
895
|
+
else if (value instanceof conditions_1.SqlFragment) {
|
|
896
|
+
// SqlFragment may contain navigation property references - extract them
|
|
897
|
+
const fieldRefs = value.getFieldRefs();
|
|
898
|
+
for (const fieldRef of fieldRefs) {
|
|
899
|
+
this.addJoinForFieldRef(fieldRef, joins);
|
|
784
900
|
}
|
|
785
901
|
}
|
|
786
902
|
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
@@ -789,6 +905,36 @@ class GroupedSelectQueryBuilder {
|
|
|
789
905
|
}
|
|
790
906
|
}
|
|
791
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* Add a JOIN for a FieldRef if it references a related table
|
|
910
|
+
*/
|
|
911
|
+
addJoinForFieldRef(fieldRef, joins) {
|
|
912
|
+
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef) || !('__dbColumnName' in fieldRef)) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
916
|
+
if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
917
|
+
// This references a related table - find the relation and add a JOIN
|
|
918
|
+
const relation = this.schema.relations[tableAlias];
|
|
919
|
+
if (relation && relation.type === 'one') {
|
|
920
|
+
// Get target schema from targetTableBuilder if available
|
|
921
|
+
let targetSchema;
|
|
922
|
+
if (relation.targetTableBuilder) {
|
|
923
|
+
const targetTableSchema = relation.targetTableBuilder.build();
|
|
924
|
+
targetSchema = targetTableSchema.schema;
|
|
925
|
+
}
|
|
926
|
+
// Add a JOIN for this reference
|
|
927
|
+
joins.push({
|
|
928
|
+
alias: tableAlias,
|
|
929
|
+
targetTable: relation.targetTable,
|
|
930
|
+
targetSchema,
|
|
931
|
+
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
932
|
+
matches: relation.matches || [],
|
|
933
|
+
isMandatory: relation.isMandatory ?? false,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
792
938
|
/**
|
|
793
939
|
* Build HAVING condition SQL - handles aggregate field refs specially
|
|
794
940
|
*/
|
|
@@ -974,20 +1120,63 @@ class GroupedJoinedQueryBuilder {
|
|
|
974
1120
|
const selectionMetadata = this.resultSelector(mockLeft, mockRight);
|
|
975
1121
|
return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata);
|
|
976
1122
|
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Get CTEs used by this query builder
|
|
1125
|
+
* @internal
|
|
1126
|
+
*/
|
|
1127
|
+
getReferencedCtes() {
|
|
1128
|
+
return this.cte ? [this.cte] : [];
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Get selection metadata for mapper preservation in CTEs
|
|
1132
|
+
* Enhances the selection result with mapper info from the left subquery
|
|
1133
|
+
* @internal
|
|
1134
|
+
*/
|
|
1135
|
+
getSelectionMetadata() {
|
|
1136
|
+
const mockLeft = this.createLeftMock();
|
|
1137
|
+
const mockRight = this.createRightMock();
|
|
1138
|
+
const mockResult = this.resultSelector(mockLeft, mockRight);
|
|
1139
|
+
// Get mapper metadata from the left subquery
|
|
1140
|
+
const leftMetadata = this.leftSubquery.getSelectionMetadata();
|
|
1141
|
+
// Build enhanced metadata with mappers
|
|
1142
|
+
const enhancedMetadata = {};
|
|
1143
|
+
for (const [key, value] of Object.entries(mockResult)) {
|
|
1144
|
+
// Check if it's a FieldRef from the left side
|
|
1145
|
+
if (typeof value === 'object' && value !== null && '__fieldName' in value) {
|
|
1146
|
+
const fieldName = value.__fieldName;
|
|
1147
|
+
// Check if left metadata has mapper for this field
|
|
1148
|
+
if (leftMetadata && fieldName in leftMetadata) {
|
|
1149
|
+
const leftValue = leftMetadata[fieldName];
|
|
1150
|
+
if (typeof leftValue === 'object' && leftValue !== null && typeof leftValue.getMapper === 'function') {
|
|
1151
|
+
enhancedMetadata[key] = {
|
|
1152
|
+
...value,
|
|
1153
|
+
getMapper: leftValue.getMapper,
|
|
1154
|
+
};
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
// No mapper found, use original value
|
|
1160
|
+
enhancedMetadata[key] = value;
|
|
1161
|
+
}
|
|
1162
|
+
return enhancedMetadata;
|
|
1163
|
+
}
|
|
977
1164
|
/**
|
|
978
1165
|
* Build SQL for use in CTEs - public interface for CTE builder
|
|
1166
|
+
* This returns SQL WITHOUT the WITH clause - CTEs should be extracted separately via getReferencedCtes()
|
|
979
1167
|
* @internal
|
|
980
1168
|
*/
|
|
981
1169
|
buildCteQuery(queryContext) {
|
|
982
|
-
return this.buildQuery(queryContext);
|
|
1170
|
+
return this.buildQuery(queryContext, true);
|
|
983
1171
|
}
|
|
984
1172
|
/**
|
|
985
1173
|
* Build the SQL query
|
|
1174
|
+
* @param skipCteClause If true, don't include WITH clause (for embedding in outer CTEs)
|
|
986
1175
|
*/
|
|
987
|
-
buildQuery(context) {
|
|
988
|
-
// Build CTE clause if needed
|
|
1176
|
+
buildQuery(context, skipCteClause = false) {
|
|
1177
|
+
// Build CTE clause if needed (unless we're being embedded in another CTE)
|
|
989
1178
|
let cteClause = '';
|
|
990
|
-
if (this.cte) {
|
|
1179
|
+
if (this.cte && !skipCteClause) {
|
|
991
1180
|
cteClause = `WITH "${this.cte.name}" AS (${this.cte.query})\n`;
|
|
992
1181
|
context.allParams.push(...this.cte.params);
|
|
993
1182
|
context.paramCounter += this.cte.params.length;
|