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,11 +1,117 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQueryBuilder = exports.QueryBuilder = void 0;
|
|
4
|
+
exports.getColumnNameMapForSchema = getColumnNameMapForSchema;
|
|
5
|
+
exports.getRelationEntriesForSchema = getRelationEntriesForSchema;
|
|
6
|
+
exports.getTargetSchemaForRelation = getTargetSchemaForRelation;
|
|
7
|
+
exports.createNestedFieldRefProxy = createNestedFieldRefProxy;
|
|
4
8
|
const conditions_1 = require("./conditions");
|
|
9
|
+
const query_utils_1 = require("./query-utils");
|
|
5
10
|
const subquery_1 = require("./subquery");
|
|
6
11
|
const grouped_query_1 = require("./grouped-query");
|
|
7
12
|
const cte_builder_1 = require("./cte-builder");
|
|
8
13
|
const collection_strategy_factory_1 = require("./collection-strategy.factory");
|
|
14
|
+
/**
|
|
15
|
+
* Performance utility: Get column name map from schema, using cached version if available
|
|
16
|
+
*/
|
|
17
|
+
function getColumnNameMapForSchema(schema) {
|
|
18
|
+
if (schema.columnNameMap) {
|
|
19
|
+
return schema.columnNameMap;
|
|
20
|
+
}
|
|
21
|
+
// Fallback: build the map (for schemas that weren't built with the new TableBuilder)
|
|
22
|
+
const map = new Map();
|
|
23
|
+
for (const [colName, colBuilder] of Object.entries(schema.columns)) {
|
|
24
|
+
map.set(colName, colBuilder.build().name);
|
|
25
|
+
}
|
|
26
|
+
return map;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Performance utility: Get relation entries array from schema, using cached version if available
|
|
30
|
+
*/
|
|
31
|
+
function getRelationEntriesForSchema(schema) {
|
|
32
|
+
if (schema.relationEntries) {
|
|
33
|
+
return schema.relationEntries;
|
|
34
|
+
}
|
|
35
|
+
// Fallback: build the array (for schemas that weren't built with the new TableBuilder)
|
|
36
|
+
return Object.entries(schema.relations);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Performance utility: Get target schema for a relation, using cached version if available
|
|
40
|
+
*/
|
|
41
|
+
function getTargetSchemaForRelation(schema, relName, relConfig) {
|
|
42
|
+
// Try cached version first
|
|
43
|
+
if (schema.relationSchemaCache) {
|
|
44
|
+
const cached = schema.relationSchemaCache.get(relName);
|
|
45
|
+
if (cached)
|
|
46
|
+
return cached;
|
|
47
|
+
}
|
|
48
|
+
// Fallback: build the schema
|
|
49
|
+
if (relConfig.targetTableBuilder) {
|
|
50
|
+
return relConfig.targetTableBuilder.build();
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
// Performance: Cache nested field ref proxies per table alias
|
|
55
|
+
const nestedFieldRefProxyCache = new Map();
|
|
56
|
+
/**
|
|
57
|
+
* Creates a nested proxy that supports accessing properties at any depth.
|
|
58
|
+
* This allows patterns like `p.product.priceMode` to work even without full schema information.
|
|
59
|
+
* Each property access returns an object that is both a FieldRef and can be further accessed.
|
|
60
|
+
*
|
|
61
|
+
* @param tableAlias The table alias to use for the FieldRef
|
|
62
|
+
* @returns A proxy that creates FieldRefs for any property access
|
|
63
|
+
*/
|
|
64
|
+
function createNestedFieldRefProxy(tableAlias) {
|
|
65
|
+
// Return cached proxy if available
|
|
66
|
+
const cached = nestedFieldRefProxyCache.get(tableAlias);
|
|
67
|
+
if (cached)
|
|
68
|
+
return cached;
|
|
69
|
+
const handler = {
|
|
70
|
+
get: (_target, prop) => {
|
|
71
|
+
// Handle Symbol.toPrimitive for string conversion (used in template literals)
|
|
72
|
+
if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') {
|
|
73
|
+
return () => `[NestedFieldRefProxy:${tableAlias}]`;
|
|
74
|
+
}
|
|
75
|
+
if (typeof prop === 'symbol')
|
|
76
|
+
return undefined;
|
|
77
|
+
// Return an object that is both a FieldRef AND a proxy for further nesting
|
|
78
|
+
const fieldRef = {
|
|
79
|
+
__fieldName: prop,
|
|
80
|
+
__dbColumnName: prop,
|
|
81
|
+
__tableAlias: tableAlias,
|
|
82
|
+
};
|
|
83
|
+
// Return a proxy that acts as both the FieldRef and allows further property access
|
|
84
|
+
return new Proxy(fieldRef, {
|
|
85
|
+
get: (fieldTarget, nestedProp) => {
|
|
86
|
+
// Handle Symbol.toPrimitive for string conversion (used in template literals)
|
|
87
|
+
if (nestedProp === Symbol.toPrimitive || nestedProp === 'toString' || nestedProp === 'valueOf') {
|
|
88
|
+
return () => fieldTarget.__dbColumnName;
|
|
89
|
+
}
|
|
90
|
+
if (typeof nestedProp === 'symbol')
|
|
91
|
+
return undefined;
|
|
92
|
+
// If accessing FieldRef properties, return them
|
|
93
|
+
if (nestedProp === '__fieldName' || nestedProp === '__dbColumnName' || nestedProp === '__tableAlias') {
|
|
94
|
+
return fieldTarget[nestedProp];
|
|
95
|
+
}
|
|
96
|
+
// Otherwise, treat as nested navigation and create a new nested proxy
|
|
97
|
+
// The nested table alias is the property name (e.g., 'product' for p.product)
|
|
98
|
+
return createNestedFieldRefProxy(prop)[nestedProp];
|
|
99
|
+
},
|
|
100
|
+
has: (_fieldTarget, _nestedProp) => true,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
has: (_target, prop) => {
|
|
104
|
+
// The outer proxy doesn't have FieldRef properties - only field names
|
|
105
|
+
if (prop === '__fieldName' || prop === '__dbColumnName' || prop === '__tableAlias') {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const proxy = new Proxy({}, handler);
|
|
112
|
+
nestedFieldRefProxyCache.set(tableAlias, proxy);
|
|
113
|
+
return proxy;
|
|
114
|
+
}
|
|
9
115
|
/**
|
|
10
116
|
* Cached regex for numeric string detection
|
|
11
117
|
* Used to convert PostgreSQL NUMERIC/BIGINT strings to numbers
|
|
@@ -38,6 +144,7 @@ class QueryBuilder {
|
|
|
38
144
|
}
|
|
39
145
|
/**
|
|
40
146
|
* Define the selection with support for nested queries
|
|
147
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
41
148
|
*/
|
|
42
149
|
select(selector) {
|
|
43
150
|
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 +154,25 @@ class QueryBuilder {
|
|
|
47
154
|
}
|
|
48
155
|
/**
|
|
49
156
|
* Add WHERE condition
|
|
157
|
+
* Multiple where() calls are chained with AND logic
|
|
50
158
|
*/
|
|
51
159
|
where(condition) {
|
|
52
160
|
const mockRow = this.createMockRow();
|
|
53
|
-
|
|
161
|
+
const newCondition = condition(mockRow);
|
|
162
|
+
if (this.whereCond) {
|
|
163
|
+
this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
this.whereCond = newCondition;
|
|
167
|
+
}
|
|
54
168
|
return this;
|
|
55
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Add CTEs (Common Table Expressions) to the query
|
|
172
|
+
*/
|
|
173
|
+
with(...ctes) {
|
|
174
|
+
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);
|
|
175
|
+
}
|
|
56
176
|
/**
|
|
57
177
|
* Create mock row for analysis
|
|
58
178
|
*/
|
|
@@ -62,14 +182,10 @@ class QueryBuilder {
|
|
|
62
182
|
return this._cachedMockRow;
|
|
63
183
|
}
|
|
64
184
|
const mock = {};
|
|
65
|
-
// Performance:
|
|
66
|
-
const
|
|
67
|
-
const columnConfigs = new Map();
|
|
68
|
-
for (const [colName, colBuilder] of columnEntries) {
|
|
69
|
-
columnConfigs.set(colName, colBuilder.build().name);
|
|
70
|
-
}
|
|
185
|
+
// Performance: Use pre-computed column name map if available
|
|
186
|
+
const columnNameMap = getColumnNameMapForSchema(this.schema);
|
|
71
187
|
// Add columns as FieldRef objects - type-safe with property name and database column name
|
|
72
|
-
for (const [colName, dbColumnName] of
|
|
188
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
73
189
|
Object.defineProperty(mock, colName, {
|
|
74
190
|
get: () => ({
|
|
75
191
|
__fieldName: colName,
|
|
@@ -80,17 +196,13 @@ class QueryBuilder {
|
|
|
80
196
|
configurable: true,
|
|
81
197
|
});
|
|
82
198
|
}
|
|
83
|
-
// Performance:
|
|
84
|
-
const
|
|
85
|
-
for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
|
|
86
|
-
if (relConfig.targetTableBuilder) {
|
|
87
|
-
relationSchemas.set(relName, relConfig.targetTableBuilder.build());
|
|
88
|
-
}
|
|
89
|
-
}
|
|
199
|
+
// Performance: Use pre-computed relation entries and cached schemas
|
|
200
|
+
const relationEntries = getRelationEntriesForSchema(this.schema);
|
|
90
201
|
// Add relations (both collections and single references)
|
|
91
|
-
for (const [relName, relConfig] of
|
|
202
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
203
|
+
// Performance: Use cached target schema
|
|
204
|
+
const targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
|
|
92
205
|
if (relConfig.type === 'many') {
|
|
93
|
-
const targetSchema = relationSchemas.get(relName);
|
|
94
206
|
Object.defineProperty(mock, relName, {
|
|
95
207
|
get: () => {
|
|
96
208
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
|
|
@@ -101,7 +213,6 @@ class QueryBuilder {
|
|
|
101
213
|
}
|
|
102
214
|
else {
|
|
103
215
|
// Single reference navigation (many-to-one, one-to-one)
|
|
104
|
-
const targetSchema = relationSchemas.get(relName);
|
|
105
216
|
Object.defineProperty(mock, relName, {
|
|
106
217
|
get: () => {
|
|
107
218
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
@@ -118,6 +229,7 @@ class QueryBuilder {
|
|
|
118
229
|
}
|
|
119
230
|
/**
|
|
120
231
|
* Add a LEFT JOIN to the query with a selector (supports both tables and subqueries)
|
|
232
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
121
233
|
*/
|
|
122
234
|
leftJoin(rightTable, condition, selector, alias) {
|
|
123
235
|
// Check if rightTable is a Subquery
|
|
@@ -166,6 +278,7 @@ class QueryBuilder {
|
|
|
166
278
|
}
|
|
167
279
|
/**
|
|
168
280
|
* Add an INNER JOIN to the query with a selector (supports both tables and subqueries)
|
|
281
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
169
282
|
*/
|
|
170
283
|
innerJoin(rightTable, condition, selector, alias) {
|
|
171
284
|
// Check if rightTable is a Subquery
|
|
@@ -217,14 +330,10 @@ class QueryBuilder {
|
|
|
217
330
|
*/
|
|
218
331
|
createMockRowForTable(schema, alias) {
|
|
219
332
|
const mock = {};
|
|
220
|
-
// Performance:
|
|
221
|
-
const
|
|
222
|
-
const columnConfigs = new Map();
|
|
223
|
-
for (const [colName, colBuilder] of columnEntries) {
|
|
224
|
-
columnConfigs.set(colName, colBuilder.build().name);
|
|
225
|
-
}
|
|
333
|
+
// Performance: Use pre-computed column name map if available
|
|
334
|
+
const columnNameMap = getColumnNameMapForSchema(schema);
|
|
226
335
|
// Add columns as FieldRef objects with table alias
|
|
227
|
-
for (const [colName, dbColumnName] of
|
|
336
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
228
337
|
Object.defineProperty(mock, colName, {
|
|
229
338
|
get: () => ({
|
|
230
339
|
__fieldName: colName,
|
|
@@ -235,18 +344,14 @@ class QueryBuilder {
|
|
|
235
344
|
configurable: true,
|
|
236
345
|
});
|
|
237
346
|
}
|
|
238
|
-
// Performance:
|
|
239
|
-
const
|
|
240
|
-
for (const [relName, relConfig] of Object.entries(schema.relations)) {
|
|
241
|
-
if (relConfig.targetTableBuilder) {
|
|
242
|
-
relationSchemas.set(relName, relConfig.targetTableBuilder.build());
|
|
243
|
-
}
|
|
244
|
-
}
|
|
347
|
+
// Performance: Use pre-computed relation entries and cached schemas
|
|
348
|
+
const relationEntries = getRelationEntriesForSchema(schema);
|
|
245
349
|
// Add navigation properties (single references and collections)
|
|
246
|
-
for (const [relName, relConfig] of
|
|
350
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
351
|
+
// Performance: Use cached target schema
|
|
352
|
+
const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
|
|
247
353
|
if (relConfig.type === 'many') {
|
|
248
354
|
// Collection navigation
|
|
249
|
-
const targetSchema = relationSchemas.get(relName);
|
|
250
355
|
Object.defineProperty(mock, relName, {
|
|
251
356
|
get: () => {
|
|
252
357
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
|
|
@@ -257,7 +362,6 @@ class QueryBuilder {
|
|
|
257
362
|
}
|
|
258
363
|
else {
|
|
259
364
|
// Single reference navigation
|
|
260
|
-
const targetSchema = relationSchemas.get(relName);
|
|
261
365
|
Object.defineProperty(mock, relName, {
|
|
262
366
|
get: () => {
|
|
263
367
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
@@ -287,35 +391,7 @@ class QueryBuilder {
|
|
|
287
391
|
orderBy(selector) {
|
|
288
392
|
const mockRow = this.createMockRow();
|
|
289
393
|
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
|
-
}
|
|
394
|
+
(0, query_utils_1.parseOrderBy)(result, this.orderByFields);
|
|
319
395
|
return this;
|
|
320
396
|
}
|
|
321
397
|
}
|
|
@@ -353,6 +429,7 @@ class SelectQueryBuilder {
|
|
|
353
429
|
}
|
|
354
430
|
/**
|
|
355
431
|
* Transform the selection with a new selector
|
|
432
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
356
433
|
*/
|
|
357
434
|
select(selector) {
|
|
358
435
|
// Create a composed selector that applies both transformations
|
|
@@ -364,13 +441,22 @@ class SelectQueryBuilder {
|
|
|
364
441
|
}
|
|
365
442
|
/**
|
|
366
443
|
* Add WHERE condition
|
|
444
|
+
* Multiple where() calls are chained with AND logic
|
|
367
445
|
* Note: The row parameter represents the selected shape (after select())
|
|
368
446
|
*/
|
|
369
447
|
where(condition) {
|
|
370
448
|
const mockRow = this.createMockRow();
|
|
371
449
|
// Apply the selector to get the selected shape that the user sees in the WHERE condition
|
|
372
450
|
const selectedMock = this.selector(mockRow);
|
|
373
|
-
|
|
451
|
+
// Wrap in proxy - for WHERE, we preserve original column names
|
|
452
|
+
const fieldRefProxy = this.createFieldRefProxy(selectedMock, true);
|
|
453
|
+
const newCondition = condition(fieldRefProxy);
|
|
454
|
+
if (this.whereCond) {
|
|
455
|
+
this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
this.whereCond = newCondition;
|
|
459
|
+
}
|
|
374
460
|
return this;
|
|
375
461
|
}
|
|
376
462
|
/**
|
|
@@ -384,7 +470,12 @@ class SelectQueryBuilder {
|
|
|
384
470
|
* .toList();
|
|
385
471
|
*/
|
|
386
472
|
with(...ctes) {
|
|
387
|
-
|
|
473
|
+
// Add CTEs, avoiding duplicates by name
|
|
474
|
+
for (const cte of ctes) {
|
|
475
|
+
if (!this.ctes.some(existing => existing.name === cte.name)) {
|
|
476
|
+
this.ctes.push(cte);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
388
479
|
return this;
|
|
389
480
|
}
|
|
390
481
|
/**
|
|
@@ -404,36 +495,12 @@ class SelectQueryBuilder {
|
|
|
404
495
|
orderBy(selector) {
|
|
405
496
|
const mockRow = this.createMockRow();
|
|
406
497
|
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
|
-
}
|
|
498
|
+
// Wrap selectedMock in a proxy that returns FieldRefs for property access
|
|
499
|
+
const fieldRefProxy = this.createFieldRefProxy(selectedMock);
|
|
500
|
+
const result = selector(fieldRefProxy);
|
|
501
|
+
// Clear previous orderBy - last one takes precedence
|
|
502
|
+
this.orderByFields = [];
|
|
503
|
+
(0, query_utils_1.parseOrderBy)(result, this.orderByFields);
|
|
437
504
|
return this;
|
|
438
505
|
}
|
|
439
506
|
/**
|
|
@@ -483,10 +550,7 @@ class SelectQueryBuilder {
|
|
|
483
550
|
};
|
|
484
551
|
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
552
|
}
|
|
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
|
-
*/
|
|
553
|
+
// Implementation
|
|
490
554
|
leftJoin(rightTable, condition, selector, alias) {
|
|
491
555
|
// Check if rightTable is a CTE
|
|
492
556
|
if ((0, cte_builder_1.isCte)(rightTable)) {
|
|
@@ -596,6 +660,7 @@ class SelectQueryBuilder {
|
|
|
596
660
|
/**
|
|
597
661
|
* Add an INNER JOIN to the query with a selector
|
|
598
662
|
* Note: After select(), the left parameter in the join will be the selected shape (TSelection)
|
|
663
|
+
* UnwrapSelection extracts the value types from SqlFragment<T> expressions
|
|
599
664
|
*/
|
|
600
665
|
innerJoin(rightTable, condition, selector, alias) {
|
|
601
666
|
// Check if rightTable is a Subquery
|
|
@@ -637,14 +702,10 @@ class SelectQueryBuilder {
|
|
|
637
702
|
*/
|
|
638
703
|
createMockRowForTable(schema, alias) {
|
|
639
704
|
const mock = {};
|
|
640
|
-
// Performance:
|
|
641
|
-
const
|
|
642
|
-
const columnConfigs = new Map();
|
|
643
|
-
for (const [colName, colBuilder] of columnEntries) {
|
|
644
|
-
columnConfigs.set(colName, colBuilder.build().name);
|
|
645
|
-
}
|
|
705
|
+
// Performance: Use pre-computed column name map if available
|
|
706
|
+
const columnNameMap = getColumnNameMapForSchema(schema);
|
|
646
707
|
// Add columns as FieldRef objects with table alias
|
|
647
|
-
for (const [colName, dbColumnName] of
|
|
708
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
648
709
|
Object.defineProperty(mock, colName, {
|
|
649
710
|
get: () => ({
|
|
650
711
|
__fieldName: colName,
|
|
@@ -655,18 +716,14 @@ class SelectQueryBuilder {
|
|
|
655
716
|
configurable: true,
|
|
656
717
|
});
|
|
657
718
|
}
|
|
658
|
-
// Performance:
|
|
659
|
-
const
|
|
660
|
-
for (const [relName, relConfig] of Object.entries(schema.relations)) {
|
|
661
|
-
if (relConfig.targetTableBuilder) {
|
|
662
|
-
relationSchemas.set(relName, relConfig.targetTableBuilder.build());
|
|
663
|
-
}
|
|
664
|
-
}
|
|
719
|
+
// Performance: Use pre-computed relation entries and cached schemas
|
|
720
|
+
const relationEntries = getRelationEntriesForSchema(schema);
|
|
665
721
|
// Add navigation properties (single references and collections)
|
|
666
|
-
for (const [relName, relConfig] of
|
|
722
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
723
|
+
// Performance: Use cached target schema
|
|
724
|
+
const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
|
|
667
725
|
if (relConfig.type === 'many') {
|
|
668
726
|
// Collection navigation
|
|
669
|
-
const targetSchema = relationSchemas.get(relName);
|
|
670
727
|
Object.defineProperty(mock, relName, {
|
|
671
728
|
get: () => {
|
|
672
729
|
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
|
|
@@ -677,7 +734,6 @@ class SelectQueryBuilder {
|
|
|
677
734
|
}
|
|
678
735
|
else {
|
|
679
736
|
// Single reference navigation
|
|
680
|
-
const targetSchema = relationSchemas.get(relName);
|
|
681
737
|
Object.defineProperty(mock, relName, {
|
|
682
738
|
get: () => {
|
|
683
739
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
@@ -1265,9 +1321,10 @@ class SelectQueryBuilder {
|
|
|
1265
1321
|
*/
|
|
1266
1322
|
createMockRow() {
|
|
1267
1323
|
const mock = {};
|
|
1324
|
+
// Performance: Use pre-computed column name map if available
|
|
1325
|
+
const columnNameMap = getColumnNameMapForSchema(this.schema);
|
|
1268
1326
|
// Add columns as FieldRef objects - type-safe with property name and database column name
|
|
1269
|
-
for (const [colName,
|
|
1270
|
-
const dbColumnName = colBuilder.build().name;
|
|
1327
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
1271
1328
|
Object.defineProperty(mock, colName, {
|
|
1272
1329
|
get: () => ({
|
|
1273
1330
|
__fieldName: colName,
|
|
@@ -1284,13 +1341,12 @@ class SelectQueryBuilder {
|
|
|
1284
1341
|
if (join.isSubquery || !join.schema) {
|
|
1285
1342
|
continue;
|
|
1286
1343
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1344
|
+
// Performance: Use pre-computed column name map for joined schema
|
|
1345
|
+
const joinColumnNameMap = getColumnNameMapForSchema(join.schema);
|
|
1346
|
+
if (!mock[join.alias]) {
|
|
1347
|
+
mock[join.alias] = {};
|
|
1348
|
+
}
|
|
1349
|
+
for (const [colName, dbColumnName] of joinColumnNameMap) {
|
|
1294
1350
|
Object.defineProperty(mock[join.alias], colName, {
|
|
1295
1351
|
get: () => ({
|
|
1296
1352
|
__fieldName: colName,
|
|
@@ -1302,13 +1358,23 @@ class SelectQueryBuilder {
|
|
|
1302
1358
|
});
|
|
1303
1359
|
}
|
|
1304
1360
|
}
|
|
1361
|
+
// Performance: Use pre-computed relation entries
|
|
1362
|
+
const relationEntries = getRelationEntriesForSchema(this.schema);
|
|
1305
1363
|
// Add relations as CollectionQueryBuilder or ReferenceQueryBuilder
|
|
1306
|
-
for (const [relName, relConfig] of
|
|
1364
|
+
for (const [relName, relConfig] of relationEntries) {
|
|
1365
|
+
// Try to get target schema from registry (preferred, has full relations) or cached schema
|
|
1366
|
+
let targetSchema;
|
|
1367
|
+
if (this.schemaRegistry) {
|
|
1368
|
+
targetSchema = this.schemaRegistry.get(relConfig.targetTable);
|
|
1369
|
+
}
|
|
1370
|
+
if (!targetSchema) {
|
|
1371
|
+
// Performance: Use cached target schema
|
|
1372
|
+
targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
|
|
1373
|
+
}
|
|
1307
1374
|
if (relConfig.type === 'many') {
|
|
1308
1375
|
Object.defineProperty(mock, relName, {
|
|
1309
1376
|
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
|
|
1377
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, // Pass the target schema directly
|
|
1312
1378
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
1313
1379
|
);
|
|
1314
1380
|
},
|
|
@@ -1320,8 +1386,7 @@ class SelectQueryBuilder {
|
|
|
1320
1386
|
// For single reference (many-to-one), create a ReferenceQueryBuilder
|
|
1321
1387
|
Object.defineProperty(mock, relName, {
|
|
1322
1388
|
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
|
|
1389
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
|
|
1325
1390
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
1326
1391
|
);
|
|
1327
1392
|
// Return a mock object that exposes the target table's columns
|
|
@@ -1334,6 +1399,67 @@ class SelectQueryBuilder {
|
|
|
1334
1399
|
}
|
|
1335
1400
|
return mock;
|
|
1336
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Create a proxy that wraps selected values and returns FieldRefs for property access
|
|
1404
|
+
* This enables orderBy and other operations to work with chained selects
|
|
1405
|
+
* @param preserveOriginal - If true (for WHERE), preserve original column names; if false (for ORDER BY), use alias names
|
|
1406
|
+
*/
|
|
1407
|
+
createFieldRefProxy(selectedMock, preserveOriginal = false) {
|
|
1408
|
+
if (!selectedMock || typeof selectedMock !== 'object') {
|
|
1409
|
+
return selectedMock;
|
|
1410
|
+
}
|
|
1411
|
+
// If it already has FieldRef properties, return as-is
|
|
1412
|
+
if ('__fieldName' in selectedMock && '__dbColumnName' in selectedMock) {
|
|
1413
|
+
return selectedMock;
|
|
1414
|
+
}
|
|
1415
|
+
// Create a proxy that returns FieldRefs for each property access
|
|
1416
|
+
return new Proxy(selectedMock, {
|
|
1417
|
+
get: (target, prop) => {
|
|
1418
|
+
if (typeof prop === 'symbol' || prop === 'constructor' || prop === 'then') {
|
|
1419
|
+
return target[prop];
|
|
1420
|
+
}
|
|
1421
|
+
const value = target[prop];
|
|
1422
|
+
// If the value is already a FieldRef
|
|
1423
|
+
if (value && typeof value === 'object' && '__fieldName' in value && '__dbColumnName' in value) {
|
|
1424
|
+
if (preserveOriginal) {
|
|
1425
|
+
// For WHERE: preserve original column name and table alias
|
|
1426
|
+
// This ensures WHERE references the actual database column
|
|
1427
|
+
return {
|
|
1428
|
+
__fieldName: prop,
|
|
1429
|
+
__dbColumnName: value.__dbColumnName,
|
|
1430
|
+
__tableAlias: value.__tableAlias,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
else {
|
|
1434
|
+
// For ORDER BY: use the alias (property name) as the column name
|
|
1435
|
+
// In chained selects, the alias becomes the column name in the subquery
|
|
1436
|
+
return {
|
|
1437
|
+
__fieldName: prop,
|
|
1438
|
+
__dbColumnName: prop,
|
|
1439
|
+
// No table alias - column comes from the selection/subquery
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
// If the value is a SqlFragment, treat it as a FieldRef using the property name as the alias
|
|
1444
|
+
if (value && typeof value === 'object' && value instanceof conditions_1.SqlFragment) {
|
|
1445
|
+
return {
|
|
1446
|
+
__fieldName: prop,
|
|
1447
|
+
__dbColumnName: prop,
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
// If the value is an object (nested selection), recursively wrap it
|
|
1451
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1452
|
+
return this.createFieldRefProxy(value, preserveOriginal);
|
|
1453
|
+
}
|
|
1454
|
+
// For primitive values or arrays, create a FieldRef
|
|
1455
|
+
// Use the property name as both fieldName and dbColumnName
|
|
1456
|
+
return {
|
|
1457
|
+
__fieldName: prop,
|
|
1458
|
+
__dbColumnName: prop,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1337
1463
|
/**
|
|
1338
1464
|
* Detect navigation property references in selection and add necessary JOINs
|
|
1339
1465
|
*/
|
|
@@ -1344,9 +1470,66 @@ class SelectQueryBuilder {
|
|
|
1344
1470
|
for (const [key, value] of Object.entries(selection)) {
|
|
1345
1471
|
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
1346
1472
|
// This is a FieldRef with a table alias - check if it's from a related table
|
|
1347
|
-
|
|
1473
|
+
this.addJoinForFieldRef(value, joins);
|
|
1474
|
+
}
|
|
1475
|
+
else if (value instanceof conditions_1.SqlFragment) {
|
|
1476
|
+
// SqlFragment may contain navigation property references - extract them
|
|
1477
|
+
const fieldRefs = value.getFieldRefs();
|
|
1478
|
+
for (const fieldRef of fieldRefs) {
|
|
1479
|
+
this.addJoinForFieldRef(fieldRef, joins);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1483
|
+
// Recursively check nested objects
|
|
1484
|
+
this.detectAndAddJoinsFromSelection(value, joins);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Add a JOIN for a FieldRef if it references a related table
|
|
1490
|
+
*/
|
|
1491
|
+
addJoinForFieldRef(fieldRef, joins) {
|
|
1492
|
+
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef) || !('__dbColumnName' in fieldRef)) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
1496
|
+
if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
1497
|
+
// This references a related table - find the relation and add a JOIN
|
|
1498
|
+
const relation = this.schema.relations[tableAlias];
|
|
1499
|
+
if (relation && relation.type === 'one') {
|
|
1500
|
+
// Get target schema from targetTableBuilder if available
|
|
1501
|
+
let targetSchema;
|
|
1502
|
+
if (relation.targetTableBuilder) {
|
|
1503
|
+
const targetTableSchema = relation.targetTableBuilder.build();
|
|
1504
|
+
targetSchema = targetTableSchema.schema;
|
|
1505
|
+
}
|
|
1506
|
+
// Add a JOIN for this reference
|
|
1507
|
+
joins.push({
|
|
1508
|
+
alias: tableAlias,
|
|
1509
|
+
targetTable: relation.targetTable,
|
|
1510
|
+
targetSchema,
|
|
1511
|
+
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
1512
|
+
matches: relation.matches || [],
|
|
1513
|
+
isMandatory: relation.isMandatory ?? false,
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Detect navigation property references in a WHERE condition and add necessary JOINs
|
|
1520
|
+
*/
|
|
1521
|
+
detectAndAddJoinsFromCondition(condition, joins) {
|
|
1522
|
+
if (!condition) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
// Get all field references from the condition
|
|
1526
|
+
const fieldRefs = condition.getFieldRefs();
|
|
1527
|
+
for (const fieldRef of fieldRefs) {
|
|
1528
|
+
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
1529
|
+
const tableAlias = fieldRef.__tableAlias;
|
|
1530
|
+
// Check if this references a related table that isn't already joined
|
|
1348
1531
|
if (tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
1349
|
-
//
|
|
1532
|
+
// Find the relation config for this navigation
|
|
1350
1533
|
const relation = this.schema.relations[tableAlias];
|
|
1351
1534
|
if (relation && relation.type === 'one') {
|
|
1352
1535
|
// Get target schema from targetTableBuilder if available
|
|
@@ -1367,10 +1550,6 @@ class SelectQueryBuilder {
|
|
|
1367
1550
|
}
|
|
1368
1551
|
}
|
|
1369
1552
|
}
|
|
1370
|
-
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1371
|
-
// Recursively check nested objects
|
|
1372
|
-
this.detectAndAddJoinsFromSelection(value, joins);
|
|
1373
|
-
}
|
|
1374
1553
|
}
|
|
1375
1554
|
}
|
|
1376
1555
|
/**
|
|
@@ -1387,6 +1566,8 @@ class SelectQueryBuilder {
|
|
|
1387
1566
|
const joins = [];
|
|
1388
1567
|
// Scan selection for navigation property references and add JOINs
|
|
1389
1568
|
this.detectAndAddJoinsFromSelection(selection, joins);
|
|
1569
|
+
// Scan WHERE condition for navigation property references and add JOINs
|
|
1570
|
+
this.detectAndAddJoinsFromCondition(this.whereCond, joins);
|
|
1390
1571
|
// Handle case where selection is a single value (not an object with properties)
|
|
1391
1572
|
if (selection instanceof conditions_1.SqlFragment) {
|
|
1392
1573
|
// Single SQL fragment - just build it directly
|
|
@@ -1411,11 +1592,19 @@ class SelectQueryBuilder {
|
|
|
1411
1592
|
// Process selection object properties
|
|
1412
1593
|
for (const [key, value] of Object.entries(selection)) {
|
|
1413
1594
|
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
1414
|
-
// Handle collection -
|
|
1415
|
-
|
|
1595
|
+
// Handle collection - delegate to strategy pattern via buildCTE
|
|
1596
|
+
// The strategy handles CTE/LATERAL specifics and returns necessary info
|
|
1416
1597
|
const cteData = value.buildCTE ? value.buildCTE(context) : value.buildCTE(context);
|
|
1417
|
-
|
|
1418
|
-
|
|
1598
|
+
const isCTE = cteData.isCTE !== false; // Default to CTE if not specified
|
|
1599
|
+
// Note: For CTE strategy, context.ctes is already populated by the strategy
|
|
1600
|
+
// We don't need to add it again - the strategy has already done this
|
|
1601
|
+
collectionFields.push({
|
|
1602
|
+
name: key,
|
|
1603
|
+
cteName: cteData.tableName || `cte_${context.cteCounter - 1}`, // Use tableName from result or infer from counter
|
|
1604
|
+
isCTE,
|
|
1605
|
+
joinClause: cteData.joinClause,
|
|
1606
|
+
selectExpression: cteData.selectExpression,
|
|
1607
|
+
});
|
|
1419
1608
|
}
|
|
1420
1609
|
else if (value instanceof subquery_1.Subquery || (value && typeof value === 'object' && 'buildSql' in value && typeof value.buildSql === 'function' && '__mode' in value)) {
|
|
1421
1610
|
// Handle Subquery - build SQL and wrap in parentheses
|
|
@@ -1512,9 +1701,10 @@ class SelectQueryBuilder {
|
|
|
1512
1701
|
// Select all columns from the target table and group them
|
|
1513
1702
|
// We'll need to use JSON object building in SQL
|
|
1514
1703
|
const fieldParts = [];
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1704
|
+
// Performance: Use cached column name map
|
|
1705
|
+
const targetColMap = getColumnNameMapForSchema(targetSchema);
|
|
1706
|
+
for (const [colKey, dbColName] of targetColMap) {
|
|
1707
|
+
fieldParts.push(`'${colKey}', "${alias}"."${dbColName}"`);
|
|
1518
1708
|
}
|
|
1519
1709
|
selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
|
|
1520
1710
|
}
|
|
@@ -1544,11 +1734,8 @@ class SelectQueryBuilder {
|
|
|
1544
1734
|
const relConfig = this.schema.relations[alias];
|
|
1545
1735
|
if (relConfig && relConfig.type === 'one') {
|
|
1546
1736
|
// This is a reference navigation - select all fields from the target table
|
|
1547
|
-
//
|
|
1548
|
-
|
|
1549
|
-
if (relConfig.targetTableBuilder) {
|
|
1550
|
-
targetSchema = relConfig.targetTableBuilder.build();
|
|
1551
|
-
}
|
|
1737
|
+
// Performance: Use cached target schema
|
|
1738
|
+
const targetSchema = getTargetSchemaForRelation(this.schema, alias, relConfig);
|
|
1552
1739
|
if (targetSchema) {
|
|
1553
1740
|
// Add JOIN if not already added
|
|
1554
1741
|
if (!joins.find(j => j.alias === alias)) {
|
|
@@ -1567,9 +1754,10 @@ class SelectQueryBuilder {
|
|
|
1567
1754
|
}
|
|
1568
1755
|
// Select all columns from the target table and group them into a JSON object
|
|
1569
1756
|
const fieldParts = [];
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1757
|
+
// Performance: Use cached column name map
|
|
1758
|
+
const targetColMap = getColumnNameMapForSchema(targetSchema);
|
|
1759
|
+
for (const [colKey, dbColName] of targetColMap) {
|
|
1760
|
+
fieldParts.push(`'${colKey}', "${alias}"."${dbColName}"`);
|
|
1573
1761
|
}
|
|
1574
1762
|
selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
|
|
1575
1763
|
continue;
|
|
@@ -1601,8 +1789,14 @@ class SelectQueryBuilder {
|
|
|
1601
1789
|
}
|
|
1602
1790
|
} // End of for loop
|
|
1603
1791
|
} // End of else block
|
|
1604
|
-
// Add collection fields as JSON/array aggregations joined from CTEs
|
|
1605
|
-
for (const { name, cteName } of collectionFields) {
|
|
1792
|
+
// Add collection fields as JSON/array aggregations joined from CTEs or LATERAL joins
|
|
1793
|
+
for (const { name, cteName, selectExpression } of collectionFields) {
|
|
1794
|
+
// If selectExpression is provided (from strategy), use it directly
|
|
1795
|
+
if (selectExpression) {
|
|
1796
|
+
selectParts.push(`${selectExpression} as "${name}"`);
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
// Fallback to old logic for backward compatibility
|
|
1606
1800
|
// Check if this is an array aggregation (from toNumberList/toStringList)
|
|
1607
1801
|
const collectionValue = selection[name];
|
|
1608
1802
|
const isArrayAgg = collectionValue && typeof collectionValue === 'object' && 'isArrayAggregation' in collectionValue && collectionValue.isArrayAggregation();
|
|
@@ -1728,9 +1922,16 @@ class SelectQueryBuilder {
|
|
|
1728
1922
|
const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
1729
1923
|
fromClause += `\n${joinType} ${joinTableName} AS "${join.alias}" ON ${onConditions.join(' AND ')}`;
|
|
1730
1924
|
}
|
|
1731
|
-
// Join CTEs for collections
|
|
1732
|
-
for (const { cteName } of collectionFields) {
|
|
1733
|
-
|
|
1925
|
+
// Join CTEs and LATERAL subqueries for collections
|
|
1926
|
+
for (const { cteName, isCTE, joinClause } of collectionFields) {
|
|
1927
|
+
if (isCTE) {
|
|
1928
|
+
// CTE strategy - join by parent_id
|
|
1929
|
+
fromClause += `\nLEFT JOIN "${cteName}" ON "${cteName}".parent_id = ${qualifiedTableName}.id`;
|
|
1930
|
+
}
|
|
1931
|
+
else if (joinClause) {
|
|
1932
|
+
// LATERAL strategy - use the provided join clause (contains full LATERAL subquery)
|
|
1933
|
+
fromClause += `\n${joinClause}`;
|
|
1934
|
+
}
|
|
1734
1935
|
}
|
|
1735
1936
|
// Add DISTINCT if needed
|
|
1736
1937
|
const distinctClause = this.isDistinct ? 'DISTINCT ' : '';
|
|
@@ -2106,7 +2307,35 @@ class SelectQueryBuilder {
|
|
|
2106
2307
|
const mockRow = this.createMockRow();
|
|
2107
2308
|
selectionMetadata = this.selector(mockRow);
|
|
2108
2309
|
}
|
|
2109
|
-
|
|
2310
|
+
// Extract outer field refs from the WHERE condition
|
|
2311
|
+
// These are field refs that reference tables other than this subquery's table
|
|
2312
|
+
// and need to be propagated to the outer query for JOIN detection
|
|
2313
|
+
const outerFieldRefs = this.extractOuterFieldRefs();
|
|
2314
|
+
return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata, outerFieldRefs);
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Extract field refs from the WHERE condition that reference outer queries.
|
|
2318
|
+
* These are field refs with a __tableAlias that doesn't match this query's schema.
|
|
2319
|
+
*/
|
|
2320
|
+
extractOuterFieldRefs() {
|
|
2321
|
+
if (!this.whereCond) {
|
|
2322
|
+
return [];
|
|
2323
|
+
}
|
|
2324
|
+
const allRefs = this.whereCond.getFieldRefs();
|
|
2325
|
+
const outerRefs = [];
|
|
2326
|
+
const currentTableName = this.schema.name;
|
|
2327
|
+
for (const ref of allRefs) {
|
|
2328
|
+
// Check if this ref is from an outer query (different table alias)
|
|
2329
|
+
if ('__tableAlias' in ref && ref.__tableAlias) {
|
|
2330
|
+
const tableAlias = ref.__tableAlias;
|
|
2331
|
+
// If the table alias doesn't match our current schema, it's from an outer query
|
|
2332
|
+
// Also check if it's not a navigation property of this table (which would be in schema.relations)
|
|
2333
|
+
if (tableAlias !== currentTableName && !this.schema.relations[tableAlias]) {
|
|
2334
|
+
outerRefs.push(ref);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return outerRefs;
|
|
2110
2339
|
}
|
|
2111
2340
|
}
|
|
2112
2341
|
exports.SelectQueryBuilder = SelectQueryBuilder;
|
|
@@ -2120,12 +2349,15 @@ class ReferenceQueryBuilder {
|
|
|
2120
2349
|
this.foreignKeys = foreignKeys;
|
|
2121
2350
|
this.matches = matches;
|
|
2122
2351
|
this.isMandatory = isMandatory;
|
|
2123
|
-
this.targetTableSchema = targetTableSchema;
|
|
2124
2352
|
this.schemaRegistry = schemaRegistry;
|
|
2125
|
-
//
|
|
2126
|
-
if (
|
|
2353
|
+
// Prefer registry lookup (has full relations) over passed schema
|
|
2354
|
+
if (this.schemaRegistry) {
|
|
2127
2355
|
this.targetTableSchema = this.schemaRegistry.get(targetTable);
|
|
2128
2356
|
}
|
|
2357
|
+
// Fallback to passed schema if registry lookup failed
|
|
2358
|
+
if (!this.targetTableSchema) {
|
|
2359
|
+
this.targetTableSchema = targetTableSchema;
|
|
2360
|
+
}
|
|
2129
2361
|
}
|
|
2130
2362
|
/**
|
|
2131
2363
|
* Get the alias to use for this reference in the query
|
|
@@ -2170,9 +2402,9 @@ class ReferenceQueryBuilder {
|
|
|
2170
2402
|
createMockTargetRow() {
|
|
2171
2403
|
if (this.targetTableSchema) {
|
|
2172
2404
|
const mock = {};
|
|
2173
|
-
// Add columns
|
|
2174
|
-
|
|
2175
|
-
|
|
2405
|
+
// Add columns - use pre-computed column name map if available
|
|
2406
|
+
const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
2407
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
2176
2408
|
Object.defineProperty(mock, colName, {
|
|
2177
2409
|
get: () => ({
|
|
2178
2410
|
__fieldName: colName,
|
|
@@ -2186,14 +2418,20 @@ class ReferenceQueryBuilder {
|
|
|
2186
2418
|
// Add navigation properties (both collections and references)
|
|
2187
2419
|
if (this.targetTableSchema.relations) {
|
|
2188
2420
|
for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
|
|
2421
|
+
// Try to get target schema from registry (preferred, has full relations) or targetTableBuilder
|
|
2422
|
+
let nestedTargetSchema;
|
|
2423
|
+
if (this.schemaRegistry) {
|
|
2424
|
+
nestedTargetSchema = this.schemaRegistry.get(relConfig.targetTable);
|
|
2425
|
+
}
|
|
2426
|
+
if (!nestedTargetSchema && relConfig.targetTableBuilder) {
|
|
2427
|
+
nestedTargetSchema = relConfig.targetTableBuilder.build();
|
|
2428
|
+
}
|
|
2189
2429
|
if (relConfig.type === 'many') {
|
|
2190
2430
|
// Collection navigation
|
|
2191
2431
|
Object.defineProperty(mock, relName, {
|
|
2192
2432
|
get: () => {
|
|
2193
|
-
// Don't call build() - it returns schema without relations
|
|
2194
|
-
// Instead, pass undefined and let CollectionQueryBuilder look it up from registry
|
|
2195
2433
|
const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
|
|
2196
|
-
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable,
|
|
2434
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, nestedTargetSchema, // Pass the target schema directly
|
|
2197
2435
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
2198
2436
|
);
|
|
2199
2437
|
},
|
|
@@ -2205,9 +2443,7 @@ class ReferenceQueryBuilder {
|
|
|
2205
2443
|
// Reference navigation
|
|
2206
2444
|
Object.defineProperty(mock, relName, {
|
|
2207
2445
|
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
|
|
2446
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
|
|
2211
2447
|
this.schemaRegistry // Pass schema registry for nested resolution
|
|
2212
2448
|
);
|
|
2213
2449
|
return refBuilder.createMockTargetRow();
|
|
@@ -2221,15 +2457,8 @@ class ReferenceQueryBuilder {
|
|
|
2221
2457
|
return mock;
|
|
2222
2458
|
}
|
|
2223
2459
|
else {
|
|
2224
|
-
// Fallback:
|
|
2225
|
-
|
|
2226
|
-
get: (target, prop) => ({
|
|
2227
|
-
__fieldName: prop,
|
|
2228
|
-
__dbColumnName: prop,
|
|
2229
|
-
__tableAlias: this.relationName,
|
|
2230
|
-
}),
|
|
2231
|
-
};
|
|
2232
|
-
return new Proxy({}, handler);
|
|
2460
|
+
// Fallback: use the shared nested proxy that supports deep property access
|
|
2461
|
+
return createNestedFieldRefProxy(this.relationName);
|
|
2233
2462
|
}
|
|
2234
2463
|
}
|
|
2235
2464
|
}
|
|
@@ -2248,9 +2477,16 @@ class CollectionQueryBuilder {
|
|
|
2248
2477
|
this.foreignKey = foreignKey;
|
|
2249
2478
|
this.sourceTable = sourceTable;
|
|
2250
2479
|
this.schemaRegistry = schemaRegistry;
|
|
2251
|
-
//
|
|
2252
|
-
if (
|
|
2253
|
-
|
|
2480
|
+
// Prefer registry lookup (has full relations) over passed schema
|
|
2481
|
+
if (this.schemaRegistry) {
|
|
2482
|
+
const registrySchema = this.schemaRegistry.get(targetTable);
|
|
2483
|
+
if (registrySchema) {
|
|
2484
|
+
this.targetTableSchema = registrySchema;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
// Fallback to passed schema if registry lookup failed
|
|
2488
|
+
if (!this.targetTableSchema) {
|
|
2489
|
+
this.targetTableSchema = targetTableSchema;
|
|
2254
2490
|
}
|
|
2255
2491
|
}
|
|
2256
2492
|
/**
|
|
@@ -2277,11 +2513,18 @@ class CollectionQueryBuilder {
|
|
|
2277
2513
|
}
|
|
2278
2514
|
/**
|
|
2279
2515
|
* Filter collection items
|
|
2516
|
+
* Multiple where() calls are chained with AND logic
|
|
2280
2517
|
*/
|
|
2281
2518
|
where(condition) {
|
|
2282
2519
|
// Create mock item with proper schema if available
|
|
2283
2520
|
const mockItem = this.createMockItem();
|
|
2284
|
-
|
|
2521
|
+
const newCondition = condition(mockItem);
|
|
2522
|
+
if (this.whereCond) {
|
|
2523
|
+
this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
|
|
2524
|
+
}
|
|
2525
|
+
else {
|
|
2526
|
+
this.whereCond = newCondition;
|
|
2527
|
+
}
|
|
2285
2528
|
return this;
|
|
2286
2529
|
}
|
|
2287
2530
|
/**
|
|
@@ -2295,14 +2538,10 @@ class CollectionQueryBuilder {
|
|
|
2295
2538
|
if (this.targetTableSchema) {
|
|
2296
2539
|
// If we have schema information, create a properly typed mock
|
|
2297
2540
|
const mock = {};
|
|
2298
|
-
// Performance:
|
|
2299
|
-
const
|
|
2300
|
-
const columnConfigs = new Map();
|
|
2301
|
-
for (const [colName, colBuilder] of columnEntries) {
|
|
2302
|
-
columnConfigs.set(colName, colBuilder.build().name);
|
|
2303
|
-
}
|
|
2541
|
+
// Performance: Use pre-computed column name map if available
|
|
2542
|
+
const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
2304
2543
|
// Add columns
|
|
2305
|
-
for (const [colName, dbColumnName] of
|
|
2544
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
2306
2545
|
Object.defineProperty(mock, colName, {
|
|
2307
2546
|
get: () => ({
|
|
2308
2547
|
__fieldName: colName,
|
|
@@ -2351,14 +2590,8 @@ class CollectionQueryBuilder {
|
|
|
2351
2590
|
return mock;
|
|
2352
2591
|
}
|
|
2353
2592
|
else {
|
|
2354
|
-
// Fallback:
|
|
2355
|
-
|
|
2356
|
-
get: (target, prop) => ({
|
|
2357
|
-
__fieldName: prop,
|
|
2358
|
-
__dbColumnName: prop,
|
|
2359
|
-
}),
|
|
2360
|
-
};
|
|
2361
|
-
return new Proxy({}, handler);
|
|
2593
|
+
// Fallback: use the shared nested proxy that supports deep property access
|
|
2594
|
+
return createNestedFieldRefProxy(this.targetTable);
|
|
2362
2595
|
}
|
|
2363
2596
|
}
|
|
2364
2597
|
/**
|
|
@@ -2378,35 +2611,7 @@ class CollectionQueryBuilder {
|
|
|
2378
2611
|
orderBy(selector) {
|
|
2379
2612
|
const mockItem = this.createMockItem();
|
|
2380
2613
|
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
|
-
}
|
|
2614
|
+
(0, query_utils_1.parseOrderBy)(result, this.orderByFields);
|
|
2410
2615
|
return this;
|
|
2411
2616
|
}
|
|
2412
2617
|
/**
|
|
@@ -2524,14 +2729,61 @@ class CollectionQueryBuilder {
|
|
|
2524
2729
|
/**
|
|
2525
2730
|
* Build CTE for this collection query
|
|
2526
2731
|
* Now delegates to collection strategy pattern
|
|
2732
|
+
* Returns full CollectionAggregationResult for strategies that need special handling (like LATERAL)
|
|
2527
2733
|
*/
|
|
2528
2734
|
buildCTE(context, client, parentIds) {
|
|
2529
|
-
// Determine strategy type - default to '
|
|
2530
|
-
const strategyType = context.collectionStrategy || '
|
|
2735
|
+
// Determine strategy type - default to 'lateral' if not specified
|
|
2736
|
+
const strategyType = context.collectionStrategy || 'lateral';
|
|
2531
2737
|
const strategy = collection_strategy_factory_1.CollectionStrategyFactory.getStrategy(strategyType);
|
|
2532
|
-
// Build selected fields configuration
|
|
2738
|
+
// Build selected fields configuration (supports nested objects)
|
|
2533
2739
|
const selectedFieldConfigs = [];
|
|
2534
2740
|
const localParams = [];
|
|
2741
|
+
// Helper function to check if a value is a plain object (not FieldRef, SqlFragment, etc.)
|
|
2742
|
+
const isPlainObject = (val) => {
|
|
2743
|
+
return typeof val === 'object' &&
|
|
2744
|
+
val !== null &&
|
|
2745
|
+
!('__dbColumnName' in val) &&
|
|
2746
|
+
!(val instanceof conditions_1.SqlFragment) &&
|
|
2747
|
+
!Array.isArray(val) &&
|
|
2748
|
+
val.constructor === Object;
|
|
2749
|
+
};
|
|
2750
|
+
// Helper function to recursively process fields and build SelectedField structures
|
|
2751
|
+
const processField = (alias, field) => {
|
|
2752
|
+
if (field instanceof conditions_1.SqlFragment) {
|
|
2753
|
+
// SQL Fragment - build the SQL expression
|
|
2754
|
+
const sqlBuildContext = {
|
|
2755
|
+
paramCounter: context.paramCounter,
|
|
2756
|
+
params: context.allParams,
|
|
2757
|
+
};
|
|
2758
|
+
const fragmentSql = field.buildSql(sqlBuildContext);
|
|
2759
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
2760
|
+
return { alias, expression: fragmentSql };
|
|
2761
|
+
}
|
|
2762
|
+
else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
2763
|
+
// FieldRef object - use database column name
|
|
2764
|
+
const dbColumnName = field.__dbColumnName;
|
|
2765
|
+
return { alias, expression: `"${dbColumnName}"` };
|
|
2766
|
+
}
|
|
2767
|
+
else if (typeof field === 'string') {
|
|
2768
|
+
// Simple string reference (for backward compatibility)
|
|
2769
|
+
return { alias, expression: `"${field}"` };
|
|
2770
|
+
}
|
|
2771
|
+
else if (isPlainObject(field)) {
|
|
2772
|
+
// Nested object - recursively process its fields
|
|
2773
|
+
const nestedFields = [];
|
|
2774
|
+
for (const [nestedAlias, nestedField] of Object.entries(field)) {
|
|
2775
|
+
nestedFields.push(processField(nestedAlias, nestedField));
|
|
2776
|
+
}
|
|
2777
|
+
return { alias, nested: nestedFields };
|
|
2778
|
+
}
|
|
2779
|
+
else {
|
|
2780
|
+
// Literal value or expression
|
|
2781
|
+
const expression = `$${context.paramCounter++}`;
|
|
2782
|
+
context.allParams.push(field);
|
|
2783
|
+
localParams.push(field);
|
|
2784
|
+
return { alias, expression };
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2535
2787
|
// Step 1: Build field selection configuration
|
|
2536
2788
|
if (this.selector) {
|
|
2537
2789
|
const mockItem = this.createMockItem();
|
|
@@ -2547,54 +2799,31 @@ class CollectionQueryBuilder {
|
|
|
2547
2799
|
});
|
|
2548
2800
|
}
|
|
2549
2801
|
else {
|
|
2550
|
-
// Object selection - extract each field
|
|
2802
|
+
// Object selection - extract each field (with support for nested objects)
|
|
2551
2803
|
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
|
-
}
|
|
2804
|
+
selectedFieldConfigs.push(processField(alias, field));
|
|
2589
2805
|
}
|
|
2590
2806
|
}
|
|
2591
2807
|
}
|
|
2592
2808
|
else {
|
|
2593
|
-
// No selector - select all fields
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2809
|
+
// No selector - select all fields from the target table schema
|
|
2810
|
+
if (this.targetTableSchema && this.targetTableSchema.columns) {
|
|
2811
|
+
// Performance: Use cached column name map
|
|
2812
|
+
const colNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
2813
|
+
for (const [colName, dbColumnName] of colNameMap) {
|
|
2814
|
+
selectedFieldConfigs.push({
|
|
2815
|
+
alias: colName,
|
|
2816
|
+
expression: `"${dbColumnName}"`,
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
else {
|
|
2821
|
+
// Fallback: use * (less ideal, may cause issues)
|
|
2822
|
+
selectedFieldConfigs.push({
|
|
2823
|
+
alias: '*',
|
|
2824
|
+
expression: '*',
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2598
2827
|
}
|
|
2599
2828
|
// Step 2: Build WHERE clause SQL (without WHERE keyword)
|
|
2600
2829
|
let whereClause;
|
|
@@ -2611,13 +2840,11 @@ class CollectionQueryBuilder {
|
|
|
2611
2840
|
// Step 3: Build ORDER BY clause SQL (without ORDER BY keyword)
|
|
2612
2841
|
let orderByClause;
|
|
2613
2842
|
if (this.orderByFields.length > 0) {
|
|
2843
|
+
// Performance: Pre-compute column name map for ORDER BY lookups
|
|
2844
|
+
const colNameMap = this.targetTableSchema ? getColumnNameMapForSchema(this.targetTableSchema) : null;
|
|
2614
2845
|
const orderParts = this.orderByFields.map(({ field, direction }) => {
|
|
2615
|
-
// Look up the database column name from the
|
|
2616
|
-
|
|
2617
|
-
if (this.targetTableSchema && this.targetTableSchema.columns[field]) {
|
|
2618
|
-
const colBuilder = this.targetTableSchema.columns[field];
|
|
2619
|
-
dbColumnName = colBuilder.build().name;
|
|
2620
|
-
}
|
|
2846
|
+
// Look up the database column name from the cached map if available
|
|
2847
|
+
const dbColumnName = colNameMap?.get(field) ?? field;
|
|
2621
2848
|
return `"${dbColumnName}" ${direction}`;
|
|
2622
2849
|
});
|
|
2623
2850
|
orderByClause = orderParts.join(', ');
|
|
@@ -2687,10 +2914,14 @@ class CollectionQueryBuilder {
|
|
|
2687
2914
|
// Async strategy - return special marker with the promise
|
|
2688
2915
|
return result;
|
|
2689
2916
|
}
|
|
2690
|
-
// Synchronous strategy (JSONB/CTE)
|
|
2917
|
+
// Synchronous strategy (JSONB/CTE/LATERAL)
|
|
2691
2918
|
return {
|
|
2692
2919
|
sql: result.sql,
|
|
2693
2920
|
params: localParams,
|
|
2921
|
+
isCTE: result.isCTE,
|
|
2922
|
+
joinClause: result.joinClause,
|
|
2923
|
+
selectExpression: result.selectExpression,
|
|
2924
|
+
tableName: result.tableName,
|
|
2694
2925
|
};
|
|
2695
2926
|
}
|
|
2696
2927
|
}
|