linkgress-orm 0.0.2 → 0.1.0
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-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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/migration/db-schema-manager.js +77 -77
- package/dist/migration/enum-migrator.js +6 -6
- 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 +178 -24
- package/dist/query/conditions.d.ts.map +1 -1
- package/dist/query/conditions.js +165 -4
- package/dist/query/conditions.js.map +1 -1
- package/dist/query/cte-builder.d.ts +21 -5
- package/dist/query/cte-builder.d.ts.map +1 -1
- package/dist/query/cte-builder.js +31 -7
- package/dist/query/cte-builder.js.map +1 -1
- package/dist/query/grouped-query.d.ts +185 -8
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +516 -30
- 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 +11 -33
- package/dist/query/join-builder.js.map +1 -1
- package/dist/query/query-builder.d.ts +89 -20
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +317 -168
- 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 +216 -94
- 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/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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQueryBuilder = exports.QueryBuilder = void 0;
|
|
4
4
|
const conditions_1 = require("./conditions");
|
|
5
|
+
const query_utils_1 = require("./query-utils");
|
|
5
6
|
const subquery_1 = require("./subquery");
|
|
6
7
|
const grouped_query_1 = require("./grouped-query");
|
|
7
8
|
const cte_builder_1 = require("./cte-builder");
|
|
@@ -38,6 +39,7 @@ class QueryBuilder {
|
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
41
|
* Define the selection with support for nested queries
|
|
42
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
41
43
|
*/
|
|
42
44
|
select(selector) {
|
|
43
45
|
return new SelectQueryBuilder(this.schema, this.client, selector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, // isDistinct defaults to false
|
|
@@ -47,12 +49,25 @@ class QueryBuilder {
|
|
|
47
49
|
}
|
|
48
50
|
/**
|
|
49
51
|
* Add WHERE condition
|
|
52
|
+
* Multiple where() calls are chained with AND logic
|
|
50
53
|
*/
|
|
51
54
|
where(condition) {
|
|
52
55
|
const mockRow = this.createMockRow();
|
|
53
|
-
|
|
56
|
+
const newCondition = condition(mockRow);
|
|
57
|
+
if (this.whereCond) {
|
|
58
|
+
this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.whereCond = newCondition;
|
|
62
|
+
}
|
|
54
63
|
return this;
|
|
55
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Add CTEs (Common Table Expressions) to the query
|
|
67
|
+
*/
|
|
68
|
+
with(...ctes) {
|
|
69
|
+
return new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, undefined, ctes, this.collectionStrategy);
|
|
70
|
+
}
|
|
56
71
|
/**
|
|
57
72
|
* Create mock row for analysis
|
|
58
73
|
*/
|
|
@@ -118,6 +133,7 @@ class QueryBuilder {
|
|
|
118
133
|
}
|
|
119
134
|
/**
|
|
120
135
|
* Add a LEFT JOIN to the query with a selector (supports both tables and subqueries)
|
|
136
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
121
137
|
*/
|
|
122
138
|
leftJoin(rightTable, condition, selector, alias) {
|
|
123
139
|
// Check if rightTable is a Subquery
|
|
@@ -166,6 +182,7 @@ class QueryBuilder {
|
|
|
166
182
|
}
|
|
167
183
|
/**
|
|
168
184
|
* Add an INNER JOIN to the query with a selector (supports both tables and subqueries)
|
|
185
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
169
186
|
*/
|
|
170
187
|
innerJoin(rightTable, condition, selector, alias) {
|
|
171
188
|
// Check if rightTable is a Subquery
|
|
@@ -287,35 +304,7 @@ class QueryBuilder {
|
|
|
287
304
|
orderBy(selector) {
|
|
288
305
|
const mockRow = this.createMockRow();
|
|
289
306
|
const result = selector(mockRow);
|
|
290
|
-
|
|
291
|
-
if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
|
|
292
|
-
for (const [fieldRef, direction] of result) {
|
|
293
|
-
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
294
|
-
this.orderByFields.push({
|
|
295
|
-
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
296
|
-
direction: direction || 'ASC'
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
// Handle array of fields (all ASC)
|
|
302
|
-
else if (Array.isArray(result)) {
|
|
303
|
-
for (const fieldRef of result) {
|
|
304
|
-
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
305
|
-
this.orderByFields.push({
|
|
306
|
-
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
307
|
-
direction: 'ASC'
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// Handle single field
|
|
313
|
-
else if (result && typeof result === 'object' && '__fieldName' in result) {
|
|
314
|
-
this.orderByFields.push({
|
|
315
|
-
field: result.__dbColumnName || result.__fieldName,
|
|
316
|
-
direction: 'ASC'
|
|
317
|
-
});
|
|
318
|
-
}
|
|
307
|
+
(0, query_utils_1.parseOrderBy)(result, this.orderByFields);
|
|
319
308
|
return this;
|
|
320
309
|
}
|
|
321
310
|
}
|
|
@@ -353,6 +342,7 @@ class SelectQueryBuilder {
|
|
|
353
342
|
}
|
|
354
343
|
/**
|
|
355
344
|
* Transform the selection with a new selector
|
|
345
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
356
346
|
*/
|
|
357
347
|
select(selector) {
|
|
358
348
|
// Create a composed selector that applies both transformations
|
|
@@ -364,13 +354,22 @@ class SelectQueryBuilder {
|
|
|
364
354
|
}
|
|
365
355
|
/**
|
|
366
356
|
* Add WHERE condition
|
|
357
|
+
* Multiple where() calls are chained with AND logic
|
|
367
358
|
* Note: The row parameter represents the selected shape (after select())
|
|
368
359
|
*/
|
|
369
360
|
where(condition) {
|
|
370
361
|
const mockRow = this.createMockRow();
|
|
371
362
|
// Apply the selector to get the selected shape that the user sees in the WHERE condition
|
|
372
363
|
const selectedMock = this.selector(mockRow);
|
|
373
|
-
|
|
364
|
+
// Wrap in proxy - for WHERE, we preserve original column names
|
|
365
|
+
const fieldRefProxy = this.createFieldRefProxy(selectedMock, true);
|
|
366
|
+
const newCondition = condition(fieldRefProxy);
|
|
367
|
+
if (this.whereCond) {
|
|
368
|
+
this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
this.whereCond = newCondition;
|
|
372
|
+
}
|
|
374
373
|
return this;
|
|
375
374
|
}
|
|
376
375
|
/**
|
|
@@ -404,36 +403,12 @@ class SelectQueryBuilder {
|
|
|
404
403
|
orderBy(selector) {
|
|
405
404
|
const mockRow = this.createMockRow();
|
|
406
405
|
const selectedMock = this.selector(mockRow);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
414
|
-
direction: direction || 'ASC'
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
// Handle array of fields (all ASC)
|
|
420
|
-
else if (Array.isArray(result)) {
|
|
421
|
-
for (const fieldRef of result) {
|
|
422
|
-
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
423
|
-
this.orderByFields.push({
|
|
424
|
-
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
425
|
-
direction: 'ASC'
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
// Handle single field
|
|
431
|
-
else if (result && typeof result === 'object' && '__fieldName' in result) {
|
|
432
|
-
this.orderByFields.push({
|
|
433
|
-
field: result.__dbColumnName || result.__fieldName,
|
|
434
|
-
direction: 'ASC'
|
|
435
|
-
});
|
|
436
|
-
}
|
|
406
|
+
// Wrap selectedMock in a proxy that returns FieldRefs for property access
|
|
407
|
+
const fieldRefProxy = this.createFieldRefProxy(selectedMock);
|
|
408
|
+
const result = selector(fieldRefProxy);
|
|
409
|
+
// Clear previous orderBy - last one takes precedence
|
|
410
|
+
this.orderByFields = [];
|
|
411
|
+
(0, query_utils_1.parseOrderBy)(result, this.orderByFields);
|
|
437
412
|
return this;
|
|
438
413
|
}
|
|
439
414
|
/**
|
|
@@ -483,10 +458,7 @@ class SelectQueryBuilder {
|
|
|
483
458
|
};
|
|
484
459
|
return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy);
|
|
485
460
|
}
|
|
486
|
-
|
|
487
|
-
* Add a LEFT JOIN to the query with a selector
|
|
488
|
-
* Note: After select(), the left parameter in the join will be the selected shape (TSelection)
|
|
489
|
-
*/
|
|
461
|
+
// Implementation
|
|
490
462
|
leftJoin(rightTable, condition, selector, alias) {
|
|
491
463
|
// Check if rightTable is a CTE
|
|
492
464
|
if ((0, cte_builder_1.isCte)(rightTable)) {
|
|
@@ -596,6 +568,7 @@ class SelectQueryBuilder {
|
|
|
596
568
|
/**
|
|
597
569
|
* Add an INNER JOIN to the query with a selector
|
|
598
570
|
* Note: After select(), the left parameter in the join will be the selected shape (TSelection)
|
|
571
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
599
572
|
*/
|
|
600
573
|
innerJoin(rightTable, condition, selector, alias) {
|
|
601
574
|
// Check if rightTable is a Subquery
|
|
@@ -1304,11 +1277,18 @@ class SelectQueryBuilder {
|
|
|
1304
1277
|
}
|
|
1305
1278
|
// Add relations as CollectionQueryBuilder or ReferenceQueryBuilder
|
|
1306
1279
|
for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
|
|
1280
|
+
// Try to get target schema from registry (preferred, has full relations) or targetTableBuilder
|
|
1281
|
+
let targetSchema;
|
|
1282
|
+
if (this.schemaRegistry) {
|
|
1283
|
+
targetSchema = this.schemaRegistry.get(relConfig.targetTable);
|
|
1284
|
+
}
|
|
1285
|
+
if (!targetSchema && relConfig.targetTableBuilder) {
|
|
1286
|
+
targetSchema = relConfig.targetTableBuilder.build();
|
|
1287
|
+
}
|
|
1307
1288
|
if (relConfig.type === 'many') {
|
|
1308
1289
|
Object.defineProperty(mock, relName, {
|
|
1309
1290
|
get: () => {
|
|
1310
|
-
|
|
1311
|
-
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, undefined, // Don't pass schema, force registry lookup
|
|
1291
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, // Pass the target schema directly
|
|
1312
1292
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
1313
1293
|
);
|
|
1314
1294
|
},
|
|
@@ -1320,8 +1300,7 @@ class SelectQueryBuilder {
|
|
|
1320
1300
|
// For single reference (many-to-one), create a ReferenceQueryBuilder
|
|
1321
1301
|
Object.defineProperty(mock, relName, {
|
|
1322
1302
|
get: () => {
|
|
1323
|
-
|
|
1324
|
-
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
|
|
1303
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
|
|
1325
1304
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
1326
1305
|
);
|
|
1327
1306
|
// Return a mock object that exposes the target table's columns
|
|
@@ -1334,6 +1313,67 @@ class SelectQueryBuilder {
|
|
|
1334
1313
|
}
|
|
1335
1314
|
return mock;
|
|
1336
1315
|
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Create a proxy that wraps selected values and returns FieldRefs for property access
|
|
1318
|
+
* This enables orderBy and other operations to work with chained selects
|
|
1319
|
+
* @param preserveOriginal - If true (for WHERE), preserve original column names; if false (for ORDER BY), use alias names
|
|
1320
|
+
*/
|
|
1321
|
+
createFieldRefProxy(selectedMock, preserveOriginal = false) {
|
|
1322
|
+
if (!selectedMock || typeof selectedMock !== 'object') {
|
|
1323
|
+
return selectedMock;
|
|
1324
|
+
}
|
|
1325
|
+
// If it already has FieldRef properties, return as-is
|
|
1326
|
+
if ('__fieldName' in selectedMock && '__dbColumnName' in selectedMock) {
|
|
1327
|
+
return selectedMock;
|
|
1328
|
+
}
|
|
1329
|
+
// Create a proxy that returns FieldRefs for each property access
|
|
1330
|
+
return new Proxy(selectedMock, {
|
|
1331
|
+
get: (target, prop) => {
|
|
1332
|
+
if (typeof prop === 'symbol' || prop === 'constructor' || prop === 'then') {
|
|
1333
|
+
return target[prop];
|
|
1334
|
+
}
|
|
1335
|
+
const value = target[prop];
|
|
1336
|
+
// If the value is already a FieldRef
|
|
1337
|
+
if (value && typeof value === 'object' && '__fieldName' in value && '__dbColumnName' in value) {
|
|
1338
|
+
if (preserveOriginal) {
|
|
1339
|
+
// For WHERE: preserve original column name and table alias
|
|
1340
|
+
// This ensures WHERE references the actual database column
|
|
1341
|
+
return {
|
|
1342
|
+
__fieldName: prop,
|
|
1343
|
+
__dbColumnName: value.__dbColumnName,
|
|
1344
|
+
__tableAlias: value.__tableAlias,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
// For ORDER BY: use the alias (property name) as the column name
|
|
1349
|
+
// In chained selects, the alias becomes the column name in the subquery
|
|
1350
|
+
return {
|
|
1351
|
+
__fieldName: prop,
|
|
1352
|
+
__dbColumnName: prop,
|
|
1353
|
+
// No table alias - column comes from the selection/subquery
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
// If the value is a SqlFragment, treat it as a FieldRef using the property name as the alias
|
|
1358
|
+
if (value && typeof value === 'object' && value instanceof conditions_1.SqlFragment) {
|
|
1359
|
+
return {
|
|
1360
|
+
__fieldName: prop,
|
|
1361
|
+
__dbColumnName: prop,
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
// If the value is an object (nested selection), recursively wrap it
|
|
1365
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1366
|
+
return this.createFieldRefProxy(value, preserveOriginal);
|
|
1367
|
+
}
|
|
1368
|
+
// For primitive values or arrays, create a FieldRef
|
|
1369
|
+
// Use the property name as both fieldName and dbColumnName
|
|
1370
|
+
return {
|
|
1371
|
+
__fieldName: prop,
|
|
1372
|
+
__dbColumnName: prop,
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1337
1377
|
/**
|
|
1338
1378
|
* Detect navigation property references in selection and add necessary JOINs
|
|
1339
1379
|
*/
|
|
@@ -1373,6 +1413,43 @@ class SelectQueryBuilder {
|
|
|
1373
1413
|
}
|
|
1374
1414
|
}
|
|
1375
1415
|
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Detect navigation property references in a WHERE condition and add necessary JOINs
|
|
1418
|
+
*/
|
|
1419
|
+
detectAndAddJoinsFromCondition(condition, joins) {
|
|
1420
|
+
if (!condition) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
// Get all field references from the condition
|
|
1424
|
+
const fieldRefs = condition.getFieldRefs();
|
|
1425
|
+
for (const fieldRef of fieldRefs) {
|
|
1426
|
+
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
1427
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
1428
|
+
// Check if this references a related table that isn't already joined
|
|
1429
|
+
if (tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
1430
|
+
// Find the relation config for this navigation
|
|
1431
|
+
const relation = this.schema.relations[tableAlias];
|
|
1432
|
+
if (relation && relation.type === 'one') {
|
|
1433
|
+
// Get target schema from targetTableBuilder if available
|
|
1434
|
+
let targetSchema;
|
|
1435
|
+
if (relation.targetTableBuilder) {
|
|
1436
|
+
const targetTableSchema = relation.targetTableBuilder.build();
|
|
1437
|
+
targetSchema = targetTableSchema.schema;
|
|
1438
|
+
}
|
|
1439
|
+
// Add a JOIN for this reference
|
|
1440
|
+
joins.push({
|
|
1441
|
+
alias: tableAlias,
|
|
1442
|
+
targetTable: relation.targetTable,
|
|
1443
|
+
targetSchema,
|
|
1444
|
+
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
1445
|
+
matches: relation.matches || [],
|
|
1446
|
+
isMandatory: relation.isMandatory ?? false,
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1376
1453
|
/**
|
|
1377
1454
|
* Build SQL query
|
|
1378
1455
|
*/
|
|
@@ -1387,6 +1464,8 @@ class SelectQueryBuilder {
|
|
|
1387
1464
|
const joins = [];
|
|
1388
1465
|
// Scan selection for navigation property references and add JOINs
|
|
1389
1466
|
this.detectAndAddJoinsFromSelection(selection, joins);
|
|
1467
|
+
// Scan WHERE condition for navigation property references and add JOINs
|
|
1468
|
+
this.detectAndAddJoinsFromCondition(this.whereCond, joins);
|
|
1390
1469
|
// Handle case where selection is a single value (not an object with properties)
|
|
1391
1470
|
if (selection instanceof conditions_1.SqlFragment) {
|
|
1392
1471
|
// Single SQL fragment - just build it directly
|
|
@@ -1411,11 +1490,19 @@ class SelectQueryBuilder {
|
|
|
1411
1490
|
// Process selection object properties
|
|
1412
1491
|
for (const [key, value] of Object.entries(selection)) {
|
|
1413
1492
|
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
1414
|
-
// Handle collection -
|
|
1415
|
-
|
|
1493
|
+
// Handle collection - delegate to strategy pattern via buildCTE
|
|
1494
|
+
// The strategy handles CTE/LATERAL specifics and returns necessary info
|
|
1416
1495
|
const cteData = value.buildCTE ? value.buildCTE(context) : value.buildCTE(context);
|
|
1417
|
-
|
|
1418
|
-
|
|
1496
|
+
const isCTE = cteData.isCTE !== false; // Default to CTE if not specified
|
|
1497
|
+
// Note: For CTE strategy, context.ctes is already populated by the strategy
|
|
1498
|
+
// We don't need to add it again - the strategy has already done this
|
|
1499
|
+
collectionFields.push({
|
|
1500
|
+
name: key,
|
|
1501
|
+
cteName: cteData.tableName || `cte_${context.cteCounter - 1}`, // Use tableName from result or infer from counter
|
|
1502
|
+
isCTE,
|
|
1503
|
+
joinClause: cteData.joinClause,
|
|
1504
|
+
selectExpression: cteData.selectExpression,
|
|
1505
|
+
});
|
|
1419
1506
|
}
|
|
1420
1507
|
else if (value instanceof subquery_1.Subquery || (value && typeof value === 'object' && 'buildSql' in value && typeof value.buildSql === 'function' && '__mode' in value)) {
|
|
1421
1508
|
// Handle Subquery - build SQL and wrap in parentheses
|
|
@@ -1601,8 +1688,14 @@ class SelectQueryBuilder {
|
|
|
1601
1688
|
}
|
|
1602
1689
|
} // End of for loop
|
|
1603
1690
|
} // End of else block
|
|
1604
|
-
// Add collection fields as JSON/array aggregations joined from CTEs
|
|
1605
|
-
for (const { name, cteName } of collectionFields) {
|
|
1691
|
+
// Add collection fields as JSON/array aggregations joined from CTEs or LATERAL joins
|
|
1692
|
+
for (const { name, cteName, selectExpression } of collectionFields) {
|
|
1693
|
+
// If selectExpression is provided (from strategy), use it directly
|
|
1694
|
+
if (selectExpression) {
|
|
1695
|
+
selectParts.push(`${selectExpression} as "${name}"`);
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
// Fallback to old logic for backward compatibility
|
|
1606
1699
|
// Check if this is an array aggregation (from toNumberList/toStringList)
|
|
1607
1700
|
const collectionValue = selection[name];
|
|
1608
1701
|
const isArrayAgg = collectionValue && typeof collectionValue === 'object' && 'isArrayAggregation' in collectionValue && collectionValue.isArrayAggregation();
|
|
@@ -1728,9 +1821,16 @@ class SelectQueryBuilder {
|
|
|
1728
1821
|
const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
1729
1822
|
fromClause += `\n${joinType} ${joinTableName} AS "${join.alias}" ON ${onConditions.join(' AND ')}`;
|
|
1730
1823
|
}
|
|
1731
|
-
// Join CTEs for collections
|
|
1732
|
-
for (const { cteName } of collectionFields) {
|
|
1733
|
-
|
|
1824
|
+
// Join CTEs and LATERAL subqueries for collections
|
|
1825
|
+
for (const { cteName, isCTE, joinClause } of collectionFields) {
|
|
1826
|
+
if (isCTE) {
|
|
1827
|
+
// CTE strategy - join by parent_id
|
|
1828
|
+
fromClause += `\nLEFT JOIN "${cteName}" ON "${cteName}".parent_id = ${qualifiedTableName}.id`;
|
|
1829
|
+
}
|
|
1830
|
+
else if (joinClause) {
|
|
1831
|
+
// LATERAL strategy - use the provided join clause (contains full LATERAL subquery)
|
|
1832
|
+
fromClause += `\n${joinClause}`;
|
|
1833
|
+
}
|
|
1734
1834
|
}
|
|
1735
1835
|
// Add DISTINCT if needed
|
|
1736
1836
|
const distinctClause = this.isDistinct ? 'DISTINCT ' : '';
|
|
@@ -2106,7 +2206,35 @@ class SelectQueryBuilder {
|
|
|
2106
2206
|
const mockRow = this.createMockRow();
|
|
2107
2207
|
selectionMetadata = this.selector(mockRow);
|
|
2108
2208
|
}
|
|
2109
|
-
|
|
2209
|
+
// Extract outer field refs from the WHERE condition
|
|
2210
|
+
// These are field refs that reference tables other than this subquery's table
|
|
2211
|
+
// and need to be propagated to the outer query for JOIN detection
|
|
2212
|
+
const outerFieldRefs = this.extractOuterFieldRefs();
|
|
2213
|
+
return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata, outerFieldRefs);
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Extract field refs from the WHERE condition that reference outer queries.
|
|
2217
|
+
* These are field refs with a __tableAlias that doesn't match this query's schema.
|
|
2218
|
+
*/
|
|
2219
|
+
extractOuterFieldRefs() {
|
|
2220
|
+
if (!this.whereCond) {
|
|
2221
|
+
return [];
|
|
2222
|
+
}
|
|
2223
|
+
const allRefs = this.whereCond.getFieldRefs();
|
|
2224
|
+
const outerRefs = [];
|
|
2225
|
+
const currentTableName = this.schema.name;
|
|
2226
|
+
for (const ref of allRefs) {
|
|
2227
|
+
// Check if this ref is from an outer query (different table alias)
|
|
2228
|
+
if ('__tableAlias' in ref && ref.__tableAlias) {
|
|
2229
|
+
const tableAlias = ref.__tableAlias;
|
|
2230
|
+
// If the table alias doesn't match our current schema, it's from an outer query
|
|
2231
|
+
// Also check if it's not a navigation property of this table (which would be in schema.relations)
|
|
2232
|
+
if (tableAlias !== currentTableName && !this.schema.relations[tableAlias]) {
|
|
2233
|
+
outerRefs.push(ref);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return outerRefs;
|
|
2110
2238
|
}
|
|
2111
2239
|
}
|
|
2112
2240
|
exports.SelectQueryBuilder = SelectQueryBuilder;
|
|
@@ -2120,12 +2248,15 @@ class ReferenceQueryBuilder {
|
|
|
2120
2248
|
this.foreignKeys = foreignKeys;
|
|
2121
2249
|
this.matches = matches;
|
|
2122
2250
|
this.isMandatory = isMandatory;
|
|
2123
|
-
this.targetTableSchema = targetTableSchema;
|
|
2124
2251
|
this.schemaRegistry = schemaRegistry;
|
|
2125
|
-
//
|
|
2126
|
-
if (
|
|
2252
|
+
// Prefer registry lookup (has full relations) over passed schema
|
|
2253
|
+
if (this.schemaRegistry) {
|
|
2127
2254
|
this.targetTableSchema = this.schemaRegistry.get(targetTable);
|
|
2128
2255
|
}
|
|
2256
|
+
// Fallback to passed schema if registry lookup failed
|
|
2257
|
+
if (!this.targetTableSchema) {
|
|
2258
|
+
this.targetTableSchema = targetTableSchema;
|
|
2259
|
+
}
|
|
2129
2260
|
}
|
|
2130
2261
|
/**
|
|
2131
2262
|
* Get the alias to use for this reference in the query
|
|
@@ -2186,14 +2317,20 @@ class ReferenceQueryBuilder {
|
|
|
2186
2317
|
// Add navigation properties (both collections and references)
|
|
2187
2318
|
if (this.targetTableSchema.relations) {
|
|
2188
2319
|
for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
|
|
2320
|
+
// Try to get target schema from registry (preferred, has full relations) or targetTableBuilder
|
|
2321
|
+
let nestedTargetSchema;
|
|
2322
|
+
if (this.schemaRegistry) {
|
|
2323
|
+
nestedTargetSchema = this.schemaRegistry.get(relConfig.targetTable);
|
|
2324
|
+
}
|
|
2325
|
+
if (!nestedTargetSchema && relConfig.targetTableBuilder) {
|
|
2326
|
+
nestedTargetSchema = relConfig.targetTableBuilder.build();
|
|
2327
|
+
}
|
|
2189
2328
|
if (relConfig.type === 'many') {
|
|
2190
2329
|
// Collection navigation
|
|
2191
2330
|
Object.defineProperty(mock, relName, {
|
|
2192
2331
|
get: () => {
|
|
2193
|
-
// Don't call build() - it returns schema without relations
|
|
2194
|
-
// Instead, pass undefined and let CollectionQueryBuilder look it up from registry
|
|
2195
2332
|
const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
|
|
2196
|
-
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable,
|
|
2333
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, nestedTargetSchema, // Pass the target schema directly
|
|
2197
2334
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
2198
2335
|
);
|
|
2199
2336
|
},
|
|
@@ -2205,9 +2342,7 @@ class ReferenceQueryBuilder {
|
|
|
2205
2342
|
// Reference navigation
|
|
2206
2343
|
Object.defineProperty(mock, relName, {
|
|
2207
2344
|
get: () => {
|
|
2208
|
-
|
|
2209
|
-
// Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
|
|
2210
|
-
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
|
|
2345
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
|
|
2211
2346
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
2212
2347
|
);
|
|
2213
2348
|
return refBuilder.createMockTargetRow();
|
|
@@ -2248,9 +2383,16 @@ class CollectionQueryBuilder {
|
|
|
2248
2383
|
this.foreignKey = foreignKey;
|
|
2249
2384
|
this.sourceTable = sourceTable;
|
|
2250
2385
|
this.schemaRegistry = schemaRegistry;
|
|
2251
|
-
//
|
|
2252
|
-
if (
|
|
2253
|
-
|
|
2386
|
+
// Prefer registry lookup (has full relations) over passed schema
|
|
2387
|
+
if (this.schemaRegistry) {
|
|
2388
|
+
const registrySchema = this.schemaRegistry.get(targetTable);
|
|
2389
|
+
if (registrySchema) {
|
|
2390
|
+
this.targetTableSchema = registrySchema;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
// Fallback to passed schema if registry lookup failed
|
|
2394
|
+
if (!this.targetTableSchema) {
|
|
2395
|
+
this.targetTableSchema = targetTableSchema;
|
|
2254
2396
|
}
|
|
2255
2397
|
}
|
|
2256
2398
|
/**
|
|
@@ -2277,11 +2419,18 @@ class CollectionQueryBuilder {
|
|
|
2277
2419
|
}
|
|
2278
2420
|
/**
|
|
2279
2421
|
* Filter collection items
|
|
2422
|
+
* Multiple where() calls are chained with AND logic
|
|
2280
2423
|
*/
|
|
2281
2424
|
where(condition) {
|
|
2282
2425
|
// Create mock item with proper schema if available
|
|
2283
2426
|
const mockItem = this.createMockItem();
|
|
2284
|
-
|
|
2427
|
+
const newCondition = condition(mockItem);
|
|
2428
|
+
if (this.whereCond) {
|
|
2429
|
+
this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
|
|
2430
|
+
}
|
|
2431
|
+
else {
|
|
2432
|
+
this.whereCond = newCondition;
|
|
2433
|
+
}
|
|
2285
2434
|
return this;
|
|
2286
2435
|
}
|
|
2287
2436
|
/**
|
|
@@ -2378,35 +2527,7 @@ class CollectionQueryBuilder {
|
|
|
2378
2527
|
orderBy(selector) {
|
|
2379
2528
|
const mockItem = this.createMockItem();
|
|
2380
2529
|
const result = selector(mockItem);
|
|
2381
|
-
|
|
2382
|
-
if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
|
|
2383
|
-
for (const [fieldRef, direction] of result) {
|
|
2384
|
-
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
2385
|
-
this.orderByFields.push({
|
|
2386
|
-
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
2387
|
-
direction: direction || 'ASC'
|
|
2388
|
-
});
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
// Handle array of fields (all ASC)
|
|
2393
|
-
else if (Array.isArray(result)) {
|
|
2394
|
-
for (const fieldRef of result) {
|
|
2395
|
-
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
2396
|
-
this.orderByFields.push({
|
|
2397
|
-
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
2398
|
-
direction: 'ASC'
|
|
2399
|
-
});
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
// Handle single field
|
|
2404
|
-
else if (result && typeof result === 'object' && '__fieldName' in result) {
|
|
2405
|
-
this.orderByFields.push({
|
|
2406
|
-
field: result.__dbColumnName || result.__fieldName,
|
|
2407
|
-
direction: 'ASC'
|
|
2408
|
-
});
|
|
2409
|
-
}
|
|
2530
|
+
(0, query_utils_1.parseOrderBy)(result, this.orderByFields);
|
|
2410
2531
|
return this;
|
|
2411
2532
|
}
|
|
2412
2533
|
/**
|
|
@@ -2524,14 +2645,61 @@ class CollectionQueryBuilder {
|
|
|
2524
2645
|
/**
|
|
2525
2646
|
* Build CTE for this collection query
|
|
2526
2647
|
* Now delegates to collection strategy pattern
|
|
2648
|
+
* Returns full CollectionAggregationResult for strategies that need special handling (like LATERAL)
|
|
2527
2649
|
*/
|
|
2528
2650
|
buildCTE(context, client, parentIds) {
|
|
2529
|
-
// Determine strategy type - default to '
|
|
2530
|
-
const strategyType = context.collectionStrategy || '
|
|
2651
|
+
// Determine strategy type - default to 'lateral' if not specified
|
|
2652
|
+
const strategyType = context.collectionStrategy || 'lateral';
|
|
2531
2653
|
const strategy = collection_strategy_factory_1.CollectionStrategyFactory.getStrategy(strategyType);
|
|
2532
|
-
// Build selected fields configuration
|
|
2654
|
+
// Build selected fields configuration (supports nested objects)
|
|
2533
2655
|
const selectedFieldConfigs = [];
|
|
2534
2656
|
const localParams = [];
|
|
2657
|
+
// Helper function to check if a value is a plain object (not FieldRef, SqlFragment, etc.)
|
|
2658
|
+
const isPlainObject = (val) => {
|
|
2659
|
+
return typeof val === 'object' &&
|
|
2660
|
+
val !== null &&
|
|
2661
|
+
!('__dbColumnName' in val) &&
|
|
2662
|
+
!(val instanceof conditions_1.SqlFragment) &&
|
|
2663
|
+
!Array.isArray(val) &&
|
|
2664
|
+
val.constructor === Object;
|
|
2665
|
+
};
|
|
2666
|
+
// Helper function to recursively process fields and build SelectedField structures
|
|
2667
|
+
const processField = (alias, field) => {
|
|
2668
|
+
if (field instanceof conditions_1.SqlFragment) {
|
|
2669
|
+
// SQL Fragment - build the SQL expression
|
|
2670
|
+
const sqlBuildContext = {
|
|
2671
|
+
paramCounter: context.paramCounter,
|
|
2672
|
+
params: context.allParams,
|
|
2673
|
+
};
|
|
2674
|
+
const fragmentSql = field.buildSql(sqlBuildContext);
|
|
2675
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
2676
|
+
return { alias, expression: fragmentSql };
|
|
2677
|
+
}
|
|
2678
|
+
else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
2679
|
+
// FieldRef object - use database column name
|
|
2680
|
+
const dbColumnName = field.__dbColumnName;
|
|
2681
|
+
return { alias, expression: `"${dbColumnName}"` };
|
|
2682
|
+
}
|
|
2683
|
+
else if (typeof field === 'string') {
|
|
2684
|
+
// Simple string reference (for backward compatibility)
|
|
2685
|
+
return { alias, expression: `"${field}"` };
|
|
2686
|
+
}
|
|
2687
|
+
else if (isPlainObject(field)) {
|
|
2688
|
+
// Nested object - recursively process its fields
|
|
2689
|
+
const nestedFields = [];
|
|
2690
|
+
for (const [nestedAlias, nestedField] of Object.entries(field)) {
|
|
2691
|
+
nestedFields.push(processField(nestedAlias, nestedField));
|
|
2692
|
+
}
|
|
2693
|
+
return { alias, nested: nestedFields };
|
|
2694
|
+
}
|
|
2695
|
+
else {
|
|
2696
|
+
// Literal value or expression
|
|
2697
|
+
const expression = `$${context.paramCounter++}`;
|
|
2698
|
+
context.allParams.push(field);
|
|
2699
|
+
localParams.push(field);
|
|
2700
|
+
return { alias, expression };
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2535
2703
|
// Step 1: Build field selection configuration
|
|
2536
2704
|
if (this.selector) {
|
|
2537
2705
|
const mockItem = this.createMockItem();
|
|
@@ -2547,54 +2715,31 @@ class CollectionQueryBuilder {
|
|
|
2547
2715
|
});
|
|
2548
2716
|
}
|
|
2549
2717
|
else {
|
|
2550
|
-
// Object selection - extract each field
|
|
2718
|
+
// Object selection - extract each field (with support for nested objects)
|
|
2551
2719
|
for (const [alias, field] of Object.entries(selectedFields)) {
|
|
2552
|
-
|
|
2553
|
-
// SQL Fragment - build the SQL expression
|
|
2554
|
-
const sqlBuildContext = {
|
|
2555
|
-
paramCounter: context.paramCounter,
|
|
2556
|
-
params: context.allParams,
|
|
2557
|
-
};
|
|
2558
|
-
const fragmentSql = field.buildSql(sqlBuildContext);
|
|
2559
|
-
context.paramCounter = sqlBuildContext.paramCounter;
|
|
2560
|
-
selectedFieldConfigs.push({
|
|
2561
|
-
alias,
|
|
2562
|
-
expression: fragmentSql,
|
|
2563
|
-
});
|
|
2564
|
-
}
|
|
2565
|
-
else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
2566
|
-
// FieldRef object - use database column name
|
|
2567
|
-
const dbColumnName = field.__dbColumnName;
|
|
2568
|
-
selectedFieldConfigs.push({
|
|
2569
|
-
alias,
|
|
2570
|
-
expression: `"${dbColumnName}"`,
|
|
2571
|
-
});
|
|
2572
|
-
}
|
|
2573
|
-
else if (typeof field === 'string') {
|
|
2574
|
-
// Simple string reference (for backward compatibility)
|
|
2575
|
-
selectedFieldConfigs.push({
|
|
2576
|
-
alias,
|
|
2577
|
-
expression: `"${field}"`,
|
|
2578
|
-
});
|
|
2579
|
-
}
|
|
2580
|
-
else {
|
|
2581
|
-
// Literal value or expression
|
|
2582
|
-
selectedFieldConfigs.push({
|
|
2583
|
-
alias,
|
|
2584
|
-
expression: `$${context.paramCounter++}`,
|
|
2585
|
-
});
|
|
2586
|
-
context.allParams.push(field);
|
|
2587
|
-
localParams.push(field);
|
|
2588
|
-
}
|
|
2720
|
+
selectedFieldConfigs.push(processField(alias, field));
|
|
2589
2721
|
}
|
|
2590
2722
|
}
|
|
2591
2723
|
}
|
|
2592
2724
|
else {
|
|
2593
|
-
// No selector - select all fields
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2725
|
+
// No selector - select all fields from the target table schema
|
|
2726
|
+
if (this.targetTableSchema && this.targetTableSchema.columns) {
|
|
2727
|
+
for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
|
|
2728
|
+
const colConfig = colBuilder.build ? colBuilder.build() : colBuilder;
|
|
2729
|
+
const dbColumnName = colConfig.name || colName;
|
|
2730
|
+
selectedFieldConfigs.push({
|
|
2731
|
+
alias: colName,
|
|
2732
|
+
expression: `"${dbColumnName}"`,
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
else {
|
|
2737
|
+
// Fallback: use * (less ideal, may cause issues)
|
|
2738
|
+
selectedFieldConfigs.push({
|
|
2739
|
+
alias: '*',
|
|
2740
|
+
expression: '*',
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2598
2743
|
}
|
|
2599
2744
|
// Step 2: Build WHERE clause SQL (without WHERE keyword)
|
|
2600
2745
|
let whereClause;
|
|
@@ -2687,10 +2832,14 @@ class CollectionQueryBuilder {
|
|
|
2687
2832
|
// Async strategy - return special marker with the promise
|
|
2688
2833
|
return result;
|
|
2689
2834
|
}
|
|
2690
|
-
// Synchronous strategy (JSONB/CTE)
|
|
2835
|
+
// Synchronous strategy (JSONB/CTE/LATERAL)
|
|
2691
2836
|
return {
|
|
2692
2837
|
sql: result.sql,
|
|
2693
2838
|
params: localParams,
|
|
2839
|
+
isCTE: result.isCTE,
|
|
2840
|
+
joinClause: result.joinClause,
|
|
2841
|
+
selectExpression: result.selectExpression,
|
|
2842
|
+
tableName: result.tableName,
|
|
2694
2843
|
};
|
|
2695
2844
|
}
|
|
2696
2845
|
}
|