linkgress-orm 0.0.3 → 0.1.1
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/README.md +3 -3
- package/dist/entity/db-column.d.ts +38 -1
- package/dist/entity/db-column.d.ts.map +1 -1
- package/dist/entity/db-column.js.map +1 -1
- package/dist/entity/db-context.d.ts +429 -50
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +884 -203
- package/dist/entity/db-context.js.map +1 -1
- package/dist/entity/entity-base.d.ts +8 -0
- package/dist/entity/entity-base.d.ts.map +1 -1
- package/dist/entity/entity-base.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/query/collection-strategy.factory.d.ts.map +1 -1
- package/dist/query/collection-strategy.factory.js +7 -3
- package/dist/query/collection-strategy.factory.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +12 -6
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/conditions.d.ts +134 -23
- package/dist/query/conditions.d.ts.map +1 -1
- package/dist/query/conditions.js +58 -0
- package/dist/query/conditions.js.map +1 -1
- package/dist/query/cte-builder.d.ts +24 -5
- package/dist/query/cte-builder.d.ts.map +1 -1
- package/dist/query/cte-builder.js +45 -7
- package/dist/query/cte-builder.js.map +1 -1
- package/dist/query/grouped-query.d.ts +196 -8
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +586 -54
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/join-builder.d.ts +5 -4
- package/dist/query/join-builder.d.ts.map +1 -1
- package/dist/query/join-builder.js +21 -47
- package/dist/query/join-builder.js.map +1 -1
- package/dist/query/query-builder.d.ts +118 -20
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +511 -280
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-utils.d.ts +45 -0
- package/dist/query/query-utils.d.ts.map +1 -0
- package/dist/query/query-utils.js +103 -0
- package/dist/query/query-utils.js.map +1 -0
- package/dist/query/sql-utils.d.ts +83 -0
- package/dist/query/sql-utils.d.ts.map +1 -0
- package/dist/query/sql-utils.js +218 -0
- package/dist/query/sql-utils.js.map +1 -0
- package/dist/query/strategies/cte-collection-strategy.d.ts +85 -0
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -0
- package/dist/query/strategies/cte-collection-strategy.js +338 -0
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -0
- package/dist/query/strategies/lateral-collection-strategy.d.ts +59 -0
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -0
- package/dist/query/strategies/lateral-collection-strategy.js +243 -0
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -0
- package/dist/query/strategies/temptable-collection-strategy.d.ts +21 -0
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +160 -38
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/dist/query/subquery.d.ts +24 -1
- package/dist/query/subquery.d.ts.map +1 -1
- package/dist/query/subquery.js +38 -2
- package/dist/query/subquery.js.map +1 -1
- 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
- package/dist/query/strategies/jsonb-collection-strategy.d.ts +0 -51
- package/dist/query/strategies/jsonb-collection-strategy.d.ts.map +0 -1
- package/dist/query/strategies/jsonb-collection-strategy.js +0 -210
- package/dist/query/strategies/jsonb-collection-strategy.js.map +0 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GroupedSelectQueryBuilder = exports.GroupedQueryBuilder = void 0;
|
|
3
|
+
exports.GroupedJoinedQueryBuilder = exports.GroupedSelectQueryBuilder = exports.GroupedQueryBuilder = void 0;
|
|
4
4
|
const conditions_1 = require("./conditions");
|
|
5
|
+
const query_utils_1 = require("./query-utils");
|
|
5
6
|
const subquery_1 = require("./subquery");
|
|
7
|
+
const query_builder_1 = require("./query-builder");
|
|
8
|
+
const cte_builder_1 = require("./cte-builder");
|
|
6
9
|
/**
|
|
7
10
|
* Create an aggregate field reference that can be used in conditions
|
|
8
11
|
*/
|
|
@@ -80,9 +83,9 @@ class GroupedQueryBuilder {
|
|
|
80
83
|
*/
|
|
81
84
|
createMockRow() {
|
|
82
85
|
const mock = {};
|
|
83
|
-
// Add columns as FieldRef objects
|
|
84
|
-
|
|
85
|
-
|
|
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) {
|
|
86
89
|
Object.defineProperty(mock, colName, {
|
|
87
90
|
get: () => ({
|
|
88
91
|
__fieldName: colName,
|
|
@@ -93,13 +96,38 @@ class GroupedQueryBuilder {
|
|
|
93
96
|
configurable: true,
|
|
94
97
|
});
|
|
95
98
|
}
|
|
99
|
+
// Add navigation properties (collections and single references)
|
|
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);
|
|
104
|
+
if (relConfig.type === 'many') {
|
|
105
|
+
Object.defineProperty(mock, relName, {
|
|
106
|
+
get: () => {
|
|
107
|
+
return new query_builder_1.CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
|
|
108
|
+
},
|
|
109
|
+
enumerable: true,
|
|
110
|
+
configurable: true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
Object.defineProperty(mock, relName, {
|
|
115
|
+
get: () => {
|
|
116
|
+
const refBuilder = new query_builder_1.ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
117
|
+
return refBuilder.createMockTargetRow();
|
|
118
|
+
},
|
|
119
|
+
enumerable: true,
|
|
120
|
+
configurable: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
96
124
|
// Add columns from manually joined tables
|
|
97
125
|
for (const join of this.manualJoins) {
|
|
98
126
|
if (join.isSubquery || !join.schema) {
|
|
99
127
|
continue;
|
|
100
128
|
}
|
|
101
|
-
|
|
102
|
-
|
|
129
|
+
const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
|
|
130
|
+
for (const [colName, dbColumnName] of joinColumnNameMap) {
|
|
103
131
|
if (!mock[join.alias]) {
|
|
104
132
|
mock[join.alias] = {};
|
|
105
133
|
}
|
|
@@ -166,35 +194,7 @@ class GroupedSelectQueryBuilder {
|
|
|
166
194
|
const mockGroup = this.createMockGroupedItem();
|
|
167
195
|
const mockResult = this.resultSelector(mockGroup);
|
|
168
196
|
const result = selector(mockResult);
|
|
169
|
-
|
|
170
|
-
if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
|
|
171
|
-
for (const [fieldRef, direction] of result) {
|
|
172
|
-
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
173
|
-
this.orderByFields.push({
|
|
174
|
-
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
175
|
-
direction: direction || 'ASC'
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
// Handle array of fields (all ASC)
|
|
181
|
-
else if (Array.isArray(result)) {
|
|
182
|
-
for (const fieldRef of result) {
|
|
183
|
-
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
184
|
-
this.orderByFields.push({
|
|
185
|
-
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
186
|
-
direction: 'ASC'
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
// Handle single field
|
|
192
|
-
else if (result && typeof result === 'object' && '__fieldName' in result) {
|
|
193
|
-
this.orderByFields.push({
|
|
194
|
-
field: result.__dbColumnName || result.__fieldName,
|
|
195
|
-
direction: 'ASC'
|
|
196
|
-
});
|
|
197
|
-
}
|
|
197
|
+
(0, query_utils_1.parseOrderBy)(result, this.orderByFields);
|
|
198
198
|
return this;
|
|
199
199
|
}
|
|
200
200
|
/**
|
|
@@ -279,6 +279,167 @@ class GroupedSelectQueryBuilder {
|
|
|
279
279
|
};
|
|
280
280
|
return new subquery_1.Subquery(sqlBuilder, mode);
|
|
281
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Build SQL for use in CTEs - public interface for CTE builder
|
|
284
|
+
* @internal
|
|
285
|
+
*/
|
|
286
|
+
buildCteQuery(queryContext) {
|
|
287
|
+
return this.buildQuery(queryContext);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Add a LEFT JOIN to the grouped query result
|
|
291
|
+
* This wraps the grouped query as a subquery and joins to it
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* const result = await db.orders
|
|
295
|
+
* .select(o => ({ customerId: o.customerId, total: o.total }))
|
|
296
|
+
* .groupBy(o => ({ customerId: o.customerId }))
|
|
297
|
+
* .select(g => ({ customerId: g.key.customerId, totalSum: g.sum(o => o.total) }))
|
|
298
|
+
* .leftJoin(
|
|
299
|
+
* customerDetailsCte,
|
|
300
|
+
* (grouped, details) => eq(grouped.customerId, details.customerId),
|
|
301
|
+
* (grouped, details) => ({ ...grouped, details: details.items })
|
|
302
|
+
* )
|
|
303
|
+
* .toList();
|
|
304
|
+
*/
|
|
305
|
+
leftJoin(rightSource, condition, selector, alias) {
|
|
306
|
+
return this.joinInternal('LEFT', rightSource, condition, selector, alias);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Add an INNER JOIN to the grouped query result
|
|
310
|
+
* This wraps the grouped query as a subquery and joins to it
|
|
311
|
+
*/
|
|
312
|
+
innerJoin(rightSource, condition, selector, alias) {
|
|
313
|
+
return this.joinInternal('INNER', rightSource, condition, selector, alias);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Internal join implementation
|
|
317
|
+
*/
|
|
318
|
+
joinInternal(joinType, rightSource, condition, selector, alias) {
|
|
319
|
+
// Wrap this grouped query as a subquery
|
|
320
|
+
const leftSubquery = this.asSubquery('table');
|
|
321
|
+
const leftAlias = 'grouped_0';
|
|
322
|
+
// Determine the right alias and source info
|
|
323
|
+
let rightAlias;
|
|
324
|
+
let isCteJoin = false;
|
|
325
|
+
let cte;
|
|
326
|
+
if ((0, cte_builder_1.isCte)(rightSource)) {
|
|
327
|
+
rightAlias = rightSource.name;
|
|
328
|
+
isCteJoin = true;
|
|
329
|
+
cte = rightSource;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
if (!alias) {
|
|
333
|
+
throw new Error('Alias is required when joining a subquery');
|
|
334
|
+
}
|
|
335
|
+
rightAlias = alias;
|
|
336
|
+
}
|
|
337
|
+
// Create mock for left (the grouped query result)
|
|
338
|
+
const mockLeft = this.createMockForSelection(leftAlias);
|
|
339
|
+
// Create mock for right - at runtime these are already FieldRef-like objects
|
|
340
|
+
const mockRight = (isCteJoin
|
|
341
|
+
? this.createMockForCte(cte)
|
|
342
|
+
: this.createMockForSubquery(rightAlias, rightSource));
|
|
343
|
+
// Evaluate the join condition
|
|
344
|
+
const joinCondition = condition(mockLeft, mockRight);
|
|
345
|
+
// Create the result selector
|
|
346
|
+
const createLeftMock = () => this.createMockForSelection(leftAlias);
|
|
347
|
+
const createRightMock = () => (isCteJoin
|
|
348
|
+
? this.createMockForCte(cte)
|
|
349
|
+
: this.createMockForSubquery(rightAlias, rightSource));
|
|
350
|
+
return new GroupedJoinedQueryBuilder(this.schema, this.client, leftSubquery, leftAlias, rightSource, rightAlias, joinType, joinCondition, selector, createLeftMock, createRightMock, this.executor, isCteJoin ? cte : undefined);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Create a mock object for the current selection (for join conditions)
|
|
354
|
+
* The key is the alias used in the SELECT clause, so we use it as __dbColumnName
|
|
355
|
+
*/
|
|
356
|
+
createMockForSelection(alias) {
|
|
357
|
+
const mockGroup = this.createMockGroupedItem();
|
|
358
|
+
const mockResult = this.resultSelector(mockGroup);
|
|
359
|
+
// Wrap with alias - always use the key as the column name since
|
|
360
|
+
// that's what the subquery SELECT clause uses as the alias
|
|
361
|
+
const wrapped = {};
|
|
362
|
+
for (const [key, value] of Object.entries(mockResult)) {
|
|
363
|
+
// Preserve mapper if present
|
|
364
|
+
const mapper = (typeof value === 'object' && value !== null && typeof value.getMapper === 'function')
|
|
365
|
+
? { getMapper: () => value.getMapper() }
|
|
366
|
+
: {};
|
|
367
|
+
wrapped[key] = {
|
|
368
|
+
__fieldName: key,
|
|
369
|
+
__dbColumnName: key, // Use key as column name (the subquery alias)
|
|
370
|
+
__tableAlias: alias,
|
|
371
|
+
...mapper,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return wrapped;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Create a mock for a subquery result
|
|
378
|
+
*/
|
|
379
|
+
createMockForSubquery(alias, subquery) {
|
|
380
|
+
const selectionMetadata = subquery.getSelectionMetadata();
|
|
381
|
+
return new Proxy({}, {
|
|
382
|
+
get(target, prop) {
|
|
383
|
+
if (typeof prop === 'symbol')
|
|
384
|
+
return undefined;
|
|
385
|
+
if (selectionMetadata && prop in selectionMetadata) {
|
|
386
|
+
const value = selectionMetadata[prop];
|
|
387
|
+
if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
388
|
+
return {
|
|
389
|
+
__fieldName: prop,
|
|
390
|
+
__dbColumnName: prop,
|
|
391
|
+
__tableAlias: alias,
|
|
392
|
+
getMapper: () => value.getMapper(),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
__fieldName: prop,
|
|
398
|
+
__dbColumnName: prop,
|
|
399
|
+
__tableAlias: alias,
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
has() { return true; },
|
|
403
|
+
ownKeys() { return []; },
|
|
404
|
+
getOwnPropertyDescriptor() {
|
|
405
|
+
return { enumerable: true, configurable: true };
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Create a mock for a CTE
|
|
411
|
+
*/
|
|
412
|
+
createMockForCte(cte) {
|
|
413
|
+
const alias = cte.name;
|
|
414
|
+
const selectionMetadata = cte.selectionMetadata;
|
|
415
|
+
return new Proxy({}, {
|
|
416
|
+
get(target, prop) {
|
|
417
|
+
if (typeof prop === 'symbol')
|
|
418
|
+
return undefined;
|
|
419
|
+
if (selectionMetadata && prop in selectionMetadata) {
|
|
420
|
+
const value = selectionMetadata[prop];
|
|
421
|
+
if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
422
|
+
return {
|
|
423
|
+
__fieldName: prop,
|
|
424
|
+
__dbColumnName: prop,
|
|
425
|
+
__tableAlias: alias,
|
|
426
|
+
getMapper: () => value.getMapper(),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
__fieldName: prop,
|
|
432
|
+
__dbColumnName: prop,
|
|
433
|
+
__tableAlias: alias,
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
has() { return true; },
|
|
437
|
+
ownKeys() { return []; },
|
|
438
|
+
getOwnPropertyDescriptor() {
|
|
439
|
+
return { enumerable: true, configurable: true };
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
282
443
|
/**
|
|
283
444
|
* Build the SQL query for grouped results
|
|
284
445
|
*/
|
|
@@ -287,12 +448,34 @@ class GroupedSelectQueryBuilder {
|
|
|
287
448
|
const mockRow = this.createMockRow();
|
|
288
449
|
const mockOriginalSelection = this.originalSelector(mockRow);
|
|
289
450
|
const mockGroupingKey = this.groupingKeySelector(mockOriginalSelection);
|
|
290
|
-
|
|
451
|
+
// Create mock grouped item using the SAME grouping key (not a fresh one)
|
|
452
|
+
// This ensures SqlFragment instances are shared between GROUP BY and SELECT
|
|
453
|
+
const mockGroup = {
|
|
454
|
+
key: mockGroupingKey,
|
|
455
|
+
count: () => createAggregateFieldRef('COUNT'),
|
|
456
|
+
sum: (selector) => createAggregateFieldRef('SUM', selector),
|
|
457
|
+
min: (selector) => createAggregateFieldRef('MIN', selector),
|
|
458
|
+
max: (selector) => createAggregateFieldRef('MAX', selector),
|
|
459
|
+
avg: (selector) => createAggregateFieldRef('AVG', selector),
|
|
460
|
+
};
|
|
291
461
|
const mockResult = this.resultSelector(mockGroup);
|
|
292
462
|
// Extract GROUP BY fields from the grouping key
|
|
463
|
+
// We build these first and cache the SQL for SqlFragments so they can be reused in SELECT
|
|
293
464
|
const groupByFields = [];
|
|
465
|
+
const sqlFragmentCache = new Map(); // Cache built SQL for reuse in SELECT
|
|
294
466
|
for (const [key, value] of Object.entries(mockGroupingKey)) {
|
|
295
|
-
if (
|
|
467
|
+
if (value instanceof conditions_1.SqlFragment) {
|
|
468
|
+
// SqlFragment in GROUP BY - build the SQL expression and cache it
|
|
469
|
+
const sqlBuildContext = {
|
|
470
|
+
paramCounter: context.paramCounter,
|
|
471
|
+
params: context.allParams,
|
|
472
|
+
};
|
|
473
|
+
const fragmentSql = value.buildSql(sqlBuildContext);
|
|
474
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
475
|
+
groupByFields.push(fragmentSql);
|
|
476
|
+
sqlFragmentCache.set(value, fragmentSql);
|
|
477
|
+
}
|
|
478
|
+
else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
|
|
296
479
|
const field = value;
|
|
297
480
|
const tableAlias = field.__tableAlias || this.schema.name;
|
|
298
481
|
groupByFields.push(`"${tableAlias}"."${field.__dbColumnName}"`);
|
|
@@ -311,8 +494,9 @@ class GroupedSelectQueryBuilder {
|
|
|
311
494
|
}
|
|
312
495
|
else if (aggField.__aggregateSelector) {
|
|
313
496
|
// SUM, MIN, MAX, AVG with selector
|
|
314
|
-
|
|
315
|
-
|
|
497
|
+
// Note: The selector references fields from the ORIGINAL SELECTION (after first .select()),
|
|
498
|
+
// not the raw table row. So we need to use mockOriginalSelection, not a fresh mock row.
|
|
499
|
+
const field = aggField.__aggregateSelector(mockOriginalSelection);
|
|
316
500
|
if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
317
501
|
const fieldRef = field;
|
|
318
502
|
const tableAlias = fieldRef.__tableAlias || this.schema.name;
|
|
@@ -339,8 +523,8 @@ class GroupedSelectQueryBuilder {
|
|
|
339
523
|
selectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
|
|
340
524
|
}
|
|
341
525
|
else if (agg.__selector) {
|
|
342
|
-
|
|
343
|
-
const field = agg.__selector(
|
|
526
|
+
// Use mockOriginalSelection - the selector references fields from the first .select()
|
|
527
|
+
const field = agg.__selector(mockOriginalSelection);
|
|
344
528
|
if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
345
529
|
const fieldRef = field;
|
|
346
530
|
const tableAlias = fieldRef.__tableAlias || this.schema.name;
|
|
@@ -360,18 +544,45 @@ class GroupedSelectQueryBuilder {
|
|
|
360
544
|
selectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${alias}"`);
|
|
361
545
|
}
|
|
362
546
|
else if (value instanceof conditions_1.SqlFragment) {
|
|
363
|
-
// SQL fragment
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
547
|
+
// SQL fragment - check if we already built this for GROUP BY
|
|
548
|
+
const cachedSql = sqlFragmentCache.get(value);
|
|
549
|
+
if (cachedSql) {
|
|
550
|
+
// Reuse the cached SQL to ensure same parameter numbers
|
|
551
|
+
selectParts.push(`${cachedSql} as "${alias}"`);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
// Build new SQL for this fragment
|
|
555
|
+
const sqlBuildContext = {
|
|
556
|
+
paramCounter: context.paramCounter,
|
|
557
|
+
params: context.allParams,
|
|
558
|
+
};
|
|
559
|
+
const fragmentSql = value.buildSql(sqlBuildContext);
|
|
560
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
561
|
+
selectParts.push(`${fragmentSql} as "${alias}"`);
|
|
562
|
+
}
|
|
371
563
|
}
|
|
372
564
|
}
|
|
565
|
+
// Detect navigation property references in WHERE and add JOINs
|
|
566
|
+
const navigationJoins = [];
|
|
567
|
+
// Detect joins from the original selection (navigation properties used in select)
|
|
568
|
+
this.detectAndAddJoinsFromSelection(mockOriginalSelection, navigationJoins);
|
|
569
|
+
// Detect joins from WHERE condition
|
|
570
|
+
this.detectAndAddJoinsFromCondition(this.whereCond, navigationJoins);
|
|
373
571
|
// Build FROM clause with JOINs
|
|
374
572
|
let fromClause = `FROM "${this.schema.name}"`;
|
|
573
|
+
// Add navigation property JOINs first
|
|
574
|
+
for (const navJoin of navigationJoins) {
|
|
575
|
+
const joinType = navJoin.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
|
|
576
|
+
const targetTableName = navJoin.targetSchema
|
|
577
|
+
? `"${navJoin.targetSchema}"."${navJoin.targetTable}"`
|
|
578
|
+
: `"${navJoin.targetTable}"`;
|
|
579
|
+
// Build join condition: source.foreignKey = target.match
|
|
580
|
+
const joinConditions = navJoin.foreignKeys.map((fk, i) => {
|
|
581
|
+
const targetCol = navJoin.matches[i] || 'id';
|
|
582
|
+
return `"${this.schema.name}"."${fk}" = "${navJoin.alias}"."${targetCol}"`;
|
|
583
|
+
});
|
|
584
|
+
fromClause += `\n${joinType} ${targetTableName} AS "${navJoin.alias}" ON ${joinConditions.join(' AND ')}`;
|
|
585
|
+
}
|
|
375
586
|
// Add manual JOINs
|
|
376
587
|
for (const manualJoin of this.manualJoins) {
|
|
377
588
|
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
@@ -436,9 +647,9 @@ class GroupedSelectQueryBuilder {
|
|
|
436
647
|
*/
|
|
437
648
|
createMockRow() {
|
|
438
649
|
const mock = {};
|
|
439
|
-
// Add columns as FieldRef objects
|
|
440
|
-
|
|
441
|
-
|
|
650
|
+
// Add columns as FieldRef objects - use pre-computed column name map if available
|
|
651
|
+
const columnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(this.schema);
|
|
652
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
442
653
|
Object.defineProperty(mock, colName, {
|
|
443
654
|
get: () => ({
|
|
444
655
|
__fieldName: colName,
|
|
@@ -449,13 +660,38 @@ class GroupedSelectQueryBuilder {
|
|
|
449
660
|
configurable: true,
|
|
450
661
|
});
|
|
451
662
|
}
|
|
663
|
+
// Add navigation properties (collections and single references)
|
|
664
|
+
// Performance: Use pre-computed relation entries and cached schemas
|
|
665
|
+
const relationEntries = (0, query_builder_1.getRelationEntriesForSchema)(this.schema);
|
|
666
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
667
|
+
const targetSchema = (0, query_builder_1.getTargetSchemaForRelation)(this.schema, relName, relConfig);
|
|
668
|
+
if (relConfig.type === 'many') {
|
|
669
|
+
Object.defineProperty(mock, relName, {
|
|
670
|
+
get: () => {
|
|
671
|
+
return new query_builder_1.CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
|
|
672
|
+
},
|
|
673
|
+
enumerable: true,
|
|
674
|
+
configurable: true,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
Object.defineProperty(mock, relName, {
|
|
679
|
+
get: () => {
|
|
680
|
+
const refBuilder = new query_builder_1.ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
681
|
+
return refBuilder.createMockTargetRow();
|
|
682
|
+
},
|
|
683
|
+
enumerable: true,
|
|
684
|
+
configurable: true,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
452
688
|
// Add columns from manually joined tables
|
|
453
689
|
for (const join of this.manualJoins) {
|
|
454
690
|
if (join.isSubquery || !join.schema) {
|
|
455
691
|
continue;
|
|
456
692
|
}
|
|
457
|
-
|
|
458
|
-
|
|
693
|
+
const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
|
|
694
|
+
for (const [colName, dbColumnName] of joinColumnNameMap) {
|
|
459
695
|
if (!mock[join.alias]) {
|
|
460
696
|
mock[join.alias] = {};
|
|
461
697
|
}
|
|
@@ -498,6 +734,98 @@ class GroupedSelectQueryBuilder {
|
|
|
498
734
|
},
|
|
499
735
|
};
|
|
500
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* Detect navigation property references in a WHERE condition and add necessary JOINs
|
|
739
|
+
*/
|
|
740
|
+
detectAndAddJoinsFromCondition(condition, joins) {
|
|
741
|
+
if (!condition) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
// Get all field references from the condition
|
|
745
|
+
const fieldRefs = condition.getFieldRefs();
|
|
746
|
+
for (const fieldRef of fieldRefs) {
|
|
747
|
+
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
748
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
749
|
+
// Check if this references a related table that isn't already joined
|
|
750
|
+
if (tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
751
|
+
// Find the relation config for this navigation
|
|
752
|
+
const relation = this.schema.relations[tableAlias];
|
|
753
|
+
if (relation && relation.type === 'one') {
|
|
754
|
+
// Get target schema from targetTableBuilder if available
|
|
755
|
+
let targetSchema;
|
|
756
|
+
if (relation.targetTableBuilder) {
|
|
757
|
+
const targetTableSchema = relation.targetTableBuilder.build();
|
|
758
|
+
targetSchema = targetTableSchema.schema;
|
|
759
|
+
}
|
|
760
|
+
// Add a JOIN for this reference
|
|
761
|
+
joins.push({
|
|
762
|
+
alias: tableAlias,
|
|
763
|
+
targetTable: relation.targetTable,
|
|
764
|
+
targetSchema,
|
|
765
|
+
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
766
|
+
matches: relation.matches || [],
|
|
767
|
+
isMandatory: relation.isMandatory ?? false,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Detect navigation properties in a selection and add JOINs for them
|
|
776
|
+
*/
|
|
777
|
+
detectAndAddJoinsFromSelection(selection, joins) {
|
|
778
|
+
if (!selection || typeof selection !== 'object') {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
for (const [, value] of Object.entries(selection)) {
|
|
782
|
+
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
783
|
+
// This is a FieldRef with a table alias - check if it's from a related table
|
|
784
|
+
this.addJoinForFieldRef(value, joins);
|
|
785
|
+
}
|
|
786
|
+
else if (value instanceof conditions_1.SqlFragment) {
|
|
787
|
+
// SqlFragment may contain navigation property references - extract them
|
|
788
|
+
const fieldRefs = value.getFieldRefs();
|
|
789
|
+
for (const fieldRef of fieldRefs) {
|
|
790
|
+
this.addJoinForFieldRef(fieldRef, joins);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
794
|
+
// Recursively check nested objects
|
|
795
|
+
this.detectAndAddJoinsFromSelection(value, joins);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Add a JOIN for a FieldRef if it references a related table
|
|
801
|
+
*/
|
|
802
|
+
addJoinForFieldRef(fieldRef, joins) {
|
|
803
|
+
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef) || !('__dbColumnName' in fieldRef)) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
807
|
+
if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
808
|
+
// This references a related table - find the relation and add a JOIN
|
|
809
|
+
const relation = this.schema.relations[tableAlias];
|
|
810
|
+
if (relation && relation.type === 'one') {
|
|
811
|
+
// Get target schema from targetTableBuilder if available
|
|
812
|
+
let targetSchema;
|
|
813
|
+
if (relation.targetTableBuilder) {
|
|
814
|
+
const targetTableSchema = relation.targetTableBuilder.build();
|
|
815
|
+
targetSchema = targetTableSchema.schema;
|
|
816
|
+
}
|
|
817
|
+
// Add a JOIN for this reference
|
|
818
|
+
joins.push({
|
|
819
|
+
alias: tableAlias,
|
|
820
|
+
targetTable: relation.targetTable,
|
|
821
|
+
targetSchema,
|
|
822
|
+
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
823
|
+
matches: relation.matches || [],
|
|
824
|
+
isMandatory: relation.isMandatory ?? false,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
501
829
|
/**
|
|
502
830
|
* Build HAVING condition SQL - handles aggregate field refs specially
|
|
503
831
|
*/
|
|
@@ -585,4 +913,208 @@ class GroupedSelectQueryBuilder {
|
|
|
585
913
|
}
|
|
586
914
|
}
|
|
587
915
|
exports.GroupedSelectQueryBuilder = GroupedSelectQueryBuilder;
|
|
916
|
+
/**
|
|
917
|
+
* Query builder for grouped queries that have been joined
|
|
918
|
+
* This handles the case where a GroupedSelectQueryBuilder is joined with a CTE or subquery
|
|
919
|
+
*/
|
|
920
|
+
class GroupedJoinedQueryBuilder {
|
|
921
|
+
constructor(schema, client, leftSubquery, leftAlias, rightSource, rightAlias, joinType, joinCondition, resultSelector, createLeftMock, createRightMock, executor, cte) {
|
|
922
|
+
this.orderByFields = [];
|
|
923
|
+
this.additionalJoins = [];
|
|
924
|
+
this.schema = schema;
|
|
925
|
+
this.client = client;
|
|
926
|
+
this.leftSubquery = leftSubquery;
|
|
927
|
+
this.leftAlias = leftAlias;
|
|
928
|
+
this.rightSource = rightSource;
|
|
929
|
+
this.rightAlias = rightAlias;
|
|
930
|
+
this.joinType = joinType;
|
|
931
|
+
this.joinCondition = joinCondition;
|
|
932
|
+
this.resultSelector = resultSelector;
|
|
933
|
+
this.createLeftMock = createLeftMock;
|
|
934
|
+
this.createRightMock = createRightMock;
|
|
935
|
+
this.executor = executor;
|
|
936
|
+
this.cte = cte;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Limit results
|
|
940
|
+
*/
|
|
941
|
+
limit(count) {
|
|
942
|
+
this.limitValue = count;
|
|
943
|
+
return this;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Offset results
|
|
947
|
+
*/
|
|
948
|
+
offset(count) {
|
|
949
|
+
this.offsetValue = count;
|
|
950
|
+
return this;
|
|
951
|
+
}
|
|
952
|
+
orderBy(selector) {
|
|
953
|
+
const mockLeft = this.createLeftMock();
|
|
954
|
+
const mockRight = this.createRightMock();
|
|
955
|
+
const mockResult = this.resultSelector(mockLeft, mockRight);
|
|
956
|
+
const result = selector(mockResult);
|
|
957
|
+
(0, query_utils_1.parseOrderBy)(result, this.orderByFields, query_utils_1.getQualifiedFieldName);
|
|
958
|
+
return this;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Execute query and return results
|
|
962
|
+
*/
|
|
963
|
+
async toList() {
|
|
964
|
+
const context = {
|
|
965
|
+
ctes: new Map(),
|
|
966
|
+
cteCounter: 0,
|
|
967
|
+
paramCounter: 1,
|
|
968
|
+
allParams: [],
|
|
969
|
+
};
|
|
970
|
+
const { sql, params } = this.buildQuery(context);
|
|
971
|
+
const result = this.executor
|
|
972
|
+
? await this.executor.query(sql, params)
|
|
973
|
+
: await this.client.query(sql, params);
|
|
974
|
+
return result.rows;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Execute query and return first result or null
|
|
978
|
+
*/
|
|
979
|
+
async first() {
|
|
980
|
+
const results = await this.limit(1).toList();
|
|
981
|
+
return results.length > 0 ? results[0] : null;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Execute query and return first result or throw
|
|
985
|
+
*/
|
|
986
|
+
async firstOrThrow() {
|
|
987
|
+
const result = await this.first();
|
|
988
|
+
if (!result) {
|
|
989
|
+
throw new Error('No results found');
|
|
990
|
+
}
|
|
991
|
+
return result;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Convert to subquery for use in other queries
|
|
995
|
+
*/
|
|
996
|
+
asSubquery(mode = 'table') {
|
|
997
|
+
const sqlBuilder = (outerContext) => {
|
|
998
|
+
const context = {
|
|
999
|
+
ctes: new Map(),
|
|
1000
|
+
cteCounter: 0,
|
|
1001
|
+
paramCounter: outerContext.paramCounter,
|
|
1002
|
+
allParams: outerContext.params,
|
|
1003
|
+
};
|
|
1004
|
+
const { sql } = this.buildQuery(context);
|
|
1005
|
+
outerContext.paramCounter = context.paramCounter;
|
|
1006
|
+
return sql;
|
|
1007
|
+
};
|
|
1008
|
+
// Preserve selection metadata for mappers
|
|
1009
|
+
const mockLeft = this.createLeftMock();
|
|
1010
|
+
const mockRight = this.createRightMock();
|
|
1011
|
+
const selectionMetadata = this.resultSelector(mockLeft, mockRight);
|
|
1012
|
+
return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata);
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Get CTEs used by this query builder
|
|
1016
|
+
* @internal
|
|
1017
|
+
*/
|
|
1018
|
+
getReferencedCtes() {
|
|
1019
|
+
return this.cte ? [this.cte] : [];
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Build SQL for use in CTEs - public interface for CTE builder
|
|
1023
|
+
* This returns SQL WITHOUT the WITH clause - CTEs should be extracted separately via getReferencedCtes()
|
|
1024
|
+
* @internal
|
|
1025
|
+
*/
|
|
1026
|
+
buildCteQuery(queryContext) {
|
|
1027
|
+
return this.buildQuery(queryContext, true);
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Build the SQL query
|
|
1031
|
+
* @param skipCteClause If true, don't include WITH clause (for embedding in outer CTEs)
|
|
1032
|
+
*/
|
|
1033
|
+
buildQuery(context, skipCteClause = false) {
|
|
1034
|
+
// Build CTE clause if needed (unless we're being embedded in another CTE)
|
|
1035
|
+
let cteClause = '';
|
|
1036
|
+
if (this.cte && !skipCteClause) {
|
|
1037
|
+
cteClause = `WITH "${this.cte.name}" AS (${this.cte.query})\n`;
|
|
1038
|
+
context.allParams.push(...this.cte.params);
|
|
1039
|
+
context.paramCounter += this.cte.params.length;
|
|
1040
|
+
}
|
|
1041
|
+
// Build SELECT clause from result selector
|
|
1042
|
+
const mockLeft = this.createLeftMock();
|
|
1043
|
+
const mockRight = this.createRightMock();
|
|
1044
|
+
const mockResult = this.resultSelector(mockLeft, mockRight);
|
|
1045
|
+
const selectParts = [];
|
|
1046
|
+
for (const [alias, value] of Object.entries(mockResult)) {
|
|
1047
|
+
if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
|
|
1048
|
+
const field = value;
|
|
1049
|
+
const tableAlias = field.__tableAlias;
|
|
1050
|
+
if (tableAlias) {
|
|
1051
|
+
selectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${alias}"`);
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
selectParts.push(`"${field.__dbColumnName}" as "${alias}"`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
else if (value instanceof conditions_1.SqlFragment) {
|
|
1058
|
+
const sqlBuildContext = {
|
|
1059
|
+
paramCounter: context.paramCounter,
|
|
1060
|
+
params: context.allParams,
|
|
1061
|
+
};
|
|
1062
|
+
const fragmentSql = value.buildSql(sqlBuildContext);
|
|
1063
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
1064
|
+
selectParts.push(`${fragmentSql} as "${alias}"`);
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
selectParts.push(`"${alias}"`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// Build FROM clause with the left subquery
|
|
1071
|
+
const leftSqlContext = {
|
|
1072
|
+
paramCounter: context.paramCounter,
|
|
1073
|
+
params: context.allParams,
|
|
1074
|
+
};
|
|
1075
|
+
const leftSql = this.leftSubquery.buildSql(leftSqlContext);
|
|
1076
|
+
context.paramCounter = leftSqlContext.paramCounter;
|
|
1077
|
+
let fromClause = `FROM (${leftSql}) AS "${this.leftAlias}"`;
|
|
1078
|
+
// Build JOIN clause
|
|
1079
|
+
const joinTypeStr = this.joinType === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1080
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1081
|
+
const { sql: condSql, params: condParams } = condBuilder.build(this.joinCondition, context.paramCounter);
|
|
1082
|
+
context.paramCounter += condParams.length;
|
|
1083
|
+
context.allParams.push(...condParams);
|
|
1084
|
+
if (this.cte) {
|
|
1085
|
+
// Join to CTE
|
|
1086
|
+
fromClause += `\n${joinTypeStr} "${this.rightAlias}" ON ${condSql}`;
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
// Join to subquery
|
|
1090
|
+
const rightSqlContext = {
|
|
1091
|
+
paramCounter: context.paramCounter,
|
|
1092
|
+
params: context.allParams,
|
|
1093
|
+
};
|
|
1094
|
+
const rightSql = this.rightSource.buildSql(rightSqlContext);
|
|
1095
|
+
context.paramCounter = rightSqlContext.paramCounter;
|
|
1096
|
+
fromClause += `\n${joinTypeStr} (${rightSql}) AS "${this.rightAlias}" ON ${condSql}`;
|
|
1097
|
+
}
|
|
1098
|
+
// Build ORDER BY clause
|
|
1099
|
+
let orderByClause = '';
|
|
1100
|
+
if (this.orderByFields.length > 0) {
|
|
1101
|
+
const orderParts = this.orderByFields.map(({ field, direction }) => `${field} ${direction}`);
|
|
1102
|
+
orderByClause = `ORDER BY ${orderParts.join(', ')}`;
|
|
1103
|
+
}
|
|
1104
|
+
// Build LIMIT/OFFSET
|
|
1105
|
+
let limitClause = '';
|
|
1106
|
+
if (this.limitValue !== undefined) {
|
|
1107
|
+
limitClause = `LIMIT ${this.limitValue}`;
|
|
1108
|
+
}
|
|
1109
|
+
if (this.offsetValue !== undefined) {
|
|
1110
|
+
limitClause += ` OFFSET ${this.offsetValue}`;
|
|
1111
|
+
}
|
|
1112
|
+
const finalQuery = `${cteClause}SELECT ${selectParts.join(', ')}\n${fromClause}\n${orderByClause}\n${limitClause}`.trim();
|
|
1113
|
+
return {
|
|
1114
|
+
sql: finalQuery,
|
|
1115
|
+
params: context.allParams,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
exports.GroupedJoinedQueryBuilder = GroupedJoinedQueryBuilder;
|
|
588
1120
|
//# sourceMappingURL=grouped-query.js.map
|