linkgress-orm 0.0.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/LICENSE +21 -0
- package/README.md +196 -0
- package/dist/database/database-client.interface.d.ts +45 -0
- package/dist/database/database-client.interface.d.ts.map +1 -0
- package/dist/database/database-client.interface.js +20 -0
- package/dist/database/database-client.interface.js.map +1 -0
- package/dist/database/index.d.ts +5 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +10 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/pg-client.d.ts +30 -0
- package/dist/database/pg-client.d.ts.map +1 -0
- package/dist/database/pg-client.js +76 -0
- package/dist/database/pg-client.js.map +1 -0
- package/dist/database/postgres-client.d.ts +44 -0
- package/dist/database/postgres-client.d.ts.map +1 -0
- package/dist/database/postgres-client.js +111 -0
- package/dist/database/postgres-client.js.map +1 -0
- package/dist/database/types.d.ts +200 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +8 -0
- package/dist/database/types.js.map +1 -0
- package/dist/entity/base-entity.d.ts +21 -0
- package/dist/entity/base-entity.d.ts.map +1 -0
- package/dist/entity/base-entity.js +27 -0
- package/dist/entity/base-entity.js.map +1 -0
- package/dist/entity/db-column.d.ts +61 -0
- package/dist/entity/db-column.d.ts.map +1 -0
- package/dist/entity/db-column.js +35 -0
- package/dist/entity/db-column.js.map +1 -0
- package/dist/entity/db-context.d.ts +665 -0
- package/dist/entity/db-context.d.ts.map +1 -0
- package/dist/entity/db-context.js +1463 -0
- package/dist/entity/db-context.js.map +1 -0
- package/dist/entity/entity-base.d.ts +76 -0
- package/dist/entity/entity-base.d.ts.map +1 -0
- package/dist/entity/entity-base.js +42 -0
- package/dist/entity/entity-base.js.map +1 -0
- package/dist/entity/entity-builder.d.ts +171 -0
- package/dist/entity/entity-builder.d.ts.map +1 -0
- package/dist/entity/entity-builder.js +376 -0
- package/dist/entity/entity-builder.js.map +1 -0
- package/dist/entity/model-config.d.ts +18 -0
- package/dist/entity/model-config.d.ts.map +1 -0
- package/dist/entity/model-config.js +157 -0
- package/dist/entity/model-config.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/dist/migration/db-schema-manager.d.ts +228 -0
- package/dist/migration/db-schema-manager.d.ts.map +1 -0
- package/dist/migration/db-schema-manager.js +1055 -0
- package/dist/migration/db-schema-manager.js.map +1 -0
- package/dist/migration/enum-migrator.d.ts +29 -0
- package/dist/migration/enum-migrator.d.ts.map +1 -0
- package/dist/migration/enum-migrator.js +137 -0
- package/dist/migration/enum-migrator.js.map +1 -0
- package/dist/query/collection-strategy.factory.d.ts +16 -0
- package/dist/query/collection-strategy.factory.d.ts.map +1 -0
- package/dist/query/collection-strategy.factory.js +37 -0
- package/dist/query/collection-strategy.factory.js.map +1 -0
- package/dist/query/collection-strategy.interface.d.ts +146 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -0
- package/dist/query/collection-strategy.interface.js +3 -0
- package/dist/query/collection-strategy.interface.js.map +1 -0
- package/dist/query/conditions.d.ts +222 -0
- package/dist/query/conditions.d.ts.map +1 -0
- package/dist/query/conditions.js +446 -0
- package/dist/query/conditions.js.map +1 -0
- package/dist/query/cte-builder.d.ts +95 -0
- package/dist/query/cte-builder.d.ts.map +1 -0
- package/dist/query/cte-builder.js +172 -0
- package/dist/query/cte-builder.js.map +1 -0
- package/dist/query/grouped-query.d.ts +186 -0
- package/dist/query/grouped-query.d.ts.map +1 -0
- package/dist/query/grouped-query.js +588 -0
- package/dist/query/grouped-query.js.map +1 -0
- package/dist/query/join-builder.d.ts +106 -0
- package/dist/query/join-builder.d.ts.map +1 -0
- package/dist/query/join-builder.js +275 -0
- package/dist/query/join-builder.js.map +1 -0
- package/dist/query/query-builder.d.ts +543 -0
- package/dist/query/query-builder.d.ts.map +1 -0
- package/dist/query/query-builder.js +2649 -0
- package/dist/query/query-builder.js.map +1 -0
- package/dist/query/strategies/jsonb-collection-strategy.d.ts +51 -0
- package/dist/query/strategies/jsonb-collection-strategy.d.ts.map +1 -0
- package/dist/query/strategies/jsonb-collection-strategy.js +210 -0
- package/dist/query/strategies/jsonb-collection-strategy.js.map +1 -0
- package/dist/query/strategies/temptable-collection-strategy.d.ts +95 -0
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -0
- package/dist/query/strategies/temptable-collection-strategy.js +456 -0
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -0
- package/dist/query/subquery.d.ts +152 -0
- package/dist/query/subquery.d.ts.map +1 -0
- package/dist/query/subquery.js +206 -0
- package/dist/query/subquery.js.map +1 -0
- package/dist/schema/column-builder.d.ts +127 -0
- package/dist/schema/column-builder.d.ts.map +1 -0
- package/dist/schema/column-builder.js +184 -0
- package/dist/schema/column-builder.js.map +1 -0
- package/dist/schema/inference.d.ts +26 -0
- package/dist/schema/inference.d.ts.map +1 -0
- package/dist/schema/inference.js +3 -0
- package/dist/schema/inference.js.map +1 -0
- package/dist/schema/navigation.d.ts +215 -0
- package/dist/schema/navigation.d.ts.map +1 -0
- package/dist/schema/navigation.js +233 -0
- package/dist/schema/navigation.js.map +1 -0
- package/dist/schema/row-type.d.ts +26 -0
- package/dist/schema/row-type.d.ts.map +1 -0
- package/dist/schema/row-type.js +3 -0
- package/dist/schema/row-type.js.map +1 -0
- package/dist/schema/sequence-builder.d.ts +87 -0
- package/dist/schema/sequence-builder.d.ts.map +1 -0
- package/dist/schema/sequence-builder.js +123 -0
- package/dist/schema/sequence-builder.js.map +1 -0
- package/dist/schema/table-builder.d.ts +122 -0
- package/dist/schema/table-builder.d.ts.map +1 -0
- package/dist/schema/table-builder.js +132 -0
- package/dist/schema/table-builder.js.map +1 -0
- package/dist/schema/typed-schema.d.ts +22 -0
- package/dist/schema/typed-schema.d.ts.map +1 -0
- package/dist/schema/typed-schema.js +28 -0
- package/dist/schema/typed-schema.js.map +1 -0
- package/dist/types/column-types.d.ts +20 -0
- package/dist/types/column-types.d.ts.map +1 -0
- package/dist/types/column-types.js +14 -0
- package/dist/types/column-types.js.map +1 -0
- package/dist/types/custom-types.d.ts +85 -0
- package/dist/types/custom-types.d.ts.map +1 -0
- package/dist/types/custom-types.js +132 -0
- package/dist/types/custom-types.js.map +1 -0
- package/dist/types/enum-builder.d.ts +31 -0
- package/dist/types/enum-builder.d.ts.map +1 -0
- package/dist/types/enum-builder.js +46 -0
- package/dist/types/enum-builder.js.map +1 -0
- package/dist/types/metadata.d.ts +67 -0
- package/dist/types/metadata.d.ts.map +1 -0
- package/dist/types/metadata.js +57 -0
- package/dist/types/metadata.js.map +1 -0
- package/dist/types/type-mapper.d.ts +49 -0
- package/dist/types/type-mapper.d.ts.map +1 -0
- package/dist/types/type-mapper.js +49 -0
- package/dist/types/type-mapper.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,2649 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQueryBuilder = exports.QueryBuilder = void 0;
|
|
4
|
+
const conditions_1 = require("./conditions");
|
|
5
|
+
const subquery_1 = require("./subquery");
|
|
6
|
+
const grouped_query_1 = require("./grouped-query");
|
|
7
|
+
const cte_builder_1 = require("./cte-builder");
|
|
8
|
+
const collection_strategy_factory_1 = require("./collection-strategy.factory");
|
|
9
|
+
/**
|
|
10
|
+
* Query builder for a table
|
|
11
|
+
*/
|
|
12
|
+
class QueryBuilder {
|
|
13
|
+
constructor(schema, client, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, collectionStrategy) {
|
|
14
|
+
this.orderByFields = [];
|
|
15
|
+
this.manualJoins = [];
|
|
16
|
+
this.joinCounter = 0;
|
|
17
|
+
this.schema = schema;
|
|
18
|
+
this.client = client;
|
|
19
|
+
this.whereCond = whereCond;
|
|
20
|
+
this.limitValue = limit;
|
|
21
|
+
this.offsetValue = offset;
|
|
22
|
+
this.orderByFields = orderBy || [];
|
|
23
|
+
this.executor = executor;
|
|
24
|
+
this.manualJoins = manualJoins || [];
|
|
25
|
+
this.joinCounter = joinCounter || 0;
|
|
26
|
+
this.collectionStrategy = collectionStrategy;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get qualified table name with schema prefix if specified
|
|
30
|
+
*/
|
|
31
|
+
getQualifiedTableName(tableName, schema) {
|
|
32
|
+
return schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Define the selection with support for nested queries
|
|
36
|
+
*/
|
|
37
|
+
select(selector) {
|
|
38
|
+
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
|
|
39
|
+
undefined, // schemaRegistry
|
|
40
|
+
[], // ctes - start with empty array
|
|
41
|
+
this.collectionStrategy);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Add WHERE condition
|
|
45
|
+
*/
|
|
46
|
+
where(condition) {
|
|
47
|
+
const mockRow = this.createMockRow();
|
|
48
|
+
this.whereCond = condition(mockRow);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create mock row for analysis
|
|
53
|
+
*/
|
|
54
|
+
createMockRow() {
|
|
55
|
+
// Performance: Return cached mock if available
|
|
56
|
+
if (this._cachedMockRow) {
|
|
57
|
+
return this._cachedMockRow;
|
|
58
|
+
}
|
|
59
|
+
const mock = {};
|
|
60
|
+
// Performance: Build column configs once and cache them
|
|
61
|
+
const columnEntries = Object.entries(this.schema.columns);
|
|
62
|
+
const columnConfigs = new Map();
|
|
63
|
+
for (const [colName, colBuilder] of columnEntries) {
|
|
64
|
+
columnConfigs.set(colName, colBuilder.build().name);
|
|
65
|
+
}
|
|
66
|
+
// Add columns as FieldRef objects - type-safe with property name and database column name
|
|
67
|
+
for (const [colName, dbColumnName] of columnConfigs) {
|
|
68
|
+
Object.defineProperty(mock, colName, {
|
|
69
|
+
get: () => ({
|
|
70
|
+
__fieldName: colName,
|
|
71
|
+
__dbColumnName: dbColumnName,
|
|
72
|
+
__tableAlias: this.schema.name,
|
|
73
|
+
}),
|
|
74
|
+
enumerable: true,
|
|
75
|
+
configurable: true,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// Performance: Cache target schemas for relations to avoid repeated .build() calls
|
|
79
|
+
const relationSchemas = new Map();
|
|
80
|
+
for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
|
|
81
|
+
if (relConfig.targetTableBuilder) {
|
|
82
|
+
relationSchemas.set(relName, relConfig.targetTableBuilder.build());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Add relations (both collections and single references)
|
|
86
|
+
for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
|
|
87
|
+
if (relConfig.type === 'many') {
|
|
88
|
+
const targetSchema = relationSchemas.get(relName);
|
|
89
|
+
Object.defineProperty(mock, relName, {
|
|
90
|
+
get: () => {
|
|
91
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
|
|
92
|
+
},
|
|
93
|
+
enumerable: true,
|
|
94
|
+
configurable: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Single reference navigation (many-to-one, one-to-one)
|
|
99
|
+
const targetSchema = relationSchemas.get(relName);
|
|
100
|
+
Object.defineProperty(mock, relName, {
|
|
101
|
+
get: () => {
|
|
102
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
103
|
+
return refBuilder.createMockTargetRow();
|
|
104
|
+
},
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Cache the mock for reuse
|
|
111
|
+
this._cachedMockRow = mock;
|
|
112
|
+
return mock;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Add a LEFT JOIN to the query with a selector (supports both tables and subqueries)
|
|
116
|
+
*/
|
|
117
|
+
leftJoin(rightTable, condition, selector, alias) {
|
|
118
|
+
// Check if rightTable is a Subquery
|
|
119
|
+
if (rightTable instanceof subquery_1.Subquery) {
|
|
120
|
+
if (!alias) {
|
|
121
|
+
throw new Error('Alias is required when joining a subquery');
|
|
122
|
+
}
|
|
123
|
+
// Delegate to SelectQueryBuilder which handles subquery joins
|
|
124
|
+
const qb = new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, // isDistinct defaults to false
|
|
125
|
+
undefined, // schemaRegistry
|
|
126
|
+
[], // ctes
|
|
127
|
+
this.collectionStrategy);
|
|
128
|
+
return qb.leftJoinSubquery(rightTable, alias, condition, selector);
|
|
129
|
+
}
|
|
130
|
+
const rightSchema = rightTable._getSchema();
|
|
131
|
+
// Generate unique alias using join counter
|
|
132
|
+
const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
|
|
133
|
+
const newJoinCounter = this.joinCounter + 1;
|
|
134
|
+
// Create mock rows for condition evaluation
|
|
135
|
+
const mockLeft = this.createMockRow();
|
|
136
|
+
const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
|
|
137
|
+
const joinCondition = condition(mockLeft, mockRight);
|
|
138
|
+
// Add the join to the list
|
|
139
|
+
const updatedJoins = [...this.manualJoins, {
|
|
140
|
+
type: 'LEFT',
|
|
141
|
+
table: rightSchema.name,
|
|
142
|
+
alias: rightAlias,
|
|
143
|
+
schema: rightSchema,
|
|
144
|
+
condition: joinCondition,
|
|
145
|
+
}];
|
|
146
|
+
// Store schemas for creating fresh mocks in the selector
|
|
147
|
+
const leftSchema = this.schema;
|
|
148
|
+
const createLeftMock = () => this.createMockRow();
|
|
149
|
+
const createRightMock = () => this.createMockRowForTable(rightSchema, rightAlias);
|
|
150
|
+
// Create a selector wrapper that generates fresh mocks and calls the user's selector
|
|
151
|
+
const wrappedSelector = (row) => {
|
|
152
|
+
// Create fresh mocks for the selector invocation
|
|
153
|
+
const freshMockLeft = createLeftMock();
|
|
154
|
+
const freshMockRight = createRightMock();
|
|
155
|
+
return selector(freshMockLeft, freshMockRight);
|
|
156
|
+
};
|
|
157
|
+
return new SelectQueryBuilder(this.schema, this.client, wrappedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, false, // isDistinct defaults to false
|
|
158
|
+
undefined, // schemaRegistry
|
|
159
|
+
[], // ctes
|
|
160
|
+
this.collectionStrategy);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Add an INNER JOIN to the query with a selector (supports both tables and subqueries)
|
|
164
|
+
*/
|
|
165
|
+
innerJoin(rightTable, condition, selector, alias) {
|
|
166
|
+
// Check if rightTable is a Subquery
|
|
167
|
+
if (rightTable instanceof subquery_1.Subquery) {
|
|
168
|
+
if (!alias) {
|
|
169
|
+
throw new Error('Alias is required when joining a subquery');
|
|
170
|
+
}
|
|
171
|
+
// Delegate to SelectQueryBuilder which handles subquery joins
|
|
172
|
+
const qb = new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, // isDistinct defaults to false
|
|
173
|
+
undefined, // schemaRegistry
|
|
174
|
+
[], // ctes
|
|
175
|
+
this.collectionStrategy);
|
|
176
|
+
return qb.innerJoinSubquery(rightTable, alias, condition, selector);
|
|
177
|
+
}
|
|
178
|
+
const rightSchema = rightTable._getSchema();
|
|
179
|
+
// Generate unique alias using join counter
|
|
180
|
+
const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
|
|
181
|
+
const newJoinCounter = this.joinCounter + 1;
|
|
182
|
+
// Create mock rows for condition evaluation
|
|
183
|
+
const mockLeft = this.createMockRow();
|
|
184
|
+
const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
|
|
185
|
+
const joinCondition = condition(mockLeft, mockRight);
|
|
186
|
+
// Add the join to the list
|
|
187
|
+
const updatedJoins = [...this.manualJoins, {
|
|
188
|
+
type: 'INNER',
|
|
189
|
+
table: rightSchema.name,
|
|
190
|
+
alias: rightAlias,
|
|
191
|
+
schema: rightSchema,
|
|
192
|
+
condition: joinCondition,
|
|
193
|
+
}];
|
|
194
|
+
// Store schemas for creating fresh mocks in the selector
|
|
195
|
+
const leftSchema = this.schema;
|
|
196
|
+
const createLeftMock = () => this.createMockRow();
|
|
197
|
+
const createRightMock = () => this.createMockRowForTable(rightSchema, rightAlias);
|
|
198
|
+
// Create a selector wrapper that generates fresh mocks and calls the user's selector
|
|
199
|
+
const wrappedSelector = (row) => {
|
|
200
|
+
// Create fresh mocks for the selector invocation
|
|
201
|
+
const freshMockLeft = createLeftMock();
|
|
202
|
+
const freshMockRight = createRightMock();
|
|
203
|
+
return selector(freshMockLeft, freshMockRight);
|
|
204
|
+
};
|
|
205
|
+
return new SelectQueryBuilder(this.schema, this.client, wrappedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, false, // isDistinct defaults to false
|
|
206
|
+
undefined, // schemaRegistry
|
|
207
|
+
[], // ctes
|
|
208
|
+
this.collectionStrategy);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Create mock row for a specific table/alias (for joins)
|
|
212
|
+
*/
|
|
213
|
+
createMockRowForTable(schema, alias) {
|
|
214
|
+
const mock = {};
|
|
215
|
+
// Performance: Build column configs once and cache them
|
|
216
|
+
const columnEntries = Object.entries(schema.columns);
|
|
217
|
+
const columnConfigs = new Map();
|
|
218
|
+
for (const [colName, colBuilder] of columnEntries) {
|
|
219
|
+
columnConfigs.set(colName, colBuilder.build().name);
|
|
220
|
+
}
|
|
221
|
+
// Add columns as FieldRef objects with table alias
|
|
222
|
+
for (const [colName, dbColumnName] of columnConfigs) {
|
|
223
|
+
Object.defineProperty(mock, colName, {
|
|
224
|
+
get: () => ({
|
|
225
|
+
__fieldName: colName,
|
|
226
|
+
__dbColumnName: dbColumnName,
|
|
227
|
+
__tableAlias: alias,
|
|
228
|
+
}),
|
|
229
|
+
enumerable: true,
|
|
230
|
+
configurable: true,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// Performance: Cache target schemas for relations
|
|
234
|
+
const relationSchemas = new Map();
|
|
235
|
+
for (const [relName, relConfig] of Object.entries(schema.relations)) {
|
|
236
|
+
if (relConfig.targetTableBuilder) {
|
|
237
|
+
relationSchemas.set(relName, relConfig.targetTableBuilder.build());
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Add navigation properties (single references and collections)
|
|
241
|
+
for (const [relName, relConfig] of Object.entries(schema.relations)) {
|
|
242
|
+
if (relConfig.type === 'many') {
|
|
243
|
+
// Collection navigation
|
|
244
|
+
const targetSchema = relationSchemas.get(relName);
|
|
245
|
+
Object.defineProperty(mock, relName, {
|
|
246
|
+
get: () => {
|
|
247
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
|
|
248
|
+
},
|
|
249
|
+
enumerable: true,
|
|
250
|
+
configurable: true,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Single reference navigation
|
|
255
|
+
const targetSchema = relationSchemas.get(relName);
|
|
256
|
+
Object.defineProperty(mock, relName, {
|
|
257
|
+
get: () => {
|
|
258
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
259
|
+
return refBuilder.createMockTargetRow();
|
|
260
|
+
},
|
|
261
|
+
enumerable: true,
|
|
262
|
+
configurable: true,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return mock;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Limit results
|
|
270
|
+
*/
|
|
271
|
+
limit(count) {
|
|
272
|
+
this.limitValue = count;
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Offset results
|
|
277
|
+
*/
|
|
278
|
+
offset(count) {
|
|
279
|
+
this.offsetValue = count;
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
orderBy(selector) {
|
|
283
|
+
const mockRow = this.createMockRow();
|
|
284
|
+
const result = selector(mockRow);
|
|
285
|
+
// Handle array of [field, direction] tuples
|
|
286
|
+
if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
|
|
287
|
+
for (const [fieldRef, direction] of result) {
|
|
288
|
+
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
289
|
+
this.orderByFields.push({
|
|
290
|
+
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
291
|
+
direction: direction || 'ASC'
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Handle array of fields (all ASC)
|
|
297
|
+
else if (Array.isArray(result)) {
|
|
298
|
+
for (const fieldRef of result) {
|
|
299
|
+
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
300
|
+
this.orderByFields.push({
|
|
301
|
+
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
302
|
+
direction: 'ASC'
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Handle single field
|
|
308
|
+
else if (result && typeof result === 'object' && '__fieldName' in result) {
|
|
309
|
+
this.orderByFields.push({
|
|
310
|
+
field: result.__dbColumnName || result.__fieldName,
|
|
311
|
+
direction: 'ASC'
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
exports.QueryBuilder = QueryBuilder;
|
|
318
|
+
/**
|
|
319
|
+
* Select query builder with nested collection support
|
|
320
|
+
*/
|
|
321
|
+
class SelectQueryBuilder {
|
|
322
|
+
/**
|
|
323
|
+
* Get qualified table name with schema prefix if specified
|
|
324
|
+
*/
|
|
325
|
+
getQualifiedTableName(tableName, schema) {
|
|
326
|
+
return schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
327
|
+
}
|
|
328
|
+
constructor(schema, client, selector, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, isDistinct, schemaRegistry, ctes, collectionStrategy) {
|
|
329
|
+
this.orderByFields = [];
|
|
330
|
+
this.manualJoins = [];
|
|
331
|
+
this.joinCounter = 0;
|
|
332
|
+
this.isDistinct = false;
|
|
333
|
+
this.ctes = []; // Track CTEs attached to this query
|
|
334
|
+
this.schema = schema;
|
|
335
|
+
this.client = client;
|
|
336
|
+
this.selector = selector;
|
|
337
|
+
this.whereCond = whereCond;
|
|
338
|
+
this.limitValue = limit;
|
|
339
|
+
this.offsetValue = offset;
|
|
340
|
+
this.orderByFields = orderBy || [];
|
|
341
|
+
this.executor = executor;
|
|
342
|
+
this.manualJoins = manualJoins || [];
|
|
343
|
+
this.joinCounter = joinCounter || 0;
|
|
344
|
+
this.isDistinct = isDistinct || false;
|
|
345
|
+
this.schemaRegistry = schemaRegistry;
|
|
346
|
+
this.ctes = ctes || [];
|
|
347
|
+
this.collectionStrategy = collectionStrategy;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Transform the selection with a new selector
|
|
351
|
+
*/
|
|
352
|
+
select(selector) {
|
|
353
|
+
// Create a composed selector that applies both transformations
|
|
354
|
+
const composedSelector = (row) => {
|
|
355
|
+
const firstResult = this.selector(row);
|
|
356
|
+
return selector(firstResult);
|
|
357
|
+
};
|
|
358
|
+
return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Add WHERE condition
|
|
362
|
+
* Note: The row parameter represents the selected shape (after select())
|
|
363
|
+
*/
|
|
364
|
+
where(condition) {
|
|
365
|
+
const mockRow = this.createMockRow();
|
|
366
|
+
// Apply the selector to get the selected shape that the user sees in the WHERE condition
|
|
367
|
+
const selectedMock = this.selector(mockRow);
|
|
368
|
+
this.whereCond = condition(selectedMock);
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Attach one or more CTEs to this query
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* const result = await db.users
|
|
376
|
+
* .where(u => eq(u.id, 1))
|
|
377
|
+
* .with(activeUsersCte.cte)
|
|
378
|
+
* .leftJoin(activeUsersCte.cte, ...)
|
|
379
|
+
* .toList();
|
|
380
|
+
*/
|
|
381
|
+
with(...ctes) {
|
|
382
|
+
this.ctes.push(...ctes);
|
|
383
|
+
return this;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Limit results
|
|
387
|
+
*/
|
|
388
|
+
limit(count) {
|
|
389
|
+
this.limitValue = count;
|
|
390
|
+
return this;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Offset results
|
|
394
|
+
*/
|
|
395
|
+
offset(count) {
|
|
396
|
+
this.offsetValue = count;
|
|
397
|
+
return this;
|
|
398
|
+
}
|
|
399
|
+
orderBy(selector) {
|
|
400
|
+
const mockRow = this.createMockRow();
|
|
401
|
+
const selectedMock = this.selector(mockRow);
|
|
402
|
+
const result = selector(selectedMock);
|
|
403
|
+
// Handle array of [field, direction] tuples
|
|
404
|
+
if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
|
|
405
|
+
for (const [fieldRef, direction] of result) {
|
|
406
|
+
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
407
|
+
this.orderByFields.push({
|
|
408
|
+
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
409
|
+
direction: direction || 'ASC'
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Handle array of fields (all ASC)
|
|
415
|
+
else if (Array.isArray(result)) {
|
|
416
|
+
for (const fieldRef of result) {
|
|
417
|
+
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
418
|
+
this.orderByFields.push({
|
|
419
|
+
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
420
|
+
direction: 'ASC'
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Handle single field
|
|
426
|
+
else if (result && typeof result === 'object' && '__fieldName' in result) {
|
|
427
|
+
this.orderByFields.push({
|
|
428
|
+
field: result.__dbColumnName || result.__fieldName,
|
|
429
|
+
direction: 'ASC'
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return this;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Group by fields - returns a GroupedQueryBuilder for type-safe aggregations
|
|
436
|
+
* @param selector Function that selects the grouping key from the current selection
|
|
437
|
+
* @example
|
|
438
|
+
* db.users
|
|
439
|
+
* .select(u => ({ id: u.id, street: u.address.street, name: u.name }))
|
|
440
|
+
* .groupBy(p => ({ street: p.street }))
|
|
441
|
+
* .select(g => ({ street: g.key.street, count: g.count() }))
|
|
442
|
+
*/
|
|
443
|
+
groupBy(selector) {
|
|
444
|
+
return new grouped_query_1.GroupedQueryBuilder(this.schema, this.client, this.selector, selector, this.whereCond, this.executor, this.manualJoins, this.joinCounter);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Add a LEFT JOIN with a subquery
|
|
448
|
+
* @param subquery The subquery to join (must be 'table' mode)
|
|
449
|
+
* @param alias Alias for the subquery in the FROM clause
|
|
450
|
+
* @param condition Join condition
|
|
451
|
+
* @param selector Result selector
|
|
452
|
+
*/
|
|
453
|
+
leftJoinSubquery(subquery, alias, condition, selector) {
|
|
454
|
+
const newJoinCounter = this.joinCounter + 1;
|
|
455
|
+
// Create mock for the current selection (left side)
|
|
456
|
+
const mockRow = this.createMockRow();
|
|
457
|
+
const mockLeftSelection = this.selector(mockRow);
|
|
458
|
+
// Create mock for the subquery result (right side)
|
|
459
|
+
// For subqueries, we create a mock based on the result type
|
|
460
|
+
const mockRight = this.createMockRowForSubquery(alias, subquery);
|
|
461
|
+
// Evaluate the join condition
|
|
462
|
+
const joinCondition = condition(mockLeftSelection, mockRight);
|
|
463
|
+
// Store the subquery join info
|
|
464
|
+
const updatedJoins = [...this.manualJoins, {
|
|
465
|
+
type: 'LEFT',
|
|
466
|
+
table: `(${subquery.buildSql({ paramCounter: 0, params: [] })})`, // This will be rebuilt properly
|
|
467
|
+
alias: alias,
|
|
468
|
+
schema: null, // Subqueries don't have schema
|
|
469
|
+
condition: joinCondition,
|
|
470
|
+
isSubquery: true,
|
|
471
|
+
subquery: subquery,
|
|
472
|
+
}];
|
|
473
|
+
// Create a new selector
|
|
474
|
+
const composedSelector = (row) => {
|
|
475
|
+
const leftResult = this.selector(row);
|
|
476
|
+
const freshMockRight = this.createMockRowForSubquery(alias, subquery);
|
|
477
|
+
return selector(leftResult, freshMockRight);
|
|
478
|
+
};
|
|
479
|
+
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);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Add a LEFT JOIN to the query with a selector
|
|
483
|
+
* Note: After select(), the left parameter in the join will be the selected shape (TSelection)
|
|
484
|
+
*/
|
|
485
|
+
leftJoin(rightTable, condition, selector, alias) {
|
|
486
|
+
// Check if rightTable is a CTE
|
|
487
|
+
if ((0, cte_builder_1.isCte)(rightTable)) {
|
|
488
|
+
return this.leftJoinCte(rightTable, condition, selector);
|
|
489
|
+
}
|
|
490
|
+
// Check if rightTable is a Subquery
|
|
491
|
+
if (rightTable instanceof subquery_1.Subquery) {
|
|
492
|
+
if (!alias) {
|
|
493
|
+
throw new Error('Alias is required when joining a subquery');
|
|
494
|
+
}
|
|
495
|
+
return this.leftJoinSubquery(rightTable, alias, condition, selector);
|
|
496
|
+
}
|
|
497
|
+
const rightSchema = rightTable._getSchema();
|
|
498
|
+
// Generate unique alias using join counter
|
|
499
|
+
const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
|
|
500
|
+
const newJoinCounter = this.joinCounter + 1;
|
|
501
|
+
// Create mock for the current selection (left side)
|
|
502
|
+
// IMPORTANT: We call the selector with the mock row to get a result that contains FieldRef objects
|
|
503
|
+
const mockRow = this.createMockRow();
|
|
504
|
+
const mockLeftSelection = this.selector(mockRow);
|
|
505
|
+
// The mockLeftSelection now contains FieldRef objects (with __fieldName, __dbColumnName, __tableAlias)
|
|
506
|
+
// These FieldRef objects preserve the table context
|
|
507
|
+
// Create mock for the right table
|
|
508
|
+
const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
|
|
509
|
+
// Evaluate the join condition - the mockLeftSelection has FieldRef objects,
|
|
510
|
+
// so the condition can properly reference table aliases
|
|
511
|
+
const joinCondition = condition(mockLeftSelection, mockRight);
|
|
512
|
+
// Add the join to the list
|
|
513
|
+
const updatedJoins = [...this.manualJoins, {
|
|
514
|
+
type: 'LEFT',
|
|
515
|
+
table: rightSchema.name,
|
|
516
|
+
alias: rightAlias,
|
|
517
|
+
schema: rightSchema,
|
|
518
|
+
condition: joinCondition,
|
|
519
|
+
}];
|
|
520
|
+
// Create a new selector that first applies the current selector, then the new selector
|
|
521
|
+
const composedSelector = (row) => {
|
|
522
|
+
const leftResult = this.selector(row);
|
|
523
|
+
const freshMockRight = this.createMockRowForTable(rightSchema, rightAlias);
|
|
524
|
+
return selector(leftResult, freshMockRight);
|
|
525
|
+
};
|
|
526
|
+
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);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Add a LEFT JOIN with a CTE
|
|
530
|
+
*/
|
|
531
|
+
leftJoinCte(cte, condition, selector) {
|
|
532
|
+
const newJoinCounter = this.joinCounter + 1;
|
|
533
|
+
// Create mock for the current selection (left side)
|
|
534
|
+
const mockRow = this.createMockRow();
|
|
535
|
+
const mockLeftSelection = this.selector(mockRow);
|
|
536
|
+
// Create mock for the CTE columns (right side)
|
|
537
|
+
const mockRight = this.createMockRowForCte(cte);
|
|
538
|
+
// Evaluate the join condition
|
|
539
|
+
const joinCondition = condition(mockLeftSelection, mockRight);
|
|
540
|
+
// Add the CTE join
|
|
541
|
+
const updatedJoins = [...this.manualJoins, {
|
|
542
|
+
type: 'LEFT',
|
|
543
|
+
table: cte.name,
|
|
544
|
+
alias: cte.name,
|
|
545
|
+
schema: null,
|
|
546
|
+
condition: joinCondition,
|
|
547
|
+
cte: cte,
|
|
548
|
+
}];
|
|
549
|
+
// Create a new selector
|
|
550
|
+
const composedSelector = (row) => {
|
|
551
|
+
const leftResult = this.selector(row);
|
|
552
|
+
const freshMockRight = this.createMockRowForCte(cte);
|
|
553
|
+
return selector(leftResult, freshMockRight);
|
|
554
|
+
};
|
|
555
|
+
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);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Add an INNER JOIN with a subquery
|
|
559
|
+
* @param subquery The subquery to join (must be 'table' mode)
|
|
560
|
+
* @param alias Alias for the subquery in the FROM clause
|
|
561
|
+
* @param condition Join condition
|
|
562
|
+
* @param selector Result selector
|
|
563
|
+
*/
|
|
564
|
+
innerJoinSubquery(subquery, alias, condition, selector) {
|
|
565
|
+
const newJoinCounter = this.joinCounter + 1;
|
|
566
|
+
// Create mock for the current selection (left side)
|
|
567
|
+
const mockRow = this.createMockRow();
|
|
568
|
+
const mockLeftSelection = this.selector(mockRow);
|
|
569
|
+
// Create mock for the subquery result (right side)
|
|
570
|
+
const mockRight = this.createMockRowForSubquery(alias, subquery);
|
|
571
|
+
// Evaluate the join condition
|
|
572
|
+
const joinCondition = condition(mockLeftSelection, mockRight);
|
|
573
|
+
// Store the subquery join info
|
|
574
|
+
const updatedJoins = [...this.manualJoins, {
|
|
575
|
+
type: 'INNER',
|
|
576
|
+
table: `(${subquery.buildSql({ paramCounter: 0, params: [] })})`,
|
|
577
|
+
alias: alias,
|
|
578
|
+
schema: null,
|
|
579
|
+
condition: joinCondition,
|
|
580
|
+
isSubquery: true,
|
|
581
|
+
subquery: subquery,
|
|
582
|
+
}];
|
|
583
|
+
// Create a new selector
|
|
584
|
+
const composedSelector = (row) => {
|
|
585
|
+
const leftResult = this.selector(row);
|
|
586
|
+
const freshMockRight = this.createMockRowForSubquery(alias, subquery);
|
|
587
|
+
return selector(leftResult, freshMockRight);
|
|
588
|
+
};
|
|
589
|
+
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);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Add an INNER JOIN to the query with a selector
|
|
593
|
+
* Note: After select(), the left parameter in the join will be the selected shape (TSelection)
|
|
594
|
+
*/
|
|
595
|
+
innerJoin(rightTable, condition, selector, alias) {
|
|
596
|
+
// Check if rightTable is a Subquery
|
|
597
|
+
if (rightTable instanceof subquery_1.Subquery) {
|
|
598
|
+
if (!alias) {
|
|
599
|
+
throw new Error('Alias is required when joining a subquery');
|
|
600
|
+
}
|
|
601
|
+
return this.innerJoinSubquery(rightTable, alias, condition, selector);
|
|
602
|
+
}
|
|
603
|
+
const rightSchema = rightTable._getSchema();
|
|
604
|
+
// Generate unique alias using join counter
|
|
605
|
+
const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
|
|
606
|
+
const newJoinCounter = this.joinCounter + 1;
|
|
607
|
+
// Create mock for the current selection (left side)
|
|
608
|
+
const mockRow = this.createMockRow();
|
|
609
|
+
const mockLeftSelection = this.selector(mockRow);
|
|
610
|
+
// Create mock for the right table
|
|
611
|
+
const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
|
|
612
|
+
// Evaluate the join condition
|
|
613
|
+
const joinCondition = condition(mockLeftSelection, mockRight);
|
|
614
|
+
// Add the join to the list
|
|
615
|
+
const updatedJoins = [...this.manualJoins, {
|
|
616
|
+
type: 'INNER',
|
|
617
|
+
table: rightSchema.name,
|
|
618
|
+
alias: rightAlias,
|
|
619
|
+
schema: rightSchema,
|
|
620
|
+
condition: joinCondition,
|
|
621
|
+
}];
|
|
622
|
+
// Create a new selector that first applies the current selector, then the new selector
|
|
623
|
+
const composedSelector = (row) => {
|
|
624
|
+
const leftResult = this.selector(row);
|
|
625
|
+
const freshMockRight = this.createMockRowForTable(rightSchema, rightAlias);
|
|
626
|
+
return selector(leftResult, freshMockRight);
|
|
627
|
+
};
|
|
628
|
+
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);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Create mock row for a specific table/alias (for joins)
|
|
632
|
+
*/
|
|
633
|
+
createMockRowForTable(schema, alias) {
|
|
634
|
+
const mock = {};
|
|
635
|
+
// Performance: Build column configs once and cache them
|
|
636
|
+
const columnEntries = Object.entries(schema.columns);
|
|
637
|
+
const columnConfigs = new Map();
|
|
638
|
+
for (const [colName, colBuilder] of columnEntries) {
|
|
639
|
+
columnConfigs.set(colName, colBuilder.build().name);
|
|
640
|
+
}
|
|
641
|
+
// Add columns as FieldRef objects with table alias
|
|
642
|
+
for (const [colName, dbColumnName] of columnConfigs) {
|
|
643
|
+
Object.defineProperty(mock, colName, {
|
|
644
|
+
get: () => ({
|
|
645
|
+
__fieldName: colName,
|
|
646
|
+
__dbColumnName: dbColumnName,
|
|
647
|
+
__tableAlias: alias,
|
|
648
|
+
}),
|
|
649
|
+
enumerable: true,
|
|
650
|
+
configurable: true,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
// Performance: Cache target schemas for relations
|
|
654
|
+
const relationSchemas = new Map();
|
|
655
|
+
for (const [relName, relConfig] of Object.entries(schema.relations)) {
|
|
656
|
+
if (relConfig.targetTableBuilder) {
|
|
657
|
+
relationSchemas.set(relName, relConfig.targetTableBuilder.build());
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Add navigation properties (single references and collections)
|
|
661
|
+
for (const [relName, relConfig] of Object.entries(schema.relations)) {
|
|
662
|
+
if (relConfig.type === 'many') {
|
|
663
|
+
// Collection navigation
|
|
664
|
+
const targetSchema = relationSchemas.get(relName);
|
|
665
|
+
Object.defineProperty(mock, relName, {
|
|
666
|
+
get: () => {
|
|
667
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
|
|
668
|
+
},
|
|
669
|
+
enumerable: true,
|
|
670
|
+
configurable: true,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Single reference navigation
|
|
675
|
+
const targetSchema = relationSchemas.get(relName);
|
|
676
|
+
Object.defineProperty(mock, relName, {
|
|
677
|
+
get: () => {
|
|
678
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
|
|
679
|
+
return refBuilder.createMockTargetRow();
|
|
680
|
+
},
|
|
681
|
+
enumerable: true,
|
|
682
|
+
configurable: true,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return mock;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Create mock row for a subquery result (for subquery joins)
|
|
690
|
+
* The subquery result type defines the shape - we create FieldRefs for each property
|
|
691
|
+
*/
|
|
692
|
+
createMockRowForSubquery(alias, subquery) {
|
|
693
|
+
const mock = {};
|
|
694
|
+
// Get selection metadata from subquery if available
|
|
695
|
+
const selectionMetadata = subquery?.getSelectionMetadata();
|
|
696
|
+
// We need to infer the structure from TSubqueryResult
|
|
697
|
+
// Since we can't iterate over a type at runtime, we create a proxy that
|
|
698
|
+
// returns FieldRefs for any property access
|
|
699
|
+
return new Proxy(mock, {
|
|
700
|
+
get(target, prop) {
|
|
701
|
+
if (typeof prop === 'symbol')
|
|
702
|
+
return undefined;
|
|
703
|
+
// If we have selection metadata, check if this property has a mapper
|
|
704
|
+
if (selectionMetadata && prop in selectionMetadata) {
|
|
705
|
+
const value = selectionMetadata[prop];
|
|
706
|
+
// If it's a SqlFragment with a mapper, preserve it
|
|
707
|
+
if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
708
|
+
// Create a SqlFragment-like object that preserves the mapper
|
|
709
|
+
return {
|
|
710
|
+
__fieldName: prop,
|
|
711
|
+
__dbColumnName: prop,
|
|
712
|
+
__tableAlias: alias,
|
|
713
|
+
getMapper: () => value.getMapper(),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Return a regular FieldRef for any property accessed
|
|
718
|
+
return {
|
|
719
|
+
__fieldName: prop,
|
|
720
|
+
__dbColumnName: prop, // Assume property name matches column name
|
|
721
|
+
__tableAlias: alias,
|
|
722
|
+
};
|
|
723
|
+
},
|
|
724
|
+
has(target, prop) {
|
|
725
|
+
return true; // All properties "exist"
|
|
726
|
+
},
|
|
727
|
+
ownKeys(target) {
|
|
728
|
+
return []; // We don't know the keys ahead of time
|
|
729
|
+
},
|
|
730
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
731
|
+
return {
|
|
732
|
+
enumerable: true,
|
|
733
|
+
configurable: true,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Create a mock row for CTE columns
|
|
740
|
+
*/
|
|
741
|
+
createMockRowForCte(cte) {
|
|
742
|
+
const mock = {};
|
|
743
|
+
// Create a proxy that returns FieldRefs for CTE columns
|
|
744
|
+
return new Proxy(mock, {
|
|
745
|
+
get(target, prop) {
|
|
746
|
+
if (typeof prop === 'symbol')
|
|
747
|
+
return undefined;
|
|
748
|
+
// If we have selection metadata, check if this property has a mapper
|
|
749
|
+
if (cte.selectionMetadata && prop in cte.selectionMetadata) {
|
|
750
|
+
const value = cte.selectionMetadata[prop];
|
|
751
|
+
// If it's a SqlFragment with a mapper, preserve it
|
|
752
|
+
if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
753
|
+
// Create a SqlFragment-like object that preserves the mapper
|
|
754
|
+
return {
|
|
755
|
+
__fieldName: prop,
|
|
756
|
+
__dbColumnName: prop,
|
|
757
|
+
__tableAlias: cte.name,
|
|
758
|
+
getMapper: () => value.getMapper(),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
// Return a regular FieldRef for any property accessed
|
|
763
|
+
return {
|
|
764
|
+
__fieldName: prop,
|
|
765
|
+
__dbColumnName: prop,
|
|
766
|
+
__tableAlias: cte.name,
|
|
767
|
+
};
|
|
768
|
+
},
|
|
769
|
+
has(target, prop) {
|
|
770
|
+
return true;
|
|
771
|
+
},
|
|
772
|
+
ownKeys(target) {
|
|
773
|
+
return cte.columnDefs ? Object.keys(cte.columnDefs) : [];
|
|
774
|
+
},
|
|
775
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
776
|
+
return {
|
|
777
|
+
enumerable: true,
|
|
778
|
+
configurable: true,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Select distinct rows
|
|
785
|
+
*/
|
|
786
|
+
selectDistinct(selector) {
|
|
787
|
+
const composedSelector = (row) => {
|
|
788
|
+
const firstResult = this.selector(row);
|
|
789
|
+
return selector(firstResult);
|
|
790
|
+
};
|
|
791
|
+
return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, true, // Set isDistinct to true
|
|
792
|
+
this.schemaRegistry, this.ctes, this.collectionStrategy);
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Get minimum value from the query
|
|
796
|
+
*/
|
|
797
|
+
async min(selector) {
|
|
798
|
+
const context = {
|
|
799
|
+
ctes: new Map(),
|
|
800
|
+
cteCounter: 0,
|
|
801
|
+
paramCounter: 1,
|
|
802
|
+
allParams: [],
|
|
803
|
+
executor: this.executor,
|
|
804
|
+
};
|
|
805
|
+
// If selector is provided, apply it to determine the field
|
|
806
|
+
let fieldToAggregate;
|
|
807
|
+
if (selector) {
|
|
808
|
+
const mockRow = this.createMockRow();
|
|
809
|
+
const mockSelection = this.selector(mockRow);
|
|
810
|
+
fieldToAggregate = selector(mockSelection);
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
// No selector - use the current selection
|
|
814
|
+
const mockRow = this.createMockRow();
|
|
815
|
+
fieldToAggregate = this.selector(mockRow);
|
|
816
|
+
}
|
|
817
|
+
// Build aggregation query
|
|
818
|
+
const { sql, params } = this.buildAggregationQuery('MIN', fieldToAggregate, context);
|
|
819
|
+
// Execute
|
|
820
|
+
const result = this.executor
|
|
821
|
+
? await this.executor.query(sql, params)
|
|
822
|
+
: await this.client.query(sql, params);
|
|
823
|
+
return result.rows[0]?.result ?? null;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Get maximum value from the query
|
|
827
|
+
*/
|
|
828
|
+
async max(selector) {
|
|
829
|
+
const context = {
|
|
830
|
+
ctes: new Map(),
|
|
831
|
+
cteCounter: 0,
|
|
832
|
+
paramCounter: 1,
|
|
833
|
+
allParams: [],
|
|
834
|
+
executor: this.executor,
|
|
835
|
+
};
|
|
836
|
+
// If selector is provided, apply it to determine the field
|
|
837
|
+
let fieldToAggregate;
|
|
838
|
+
if (selector) {
|
|
839
|
+
const mockRow = this.createMockRow();
|
|
840
|
+
const mockSelection = this.selector(mockRow);
|
|
841
|
+
fieldToAggregate = selector(mockSelection);
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
// No selector - use the current selection
|
|
845
|
+
const mockRow = this.createMockRow();
|
|
846
|
+
fieldToAggregate = this.selector(mockRow);
|
|
847
|
+
}
|
|
848
|
+
// Build aggregation query
|
|
849
|
+
const { sql, params } = this.buildAggregationQuery('MAX', fieldToAggregate, context);
|
|
850
|
+
// Execute
|
|
851
|
+
const result = this.executor
|
|
852
|
+
? await this.executor.query(sql, params)
|
|
853
|
+
: await this.client.query(sql, params);
|
|
854
|
+
return result.rows[0]?.result ?? null;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Get sum of values from the query
|
|
858
|
+
*/
|
|
859
|
+
async sum(selector) {
|
|
860
|
+
const context = {
|
|
861
|
+
ctes: new Map(),
|
|
862
|
+
cteCounter: 0,
|
|
863
|
+
paramCounter: 1,
|
|
864
|
+
allParams: [],
|
|
865
|
+
executor: this.executor,
|
|
866
|
+
};
|
|
867
|
+
// If selector is provided, apply it to determine the field
|
|
868
|
+
let fieldToAggregate;
|
|
869
|
+
if (selector) {
|
|
870
|
+
const mockRow = this.createMockRow();
|
|
871
|
+
const mockSelection = this.selector(mockRow);
|
|
872
|
+
fieldToAggregate = selector(mockSelection);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
// No selector - use the current selection
|
|
876
|
+
const mockRow = this.createMockRow();
|
|
877
|
+
fieldToAggregate = this.selector(mockRow);
|
|
878
|
+
}
|
|
879
|
+
// Build aggregation query
|
|
880
|
+
const { sql, params } = this.buildAggregationQuery('SUM', fieldToAggregate, context);
|
|
881
|
+
// Execute
|
|
882
|
+
const result = this.executor
|
|
883
|
+
? await this.executor.query(sql, params)
|
|
884
|
+
: await this.client.query(sql, params);
|
|
885
|
+
return result.rows[0]?.result ?? null;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Get count of rows from the query
|
|
889
|
+
*/
|
|
890
|
+
async count() {
|
|
891
|
+
const context = {
|
|
892
|
+
ctes: new Map(),
|
|
893
|
+
cteCounter: 0,
|
|
894
|
+
paramCounter: 1,
|
|
895
|
+
allParams: [],
|
|
896
|
+
executor: this.executor,
|
|
897
|
+
};
|
|
898
|
+
// Build count query
|
|
899
|
+
const { sql, params } = this.buildCountQuery(context);
|
|
900
|
+
// Execute
|
|
901
|
+
const result = this.executor
|
|
902
|
+
? await this.executor.query(sql, params)
|
|
903
|
+
: await this.client.query(sql, params);
|
|
904
|
+
return parseInt(result.rows[0]?.count ?? '0', 10);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Execute query and return results as array
|
|
908
|
+
* Collection results are automatically resolved to arrays
|
|
909
|
+
*/
|
|
910
|
+
async toList() {
|
|
911
|
+
const context = {
|
|
912
|
+
ctes: new Map(),
|
|
913
|
+
cteCounter: 0,
|
|
914
|
+
paramCounter: 1,
|
|
915
|
+
allParams: [],
|
|
916
|
+
collectionStrategy: this.collectionStrategy,
|
|
917
|
+
executor: this.executor,
|
|
918
|
+
};
|
|
919
|
+
// Analyze the selector to extract nested queries
|
|
920
|
+
const mockRow = this.createMockRow();
|
|
921
|
+
const selectionResult = this.selector(mockRow);
|
|
922
|
+
// Check if we're using temp table strategy and have collections
|
|
923
|
+
const collections = this.detectCollections(selectionResult);
|
|
924
|
+
const useTempTableStrategy = this.collectionStrategy === 'temptable' && collections.length > 0;
|
|
925
|
+
if (useTempTableStrategy) {
|
|
926
|
+
// Two-phase execution for temp table strategy
|
|
927
|
+
return this.executeWithTempTables(selectionResult, context, collections);
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
// Single-phase execution for JSONB strategy (current behavior)
|
|
931
|
+
return this.executeSinglePhase(selectionResult, context);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Execute query using single-phase approach (JSONB/CTE strategy)
|
|
936
|
+
*/
|
|
937
|
+
async executeSinglePhase(selectionResult, context) {
|
|
938
|
+
// Build the query
|
|
939
|
+
const { sql, params } = this.buildQuery(selectionResult, context);
|
|
940
|
+
// Execute using executor if available, otherwise use client directly
|
|
941
|
+
const result = this.executor
|
|
942
|
+
? await this.executor.query(sql, params)
|
|
943
|
+
: await this.client.query(sql, params);
|
|
944
|
+
// Transform results
|
|
945
|
+
return this.transformResults(result.rows, selectionResult);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Execute query using two-phase approach (temp table strategy)
|
|
949
|
+
*/
|
|
950
|
+
async executeWithTempTables(selectionResult, context, collections) {
|
|
951
|
+
// Build base selection (excludes collections, includes foreign keys)
|
|
952
|
+
const baseSelection = this.buildBaseSelection(selectionResult, collections);
|
|
953
|
+
const { sql: baseSql, params: baseParams } = this.buildQuery(baseSelection, {
|
|
954
|
+
...context,
|
|
955
|
+
ctes: new Map(), // Clear CTEs since we're not using them for collections
|
|
956
|
+
});
|
|
957
|
+
// Check if we can use fully optimized single-query approach
|
|
958
|
+
// Requirements: PostgresClient with querySimpleMulti support AND no parameters in base query
|
|
959
|
+
const canUseFullOptimization = this.client.supportsMultiStatementQueries() &&
|
|
960
|
+
baseParams.length === 0 &&
|
|
961
|
+
collections.length > 0;
|
|
962
|
+
if (canUseFullOptimization) {
|
|
963
|
+
return this.executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections);
|
|
964
|
+
}
|
|
965
|
+
// Legacy two-phase approach: execute base query first
|
|
966
|
+
const baseResult = this.executor
|
|
967
|
+
? await this.executor.query(baseSql, baseParams)
|
|
968
|
+
: await this.client.query(baseSql, baseParams);
|
|
969
|
+
if (baseResult.rows.length === 0) {
|
|
970
|
+
return [];
|
|
971
|
+
}
|
|
972
|
+
// Extract parent IDs from base results (using the known alias we added in buildBaseSelection)
|
|
973
|
+
const parentIds = baseResult.rows.map(row => row.__pk_id);
|
|
974
|
+
// Phase 2: Execute collection aggregations using temp tables
|
|
975
|
+
// For each collection, call buildCTE with parent IDs
|
|
976
|
+
const collectionResults = new Map();
|
|
977
|
+
for (const collection of collections) {
|
|
978
|
+
const builder = collection.builder;
|
|
979
|
+
// Call buildCTE with parent IDs - this will use the temp table strategy
|
|
980
|
+
const aggResult = await builder.buildCTE(context, this.client, parentIds);
|
|
981
|
+
// aggResult is a Promise<CollectionAggregationResult> for temp table strategy
|
|
982
|
+
const result = await aggResult;
|
|
983
|
+
// If the result has a tableName, it means temp tables were created and we need to query them
|
|
984
|
+
if (result.tableName && !result.isCTE) {
|
|
985
|
+
// Check if data was already fetched (multi-statement optimization)
|
|
986
|
+
if (result.dataFetched && result.data) {
|
|
987
|
+
// Data already fetched - use it directly
|
|
988
|
+
collectionResults.set(collection.name, result.data);
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
// Temp table strategy (legacy) - query the aggregation table
|
|
992
|
+
const aggQuery = `SELECT parent_id, data FROM ${result.tableName}`;
|
|
993
|
+
const aggQueryResult = this.executor
|
|
994
|
+
? await this.executor.query(aggQuery, [])
|
|
995
|
+
: await this.client.query(aggQuery, []);
|
|
996
|
+
// Cleanup temp tables if needed
|
|
997
|
+
if (result.cleanupSql) {
|
|
998
|
+
await this.client.query(result.cleanupSql);
|
|
999
|
+
}
|
|
1000
|
+
// Index results by parent_id for merging
|
|
1001
|
+
const resultMap = new Map();
|
|
1002
|
+
for (const row of aggQueryResult.rows) {
|
|
1003
|
+
resultMap.set(row.parent_id, row.data);
|
|
1004
|
+
}
|
|
1005
|
+
collectionResults.set(collection.name, resultMap);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
// CTE strategy (shouldn't happen in temp table mode, but handle it)
|
|
1010
|
+
throw new Error('Expected temp table result but got CTE');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// Phase 3: Merge base results with collection results
|
|
1014
|
+
const mergedRows = baseResult.rows.map(baseRow => {
|
|
1015
|
+
const merged = { ...baseRow };
|
|
1016
|
+
for (const collection of collections) {
|
|
1017
|
+
const resultMap = collectionResults.get(collection.name);
|
|
1018
|
+
const parentId = baseRow.__pk_id;
|
|
1019
|
+
merged[collection.name] = resultMap?.get(parentId) || this.getDefaultValueForCollection(collection.builder);
|
|
1020
|
+
}
|
|
1021
|
+
// Remove the internal __pk_id field before returning
|
|
1022
|
+
delete merged.__pk_id;
|
|
1023
|
+
return merged;
|
|
1024
|
+
});
|
|
1025
|
+
// Transform results using the original selection
|
|
1026
|
+
return this.transformResults(mergedRows, selectionResult);
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Execute using fully optimized single-query approach (PostgresClient only)
|
|
1030
|
+
* Combines base query + all collections into ONE multi-statement query
|
|
1031
|
+
*/
|
|
1032
|
+
async executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections) {
|
|
1033
|
+
const baseTempTable = `tmp_base_${context.cteCounter++}`;
|
|
1034
|
+
// Build SQL for each collection
|
|
1035
|
+
const collectionSQLs = [];
|
|
1036
|
+
for (const collection of collections) {
|
|
1037
|
+
const builderAny = collection.builder;
|
|
1038
|
+
const targetTable = builderAny.targetTable;
|
|
1039
|
+
const foreignKey = builderAny.foreignKey;
|
|
1040
|
+
const selector = builderAny.selector;
|
|
1041
|
+
const orderByFields = builderAny.orderByFields || [];
|
|
1042
|
+
// Build selected fields
|
|
1043
|
+
let selectedFieldsSQL = '';
|
|
1044
|
+
if (selector) {
|
|
1045
|
+
const mockItem = builderAny.createMockItem();
|
|
1046
|
+
const selectedFields = selector(mockItem);
|
|
1047
|
+
const fieldParts = [];
|
|
1048
|
+
for (const [alias, field] of Object.entries(selectedFields)) {
|
|
1049
|
+
if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
1050
|
+
const dbColumnName = field.__dbColumnName;
|
|
1051
|
+
fieldParts.push(`"${dbColumnName}" as "${alias}"`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
selectedFieldsSQL = fieldParts.join(', ');
|
|
1055
|
+
}
|
|
1056
|
+
// Build ORDER BY
|
|
1057
|
+
let orderBySQL = orderByFields.length > 0
|
|
1058
|
+
? ` ORDER BY ${orderByFields.map(({ field, direction }) => `"${field}" ${direction}`).join(', ')}`
|
|
1059
|
+
: ` ORDER BY "id" DESC`;
|
|
1060
|
+
const collectionSQL = `SELECT "${foreignKey}" as parent_id, ${selectedFieldsSQL} FROM "${targetTable}" WHERE "${foreignKey}" IN (SELECT "__pk_id" FROM ${baseTempTable})${orderBySQL}`;
|
|
1061
|
+
collectionSQLs.push(collectionSQL);
|
|
1062
|
+
}
|
|
1063
|
+
// Build mega multi-statement SQL
|
|
1064
|
+
const statements = [
|
|
1065
|
+
`CREATE TEMP TABLE ${baseTempTable} AS ${baseSql}`,
|
|
1066
|
+
`SELECT * FROM ${baseTempTable}`,
|
|
1067
|
+
...collectionSQLs,
|
|
1068
|
+
`DROP TABLE IF EXISTS ${baseTempTable}`
|
|
1069
|
+
];
|
|
1070
|
+
const multiStatementSQL = statements.join(';\n');
|
|
1071
|
+
// Execute via querySimpleMulti
|
|
1072
|
+
const executor = this.executor || this.client;
|
|
1073
|
+
let resultSets;
|
|
1074
|
+
if ('querySimpleMulti' in executor && typeof executor.querySimpleMulti === 'function') {
|
|
1075
|
+
resultSets = await executor.querySimpleMulti(multiStatementSQL);
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
throw new Error('Fully optimized mode requires querySimpleMulti support');
|
|
1079
|
+
}
|
|
1080
|
+
// Parse result sets: [0]=CREATE, [1]=base, [2..N]=collections, [N+1]=DROP
|
|
1081
|
+
const baseResult = resultSets[1];
|
|
1082
|
+
if (!baseResult || baseResult.rows.length === 0) {
|
|
1083
|
+
return [];
|
|
1084
|
+
}
|
|
1085
|
+
// Group collection results by parent_id
|
|
1086
|
+
const collectionResults = new Map();
|
|
1087
|
+
collections.forEach((collection, idx) => {
|
|
1088
|
+
const collectionResultSet = resultSets[2 + idx];
|
|
1089
|
+
const dataMap = new Map();
|
|
1090
|
+
for (const row of collectionResultSet.rows) {
|
|
1091
|
+
const parentId = row.parent_id;
|
|
1092
|
+
if (!dataMap.has(parentId)) {
|
|
1093
|
+
dataMap.set(parentId, []);
|
|
1094
|
+
}
|
|
1095
|
+
const { parent_id, ...rowData } = row;
|
|
1096
|
+
dataMap.get(parentId).push(rowData);
|
|
1097
|
+
}
|
|
1098
|
+
collectionResults.set(collection.name, dataMap);
|
|
1099
|
+
});
|
|
1100
|
+
// Merge base results with collection results
|
|
1101
|
+
const mergedRows = baseResult.rows.map((baseRow) => {
|
|
1102
|
+
const merged = { ...baseRow };
|
|
1103
|
+
for (const collection of collections) {
|
|
1104
|
+
const resultMap = collectionResults.get(collection.name);
|
|
1105
|
+
const parentId = baseRow.__pk_id;
|
|
1106
|
+
merged[collection.name] = resultMap?.get(parentId) || [];
|
|
1107
|
+
}
|
|
1108
|
+
delete merged.__pk_id;
|
|
1109
|
+
return merged;
|
|
1110
|
+
});
|
|
1111
|
+
return this.transformResults(mergedRows, selectionResult);
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Detect collections in the selection result
|
|
1115
|
+
*/
|
|
1116
|
+
detectCollections(selection) {
|
|
1117
|
+
const collections = [];
|
|
1118
|
+
if (typeof selection === 'object' && selection !== null && !(selection instanceof conditions_1.SqlFragment)) {
|
|
1119
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
1120
|
+
if (value instanceof CollectionQueryBuilder) {
|
|
1121
|
+
collections.push({ name: key, builder: value });
|
|
1122
|
+
}
|
|
1123
|
+
else if (value && typeof value === 'object' && '__collectionResult' in value && 'buildCTE' in value) {
|
|
1124
|
+
// This is a CollectionResult which wraps a CollectionQueryBuilder
|
|
1125
|
+
collections.push({ name: key, builder: value });
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return collections;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Build base selection excluding collections but including necessary foreign keys
|
|
1133
|
+
*/
|
|
1134
|
+
buildBaseSelection(selection, collections) {
|
|
1135
|
+
const baseSelection = {};
|
|
1136
|
+
const collectionNames = new Set(collections.map(c => c.name));
|
|
1137
|
+
// Always ensure we have the primary key in the base selection with a known alias
|
|
1138
|
+
const mockRow = this.createMockRow();
|
|
1139
|
+
baseSelection['__pk_id'] = mockRow.id; // Add primary key with a known alias
|
|
1140
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
1141
|
+
if (!collectionNames.has(key)) {
|
|
1142
|
+
baseSelection[key] = value;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return baseSelection;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Build collection aggregation config from CollectionQueryBuilder
|
|
1149
|
+
*/
|
|
1150
|
+
buildCollectionConfig(builder, context, parentIds) {
|
|
1151
|
+
// This is similar to the logic in CollectionQueryBuilder.buildCTE()
|
|
1152
|
+
// but extracts the config instead of building SQL directly
|
|
1153
|
+
// We need to access private members - use type assertion
|
|
1154
|
+
const builderAny = builder;
|
|
1155
|
+
const selectedFieldConfigs = [];
|
|
1156
|
+
// Determine aggregation type
|
|
1157
|
+
let aggregationType = 'jsonb';
|
|
1158
|
+
let aggregateField;
|
|
1159
|
+
let arrayField;
|
|
1160
|
+
if (builderAny.aggregationType) {
|
|
1161
|
+
aggregationType = builderAny.aggregationType.toLowerCase();
|
|
1162
|
+
}
|
|
1163
|
+
else if (builderAny.flattenResultType) {
|
|
1164
|
+
aggregationType = 'array';
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
relationName: builderAny.relationName,
|
|
1168
|
+
targetTable: builderAny.targetTable,
|
|
1169
|
+
foreignKey: builderAny.foreignKey,
|
|
1170
|
+
sourceTable: builderAny.sourceTable,
|
|
1171
|
+
parentIds,
|
|
1172
|
+
selectedFields: selectedFieldConfigs,
|
|
1173
|
+
whereClause: '', // Will be built from whereCond
|
|
1174
|
+
orderByClause: '', // Will be built from orderByFields
|
|
1175
|
+
limitValue: builderAny.limitValue,
|
|
1176
|
+
offsetValue: builderAny.offsetValue,
|
|
1177
|
+
isDistinct: builderAny.isDistinct,
|
|
1178
|
+
aggregationType,
|
|
1179
|
+
aggregateField,
|
|
1180
|
+
arrayField: builderAny.flattenResultType ? this.extractArrayField(builderAny) : undefined,
|
|
1181
|
+
defaultValue: this.getDefaultValueString(aggregationType),
|
|
1182
|
+
counter: context.cteCounter++,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Extract array field from collection builder for array aggregations
|
|
1187
|
+
*/
|
|
1188
|
+
extractArrayField(builder) {
|
|
1189
|
+
// For array aggregations, we need to determine which field to aggregate
|
|
1190
|
+
if (builder.selector) {
|
|
1191
|
+
const mockItem = builder.createMockItem?.() || {};
|
|
1192
|
+
const selectedField = builder.selector(mockItem);
|
|
1193
|
+
if (typeof selectedField === 'object' && selectedField !== null && '__dbColumnName' in selectedField) {
|
|
1194
|
+
return selectedField.__dbColumnName;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return undefined;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Get default value string for aggregation type
|
|
1201
|
+
*/
|
|
1202
|
+
getDefaultValueString(aggregationType) {
|
|
1203
|
+
switch (aggregationType) {
|
|
1204
|
+
case 'jsonb':
|
|
1205
|
+
case 'array':
|
|
1206
|
+
return "'[]'::jsonb";
|
|
1207
|
+
case 'count':
|
|
1208
|
+
return '0';
|
|
1209
|
+
case 'min':
|
|
1210
|
+
case 'max':
|
|
1211
|
+
case 'sum':
|
|
1212
|
+
return 'null';
|
|
1213
|
+
default:
|
|
1214
|
+
return "'[]'::jsonb";
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Get default value for collection based on aggregation type
|
|
1219
|
+
*/
|
|
1220
|
+
getDefaultValueForCollection(builder) {
|
|
1221
|
+
const builderAny = builder;
|
|
1222
|
+
if (builderAny.aggregationType) {
|
|
1223
|
+
// Scalar aggregation
|
|
1224
|
+
return builderAny.aggregationType === 'COUNT' ? 0 : null;
|
|
1225
|
+
}
|
|
1226
|
+
else if (builderAny.flattenResultType) {
|
|
1227
|
+
// Array aggregation
|
|
1228
|
+
return [];
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
// JSONB aggregation (object array)
|
|
1232
|
+
return [];
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Execute query and return first result or null
|
|
1237
|
+
*/
|
|
1238
|
+
async first() {
|
|
1239
|
+
const results = await this.limit(1).toList();
|
|
1240
|
+
return results.length > 0 ? results[0] : null;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Execute query and return first result or null (alias for first)
|
|
1244
|
+
*/
|
|
1245
|
+
async firstOrDefault() {
|
|
1246
|
+
return this.first();
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Execute query and return first result or throw
|
|
1250
|
+
*/
|
|
1251
|
+
async firstOrThrow() {
|
|
1252
|
+
const result = await this.first();
|
|
1253
|
+
if (!result) {
|
|
1254
|
+
throw new Error('No results found');
|
|
1255
|
+
}
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Create mock row for analysis
|
|
1260
|
+
*/
|
|
1261
|
+
createMockRow() {
|
|
1262
|
+
const mock = {};
|
|
1263
|
+
// Add columns as FieldRef objects - type-safe with property name and database column name
|
|
1264
|
+
for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
|
|
1265
|
+
const dbColumnName = colBuilder.build().name;
|
|
1266
|
+
Object.defineProperty(mock, colName, {
|
|
1267
|
+
get: () => ({
|
|
1268
|
+
__fieldName: colName,
|
|
1269
|
+
__dbColumnName: dbColumnName,
|
|
1270
|
+
__tableAlias: this.schema.name,
|
|
1271
|
+
}),
|
|
1272
|
+
enumerable: true,
|
|
1273
|
+
configurable: true,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
// Add columns from manually joined tables
|
|
1277
|
+
for (const join of this.manualJoins) {
|
|
1278
|
+
// Skip subquery joins (they don't have a schema)
|
|
1279
|
+
if (join.isSubquery || !join.schema) {
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
for (const [colName, colBuilder] of Object.entries(join.schema.columns)) {
|
|
1283
|
+
const dbColumnName = colBuilder.build().name;
|
|
1284
|
+
// Create a unique property name by prefixing with table alias or using the column name directly
|
|
1285
|
+
// For now, we'll create nested objects for each joined table
|
|
1286
|
+
if (!mock[join.alias]) {
|
|
1287
|
+
mock[join.alias] = {};
|
|
1288
|
+
}
|
|
1289
|
+
Object.defineProperty(mock[join.alias], colName, {
|
|
1290
|
+
get: () => ({
|
|
1291
|
+
__fieldName: colName,
|
|
1292
|
+
__dbColumnName: dbColumnName,
|
|
1293
|
+
__tableAlias: join.alias,
|
|
1294
|
+
}),
|
|
1295
|
+
enumerable: true,
|
|
1296
|
+
configurable: true,
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// Add relations as CollectionQueryBuilder or ReferenceQueryBuilder
|
|
1301
|
+
for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
|
|
1302
|
+
if (relConfig.type === 'many') {
|
|
1303
|
+
Object.defineProperty(mock, relName, {
|
|
1304
|
+
get: () => {
|
|
1305
|
+
// Don't call build() - force registry lookup to get schema with relations
|
|
1306
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, undefined, // Don't pass schema, force registry lookup
|
|
1307
|
+
this.schemaRegistry // Pass schema registry for nested resolution
|
|
1308
|
+
);
|
|
1309
|
+
},
|
|
1310
|
+
enumerable: true,
|
|
1311
|
+
configurable: true,
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
else {
|
|
1315
|
+
// For single reference (many-to-one), create a ReferenceQueryBuilder
|
|
1316
|
+
Object.defineProperty(mock, relName, {
|
|
1317
|
+
get: () => {
|
|
1318
|
+
// Don't call build() - force registry lookup to get schema with relations
|
|
1319
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
|
|
1320
|
+
this.schemaRegistry // Pass schema registry for nested resolution
|
|
1321
|
+
);
|
|
1322
|
+
// Return a mock object that exposes the target table's columns
|
|
1323
|
+
return refBuilder.createMockTargetRow();
|
|
1324
|
+
},
|
|
1325
|
+
enumerable: true,
|
|
1326
|
+
configurable: true,
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return mock;
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Detect navigation property references in selection and add necessary JOINs
|
|
1334
|
+
*/
|
|
1335
|
+
detectAndAddJoinsFromSelection(selection, joins) {
|
|
1336
|
+
if (!selection || typeof selection !== 'object') {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
1340
|
+
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
1341
|
+
// This is a FieldRef with a table alias - check if it's from a related table
|
|
1342
|
+
const tableAlias = value.__tableAlias;
|
|
1343
|
+
if (tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
1344
|
+
// This references a related table - find the relation and add a JOIN
|
|
1345
|
+
const relation = this.schema.relations[tableAlias];
|
|
1346
|
+
if (relation && relation.type === 'one') {
|
|
1347
|
+
// Get target schema from targetTableBuilder if available
|
|
1348
|
+
let targetSchema;
|
|
1349
|
+
if (relation.targetTableBuilder) {
|
|
1350
|
+
const targetTableSchema = relation.targetTableBuilder.build();
|
|
1351
|
+
targetSchema = targetTableSchema.schema;
|
|
1352
|
+
}
|
|
1353
|
+
// Add a JOIN for this reference
|
|
1354
|
+
joins.push({
|
|
1355
|
+
alias: tableAlias,
|
|
1356
|
+
targetTable: relation.targetTable,
|
|
1357
|
+
targetSchema,
|
|
1358
|
+
foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
|
|
1359
|
+
matches: relation.matches || [],
|
|
1360
|
+
isMandatory: relation.isMandatory ?? false,
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1366
|
+
// Recursively check nested objects
|
|
1367
|
+
this.detectAndAddJoinsFromSelection(value, joins);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Build SQL query
|
|
1373
|
+
*/
|
|
1374
|
+
buildQuery(selection, context) {
|
|
1375
|
+
// Handle user-defined CTEs first - their params need to come before main query params
|
|
1376
|
+
for (const cte of this.ctes) {
|
|
1377
|
+
context.allParams.push(...cte.params);
|
|
1378
|
+
context.paramCounter += cte.params.length;
|
|
1379
|
+
}
|
|
1380
|
+
const selectParts = [];
|
|
1381
|
+
const collectionFields = [];
|
|
1382
|
+
const joins = [];
|
|
1383
|
+
// Scan selection for navigation property references and add JOINs
|
|
1384
|
+
this.detectAndAddJoinsFromSelection(selection, joins);
|
|
1385
|
+
// Handle case where selection is a single value (not an object with properties)
|
|
1386
|
+
if (selection instanceof conditions_1.SqlFragment) {
|
|
1387
|
+
// Single SQL fragment - just build it directly
|
|
1388
|
+
const sqlBuildContext = {
|
|
1389
|
+
paramCounter: context.paramCounter,
|
|
1390
|
+
params: context.allParams,
|
|
1391
|
+
};
|
|
1392
|
+
const fragmentSql = selection.buildSql(sqlBuildContext);
|
|
1393
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
1394
|
+
selectParts.push(fragmentSql);
|
|
1395
|
+
}
|
|
1396
|
+
else if (typeof selection === 'object' && selection !== null && '__dbColumnName' in selection) {
|
|
1397
|
+
// Single FieldRef
|
|
1398
|
+
const tableAlias = ('__tableAlias' in selection && selection.__tableAlias) ? selection.__tableAlias : this.schema.name;
|
|
1399
|
+
selectParts.push(`"${tableAlias}"."${selection.__dbColumnName}"`);
|
|
1400
|
+
}
|
|
1401
|
+
else if (selection instanceof CollectionQueryBuilder) {
|
|
1402
|
+
// This shouldn't happen in normal flow, but handle it
|
|
1403
|
+
throw new Error('Cannot use CollectionQueryBuilder directly as selection');
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
// Process selection object properties
|
|
1407
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
1408
|
+
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
1409
|
+
// Handle collection - create CTE (works for both CollectionQueryBuilder and CollectionResult)
|
|
1410
|
+
const cteName = `cte_${context.cteCounter++}`;
|
|
1411
|
+
const cteData = value.buildCTE ? value.buildCTE(context) : value.buildCTE(context);
|
|
1412
|
+
context.ctes.set(cteName, cteData);
|
|
1413
|
+
collectionFields.push({ name: key, cteName });
|
|
1414
|
+
}
|
|
1415
|
+
else if (value instanceof subquery_1.Subquery || (value && typeof value === 'object' && 'buildSql' in value && typeof value.buildSql === 'function' && '__mode' in value)) {
|
|
1416
|
+
// Handle Subquery - build SQL and wrap in parentheses
|
|
1417
|
+
// Check both instanceof and duck typing for Subquery
|
|
1418
|
+
const sqlBuildContext = {
|
|
1419
|
+
paramCounter: context.paramCounter,
|
|
1420
|
+
params: context.allParams,
|
|
1421
|
+
};
|
|
1422
|
+
const subquerySql = value.buildSql(sqlBuildContext);
|
|
1423
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
1424
|
+
selectParts.push(`(${subquerySql}) as "${key}"`);
|
|
1425
|
+
}
|
|
1426
|
+
else if (value instanceof conditions_1.SqlFragment) {
|
|
1427
|
+
// SQL Fragment - build the SQL expression
|
|
1428
|
+
const sqlBuildContext = {
|
|
1429
|
+
paramCounter: context.paramCounter,
|
|
1430
|
+
params: context.allParams,
|
|
1431
|
+
};
|
|
1432
|
+
const fragmentSql = value.buildSql(sqlBuildContext);
|
|
1433
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
1434
|
+
selectParts.push(`${fragmentSql} as "${key}"`);
|
|
1435
|
+
}
|
|
1436
|
+
else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
|
|
1437
|
+
// FieldRef object - check if it has a table alias (from navigation)
|
|
1438
|
+
if ('__tableAlias' in value && value.__tableAlias && typeof value.__tableAlias === 'string') {
|
|
1439
|
+
// This is a field from a joined table
|
|
1440
|
+
const tableAlias = value.__tableAlias;
|
|
1441
|
+
// Find the relation config for this navigation
|
|
1442
|
+
const relConfig = this.schema.relations[tableAlias];
|
|
1443
|
+
if (relConfig) {
|
|
1444
|
+
// Add JOIN if not already added
|
|
1445
|
+
if (!joins.find(j => j.alias === tableAlias)) {
|
|
1446
|
+
// Get target schema from targetTableBuilder if available
|
|
1447
|
+
let targetSchema;
|
|
1448
|
+
if (relConfig.targetTableBuilder) {
|
|
1449
|
+
const targetTableSchema = relConfig.targetTableBuilder.build();
|
|
1450
|
+
targetSchema = targetTableSchema.schema;
|
|
1451
|
+
}
|
|
1452
|
+
joins.push({
|
|
1453
|
+
alias: tableAlias,
|
|
1454
|
+
targetTable: relConfig.targetTable,
|
|
1455
|
+
targetSchema,
|
|
1456
|
+
foreignKeys: relConfig.foreignKeys || [relConfig.foreignKey || ''],
|
|
1457
|
+
matches: relConfig.matches || [],
|
|
1458
|
+
isMandatory: relConfig.isMandatory ?? false,
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
selectParts.push(`"${tableAlias}"."${value.__dbColumnName}" as "${key}"`);
|
|
1463
|
+
}
|
|
1464
|
+
else {
|
|
1465
|
+
// Regular field from the main table
|
|
1466
|
+
selectParts.push(`"${this.schema.name}"."${value.__dbColumnName}" as "${key}"`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
else if (typeof value === 'string') {
|
|
1470
|
+
// Simple column reference (for backward compatibility or direct usage)
|
|
1471
|
+
selectParts.push(`"${this.schema.name}"."${value}" as "${key}"`);
|
|
1472
|
+
}
|
|
1473
|
+
else if (typeof value === 'object' && value !== null) {
|
|
1474
|
+
// Check if this is a navigation property mock or placeholder
|
|
1475
|
+
if (!('__dbColumnName' in value)) {
|
|
1476
|
+
// This is not a FieldRef - check if it's a navigation property mock or array
|
|
1477
|
+
if (Array.isArray(value)) {
|
|
1478
|
+
// Skip arrays (empty navigation placeholders)
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
// Check if it's a CollectionQueryBuilder or ReferenceQueryBuilder instance
|
|
1482
|
+
if (value instanceof CollectionQueryBuilder) {
|
|
1483
|
+
// Skip collection query builders that haven't been resolved
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
else if (value instanceof ReferenceQueryBuilder) {
|
|
1487
|
+
// Handle ReferenceQueryBuilder - select all fields from the target table
|
|
1488
|
+
const targetSchema = value.getTargetTableSchema();
|
|
1489
|
+
const alias = value.getAlias();
|
|
1490
|
+
if (targetSchema) {
|
|
1491
|
+
// Add JOIN if not already added
|
|
1492
|
+
if (!joins.find(j => j.alias === alias)) {
|
|
1493
|
+
// Get target schema name from targetSchema
|
|
1494
|
+
let targetTableSchema;
|
|
1495
|
+
if (targetSchema.schema) {
|
|
1496
|
+
targetTableSchema = targetSchema.schema;
|
|
1497
|
+
}
|
|
1498
|
+
joins.push({
|
|
1499
|
+
alias,
|
|
1500
|
+
targetTable: value.getTargetTable(),
|
|
1501
|
+
targetSchema: targetTableSchema,
|
|
1502
|
+
foreignKeys: value.getForeignKeys(),
|
|
1503
|
+
matches: value.getMatches(),
|
|
1504
|
+
isMandatory: value.getIsMandatory(),
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
// Select all columns from the target table and group them
|
|
1508
|
+
// We'll need to use JSON object building in SQL
|
|
1509
|
+
const fieldParts = [];
|
|
1510
|
+
for (const [colKey, col] of Object.entries(targetSchema.columns)) {
|
|
1511
|
+
const config = col.build();
|
|
1512
|
+
fieldParts.push(`'${colKey}', "${alias}"."${config.name}"`);
|
|
1513
|
+
}
|
|
1514
|
+
selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
|
|
1515
|
+
}
|
|
1516
|
+
else {
|
|
1517
|
+
// No target schema available, skip
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// Check if it's a mock object with property descriptors (navigation property mock)
|
|
1522
|
+
const props = Object.getOwnPropertyNames(value);
|
|
1523
|
+
if (props.length > 0) {
|
|
1524
|
+
const firstProp = props[0];
|
|
1525
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, firstProp);
|
|
1526
|
+
if (descriptor && descriptor.get) {
|
|
1527
|
+
// This object has getter properties - likely a navigation mock
|
|
1528
|
+
// Try to determine if this is a reference navigation by checking the schema relations
|
|
1529
|
+
const tableAlias = Object.keys(value).find(k => {
|
|
1530
|
+
const desc = Object.getOwnPropertyDescriptor(value, k);
|
|
1531
|
+
return desc && desc.get && typeof desc.get === 'function';
|
|
1532
|
+
});
|
|
1533
|
+
if (tableAlias) {
|
|
1534
|
+
// Try to get the first property to check if it has __tableAlias
|
|
1535
|
+
try {
|
|
1536
|
+
const firstValue = value[tableAlias];
|
|
1537
|
+
if (firstValue && typeof firstValue === 'object' && '__tableAlias' in firstValue) {
|
|
1538
|
+
const alias = firstValue.__tableAlias;
|
|
1539
|
+
const relConfig = this.schema.relations[alias];
|
|
1540
|
+
if (relConfig && relConfig.type === 'one') {
|
|
1541
|
+
// This is a reference navigation - select all fields from the target table
|
|
1542
|
+
// Find the target table schema
|
|
1543
|
+
let targetSchema;
|
|
1544
|
+
if (relConfig.targetTableBuilder) {
|
|
1545
|
+
targetSchema = relConfig.targetTableBuilder.build();
|
|
1546
|
+
}
|
|
1547
|
+
if (targetSchema) {
|
|
1548
|
+
// Add JOIN if not already added
|
|
1549
|
+
if (!joins.find(j => j.alias === alias)) {
|
|
1550
|
+
let targetTableSchema;
|
|
1551
|
+
if (targetSchema.schema) {
|
|
1552
|
+
targetTableSchema = targetSchema.schema;
|
|
1553
|
+
}
|
|
1554
|
+
joins.push({
|
|
1555
|
+
alias,
|
|
1556
|
+
targetTable: relConfig.targetTable,
|
|
1557
|
+
targetSchema: targetTableSchema,
|
|
1558
|
+
foreignKeys: relConfig.foreignKeys || [relConfig.foreignKey || ''],
|
|
1559
|
+
matches: relConfig.matches || [],
|
|
1560
|
+
isMandatory: relConfig.isMandatory ?? false,
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
// Select all columns from the target table and group them into a JSON object
|
|
1564
|
+
const fieldParts = [];
|
|
1565
|
+
for (const [colKey, col] of Object.entries(targetSchema.columns)) {
|
|
1566
|
+
const config = col.build();
|
|
1567
|
+
fieldParts.push(`'${colKey}', "${alias}"."${config.name}"`);
|
|
1568
|
+
}
|
|
1569
|
+
selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
catch (e) {
|
|
1576
|
+
// If accessing the property fails, just skip this navigation
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
// Default: skip this navigation mock
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
// Otherwise, treat as literal value
|
|
1585
|
+
selectParts.push(`$${context.paramCounter++} as "${key}"`);
|
|
1586
|
+
context.allParams.push(value);
|
|
1587
|
+
}
|
|
1588
|
+
else if (value === undefined) {
|
|
1589
|
+
// Skip undefined values (navigation property placeholders)
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
else {
|
|
1593
|
+
// Literal value or expression
|
|
1594
|
+
selectParts.push(`$${context.paramCounter++} as "${key}"`);
|
|
1595
|
+
context.allParams.push(value);
|
|
1596
|
+
}
|
|
1597
|
+
} // End of for loop
|
|
1598
|
+
} // End of else block
|
|
1599
|
+
// Add collection fields as JSON/array aggregations joined from CTEs
|
|
1600
|
+
for (const { name, cteName } of collectionFields) {
|
|
1601
|
+
// Check if this is an array aggregation (from toNumberList/toStringList)
|
|
1602
|
+
const collectionValue = selection[name];
|
|
1603
|
+
const isArrayAgg = collectionValue && typeof collectionValue === 'object' && 'isArrayAggregation' in collectionValue && collectionValue.isArrayAggregation();
|
|
1604
|
+
// Check if this is a scalar aggregation (count, sum, max, min)
|
|
1605
|
+
const isScalarAgg = collectionValue instanceof CollectionQueryBuilder &&
|
|
1606
|
+
collectionValue.isScalarAggregation();
|
|
1607
|
+
if (isScalarAgg) {
|
|
1608
|
+
// For scalar aggregations, handle COUNT vs other aggregations differently
|
|
1609
|
+
// COUNT should default to 0, while MAX/MIN/SUM should remain NULL
|
|
1610
|
+
const aggregationType = collectionValue.getAggregationType();
|
|
1611
|
+
if (aggregationType === 'COUNT') {
|
|
1612
|
+
selectParts.push(`COALESCE("${cteName}".data, 0) as "${name}"`);
|
|
1613
|
+
}
|
|
1614
|
+
else {
|
|
1615
|
+
// For MAX/MIN/SUM, keep NULL as-is
|
|
1616
|
+
selectParts.push(`"${cteName}".data as "${name}"`);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
else if (isArrayAgg) {
|
|
1620
|
+
// For array aggregation, determine the array type from flattenResultType
|
|
1621
|
+
const flattenType = collectionValue.getFlattenResultType?.() || 'string';
|
|
1622
|
+
const arrayType = flattenType === 'number' ? 'integer[]' : 'text[]';
|
|
1623
|
+
selectParts.push(`COALESCE("${cteName}".data, ARRAY[]::${arrayType}) as "${name}"`);
|
|
1624
|
+
}
|
|
1625
|
+
else {
|
|
1626
|
+
// For JSON aggregation, use jsonb type
|
|
1627
|
+
selectParts.push(`COALESCE("${cteName}".data, '[]'::jsonb) as "${name}"`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
// Build WHERE clause
|
|
1631
|
+
let whereClause = '';
|
|
1632
|
+
if (this.whereCond) {
|
|
1633
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1634
|
+
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
1635
|
+
whereClause = `WHERE ${sql}`;
|
|
1636
|
+
context.paramCounter += params.length;
|
|
1637
|
+
context.allParams.push(...params);
|
|
1638
|
+
}
|
|
1639
|
+
// Build ORDER BY clause
|
|
1640
|
+
let orderByClause = '';
|
|
1641
|
+
if (this.orderByFields.length > 0) {
|
|
1642
|
+
const orderParts = this.orderByFields.map(({ field, direction }) => {
|
|
1643
|
+
// Check if the field is in the selection (after a select() call)
|
|
1644
|
+
// If so, reference it as an alias, otherwise use table.column notation
|
|
1645
|
+
if (selection && typeof selection === 'object' && !Array.isArray(selection) && field in selection) {
|
|
1646
|
+
// Field is in the selected output, use it as an alias
|
|
1647
|
+
return `"${field}" ${direction}`;
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
// Field is not in the selection, use table.column notation
|
|
1651
|
+
return `"${this.schema.name}"."${field}" ${direction}`;
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
orderByClause = `ORDER BY ${orderParts.join(', ')}`;
|
|
1655
|
+
}
|
|
1656
|
+
// Build LIMIT/OFFSET
|
|
1657
|
+
let limitClause = '';
|
|
1658
|
+
if (this.limitValue !== undefined) {
|
|
1659
|
+
limitClause = `LIMIT ${this.limitValue}`;
|
|
1660
|
+
}
|
|
1661
|
+
if (this.offsetValue !== undefined) {
|
|
1662
|
+
limitClause += ` OFFSET ${this.offsetValue}`;
|
|
1663
|
+
}
|
|
1664
|
+
// Build final query with CTEs
|
|
1665
|
+
let finalQuery = '';
|
|
1666
|
+
const allCtes = [];
|
|
1667
|
+
// Add user-defined CTEs (from .with() method)
|
|
1668
|
+
// Note: CTE params were already added to context.allParams at the start of buildQuery
|
|
1669
|
+
for (const cte of this.ctes) {
|
|
1670
|
+
allCtes.push(`"${cte.name}" AS (${cte.query})`);
|
|
1671
|
+
}
|
|
1672
|
+
// Add generated CTEs (from collection queries)
|
|
1673
|
+
if (context.ctes.size > 0) {
|
|
1674
|
+
for (const [cteName, { sql }] of context.ctes.entries()) {
|
|
1675
|
+
allCtes.push(`"${cteName}" AS (${sql})`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (allCtes.length > 0) {
|
|
1679
|
+
finalQuery = `WITH ${allCtes.join(', ')}\n`;
|
|
1680
|
+
}
|
|
1681
|
+
// Build main query
|
|
1682
|
+
const qualifiedTableName = this.getQualifiedTableName(this.schema.name, this.schema.schema);
|
|
1683
|
+
let fromClause = `FROM ${qualifiedTableName}`;
|
|
1684
|
+
// Add manual JOINs (from leftJoin/innerJoin methods)
|
|
1685
|
+
for (const manualJoin of this.manualJoins) {
|
|
1686
|
+
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1687
|
+
// Build ON condition
|
|
1688
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1689
|
+
const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
|
|
1690
|
+
context.paramCounter += condParams.length;
|
|
1691
|
+
context.allParams.push(...condParams);
|
|
1692
|
+
// Check if this is a CTE join
|
|
1693
|
+
if (manualJoin.cte) {
|
|
1694
|
+
// Join with CTE - use CTE name directly
|
|
1695
|
+
fromClause += `\n${joinTypeStr} "${manualJoin.cte.name}" ON ${condSql}`;
|
|
1696
|
+
}
|
|
1697
|
+
else if (manualJoin.isSubquery && manualJoin.subquery) {
|
|
1698
|
+
// Build the subquery SQL
|
|
1699
|
+
const subqueryBuildContext = {
|
|
1700
|
+
paramCounter: context.paramCounter,
|
|
1701
|
+
params: context.allParams,
|
|
1702
|
+
};
|
|
1703
|
+
const subquerySql = manualJoin.subquery.buildSql(subqueryBuildContext);
|
|
1704
|
+
context.paramCounter = subqueryBuildContext.paramCounter;
|
|
1705
|
+
fromClause += `\n${joinTypeStr} (${subquerySql}) AS "${manualJoin.alias}" ON ${condSql}`;
|
|
1706
|
+
}
|
|
1707
|
+
else {
|
|
1708
|
+
// Regular table join
|
|
1709
|
+
fromClause += `\n${joinTypeStr} "${manualJoin.table}" AS "${manualJoin.alias}" ON ${condSql}`;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
// Add JOINs for single navigation (references)
|
|
1713
|
+
for (const join of joins) {
|
|
1714
|
+
const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1715
|
+
// Build ON clause for the join
|
|
1716
|
+
const onConditions = [];
|
|
1717
|
+
for (let i = 0; i < join.foreignKeys.length; i++) {
|
|
1718
|
+
const fk = join.foreignKeys[i];
|
|
1719
|
+
const match = join.matches[i];
|
|
1720
|
+
onConditions.push(`"${this.schema.name}"."${fk}" = "${join.alias}"."${match}"`);
|
|
1721
|
+
}
|
|
1722
|
+
// Use schema-qualified table name if schema is specified
|
|
1723
|
+
const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
1724
|
+
fromClause += `\n${joinType} ${joinTableName} AS "${join.alias}" ON ${onConditions.join(' AND ')}`;
|
|
1725
|
+
}
|
|
1726
|
+
// Join CTEs for collections
|
|
1727
|
+
for (const { cteName } of collectionFields) {
|
|
1728
|
+
fromClause += `\nLEFT JOIN "${cteName}" ON "${cteName}".parent_id = ${qualifiedTableName}.id`;
|
|
1729
|
+
}
|
|
1730
|
+
// Add DISTINCT if needed
|
|
1731
|
+
const distinctClause = this.isDistinct ? 'DISTINCT ' : '';
|
|
1732
|
+
finalQuery += `SELECT ${distinctClause}${selectParts.join(', ')}\n${fromClause}\n${whereClause}\n${orderByClause}\n${limitClause}`.trim();
|
|
1733
|
+
return {
|
|
1734
|
+
sql: finalQuery,
|
|
1735
|
+
params: context.allParams,
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Transform database results
|
|
1740
|
+
*/
|
|
1741
|
+
transformResults(rows, selection) {
|
|
1742
|
+
return rows.map(row => {
|
|
1743
|
+
const result = {};
|
|
1744
|
+
// First, copy navigation property placeholders from selection
|
|
1745
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
1746
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
1747
|
+
// Empty array placeholder for collection navigation
|
|
1748
|
+
result[key] = [];
|
|
1749
|
+
}
|
|
1750
|
+
else if (value === undefined) {
|
|
1751
|
+
// Undefined placeholder for reference navigation
|
|
1752
|
+
result[key] = undefined;
|
|
1753
|
+
}
|
|
1754
|
+
else if (value && typeof value === 'object' && !('__dbColumnName' in value)) {
|
|
1755
|
+
// Check if it's a navigation property mock (object with getters)
|
|
1756
|
+
const props = Object.getOwnPropertyNames(value);
|
|
1757
|
+
if (props.length > 0) {
|
|
1758
|
+
const firstProp = props[0];
|
|
1759
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, firstProp);
|
|
1760
|
+
if (descriptor && descriptor.get) {
|
|
1761
|
+
// This is a navigation mock - add undefined placeholder
|
|
1762
|
+
result[key] = undefined;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
// Then process actual data fields
|
|
1768
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
1769
|
+
// Skip if we already set this key as a navigation placeholder
|
|
1770
|
+
// UNLESS there's actual data for this key in the row (e.g., from json_build_object)
|
|
1771
|
+
if (key in result && (result[key] === undefined || Array.isArray(result[key])) && !(key in row && row[key] !== undefined && row[key] !== null)) {
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
1775
|
+
// Check if this is a scalar aggregation (count, sum, max, min)
|
|
1776
|
+
const isScalarAgg = value instanceof CollectionQueryBuilder && value.isScalarAggregation();
|
|
1777
|
+
if (isScalarAgg) {
|
|
1778
|
+
// For scalar aggregations, return the value directly
|
|
1779
|
+
// For COUNT, convertValue will handle numeric conversion (NULL is already COALESCE'd to 0 in SQL)
|
|
1780
|
+
// For MAX/MIN/SUM, we want to keep NULL as null (not undefined)
|
|
1781
|
+
const aggregationType = value.getAggregationType();
|
|
1782
|
+
if (aggregationType === 'COUNT') {
|
|
1783
|
+
result[key] = this.convertValue(row[key]);
|
|
1784
|
+
}
|
|
1785
|
+
else {
|
|
1786
|
+
// For MAX/MIN/SUM, preserve NULL and convert numeric strings to numbers
|
|
1787
|
+
const rawValue = row[key];
|
|
1788
|
+
if (rawValue === null) {
|
|
1789
|
+
result[key] = null;
|
|
1790
|
+
}
|
|
1791
|
+
else if (typeof rawValue === 'string' && /^-?\d+(\.\d+)?$/.test(rawValue)) {
|
|
1792
|
+
result[key] = Number(rawValue);
|
|
1793
|
+
}
|
|
1794
|
+
else {
|
|
1795
|
+
result[key] = rawValue;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
else {
|
|
1800
|
+
// Check if this is a flattened array result (toNumberList/toStringList)
|
|
1801
|
+
const isArrayAgg = value && typeof value === 'object' && 'isArrayAggregation' in value && value.isArrayAggregation();
|
|
1802
|
+
if (isArrayAgg) {
|
|
1803
|
+
// For flattened arrays, PostgreSQL returns a native array - use it directly
|
|
1804
|
+
result[key] = row[key] || [];
|
|
1805
|
+
}
|
|
1806
|
+
else {
|
|
1807
|
+
// Parse JSON array from CTE (both CollectionQueryBuilder and CollectionResult are treated the same at runtime)
|
|
1808
|
+
const collectionItems = row[key] || [];
|
|
1809
|
+
// Apply fromDriver mappers to collection items if needed
|
|
1810
|
+
if (value instanceof CollectionQueryBuilder) {
|
|
1811
|
+
result[key] = this.transformCollectionItems(collectionItems, value);
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
result[key] = collectionItems;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
else if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
1820
|
+
// SqlFragment with custom mapper (check this BEFORE FieldRef to handle subquery/CTE fields with mappers)
|
|
1821
|
+
let mapper = value.getMapper();
|
|
1822
|
+
const rawValue = row[key];
|
|
1823
|
+
if (mapper && rawValue !== null && rawValue !== undefined) {
|
|
1824
|
+
// If mapper is a CustomTypeBuilder, get the actual type
|
|
1825
|
+
if (typeof mapper.getType === 'function') {
|
|
1826
|
+
mapper = mapper.getType();
|
|
1827
|
+
}
|
|
1828
|
+
// Apply the fromDriver transformation
|
|
1829
|
+
if (typeof mapper.fromDriver === 'function') {
|
|
1830
|
+
result[key] = mapper.fromDriver(rawValue);
|
|
1831
|
+
}
|
|
1832
|
+
else {
|
|
1833
|
+
// Fallback if fromDriver doesn't exist
|
|
1834
|
+
result[key] = this.convertValue(rawValue);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
else {
|
|
1838
|
+
// No mapper or null value - convert normally
|
|
1839
|
+
result[key] = this.convertValue(rawValue);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
|
|
1843
|
+
// FieldRef object - check if it has a custom mapper
|
|
1844
|
+
const fieldName = value.__fieldName;
|
|
1845
|
+
const column = this.schema.columns[fieldName];
|
|
1846
|
+
if (column) {
|
|
1847
|
+
const config = column.build();
|
|
1848
|
+
// Apply fromDriver mapper if present, convert null to undefined
|
|
1849
|
+
const rawValue = row[key];
|
|
1850
|
+
result[key] = rawValue === null
|
|
1851
|
+
? undefined
|
|
1852
|
+
: (config.mapper ? config.mapper.fromDriver(rawValue) : rawValue);
|
|
1853
|
+
}
|
|
1854
|
+
else {
|
|
1855
|
+
// Convert null to undefined for fields from joined tables
|
|
1856
|
+
// Also convert numeric strings to numbers for scalar subqueries (PostgreSQL returns NUMERIC as string)
|
|
1857
|
+
result[key] = this.convertValue(row[key]);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
else {
|
|
1861
|
+
// Convert null to undefined for all other values
|
|
1862
|
+
// Also convert numeric strings to numbers for scalar subqueries (PostgreSQL returns NUMERIC as string)
|
|
1863
|
+
result[key] = this.convertValue(row[key]);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return result;
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Convert database values: null to undefined, numeric strings to numbers
|
|
1871
|
+
*/
|
|
1872
|
+
convertValue(value) {
|
|
1873
|
+
if (value === null) {
|
|
1874
|
+
return undefined;
|
|
1875
|
+
}
|
|
1876
|
+
// Check if it's a numeric string (PostgreSQL NUMERIC type)
|
|
1877
|
+
// This handles scalar subqueries with aggregates like AVG, SUM, etc.
|
|
1878
|
+
if (typeof value === 'string' && /^-?\d+(\.\d+)?$/.test(value)) {
|
|
1879
|
+
const num = Number(value);
|
|
1880
|
+
if (!isNaN(num)) {
|
|
1881
|
+
return num;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return value;
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Transform collection items applying fromDriver mappers
|
|
1888
|
+
*/
|
|
1889
|
+
transformCollectionItems(items, collectionBuilder) {
|
|
1890
|
+
const targetSchema = collectionBuilder.getTargetTableSchema();
|
|
1891
|
+
if (!targetSchema) {
|
|
1892
|
+
return items;
|
|
1893
|
+
}
|
|
1894
|
+
return items.map(item => {
|
|
1895
|
+
const transformedItem = {};
|
|
1896
|
+
for (const [key, value] of Object.entries(item)) {
|
|
1897
|
+
// Find the column in target schema
|
|
1898
|
+
const column = targetSchema.columns[key];
|
|
1899
|
+
if (column) {
|
|
1900
|
+
const config = column.build();
|
|
1901
|
+
// Apply fromDriver mapper if present
|
|
1902
|
+
transformedItem[key] = config.mapper
|
|
1903
|
+
? config.mapper.fromDriver(value)
|
|
1904
|
+
: value;
|
|
1905
|
+
}
|
|
1906
|
+
else {
|
|
1907
|
+
transformedItem[key] = value;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
return transformedItem;
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Build aggregation query (MIN, MAX, SUM)
|
|
1915
|
+
*/
|
|
1916
|
+
buildAggregationQuery(aggregation, fieldToAggregate, context) {
|
|
1917
|
+
// Extract the field name from FieldRef object
|
|
1918
|
+
let fieldName;
|
|
1919
|
+
let tableAlias = this.schema.name;
|
|
1920
|
+
if (typeof fieldToAggregate === 'object' && fieldToAggregate !== null && '__dbColumnName' in fieldToAggregate) {
|
|
1921
|
+
fieldName = fieldToAggregate.__dbColumnName;
|
|
1922
|
+
if ('__tableAlias' in fieldToAggregate && fieldToAggregate.__tableAlias) {
|
|
1923
|
+
tableAlias = fieldToAggregate.__tableAlias;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
else if (typeof fieldToAggregate === 'string') {
|
|
1927
|
+
fieldName = fieldToAggregate;
|
|
1928
|
+
}
|
|
1929
|
+
else {
|
|
1930
|
+
throw new Error('Aggregation selector must return a field reference');
|
|
1931
|
+
}
|
|
1932
|
+
// Build WHERE clause
|
|
1933
|
+
let whereClause = '';
|
|
1934
|
+
if (this.whereCond) {
|
|
1935
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1936
|
+
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
1937
|
+
whereClause = `WHERE ${sql}`;
|
|
1938
|
+
context.paramCounter += params.length;
|
|
1939
|
+
context.allParams.push(...params);
|
|
1940
|
+
}
|
|
1941
|
+
// Build FROM clause with JOINs
|
|
1942
|
+
let fromClause = `FROM "${this.schema.name}"`;
|
|
1943
|
+
// Add manual JOINs
|
|
1944
|
+
for (const manualJoin of this.manualJoins) {
|
|
1945
|
+
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1946
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1947
|
+
const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
|
|
1948
|
+
context.paramCounter += condParams.length;
|
|
1949
|
+
context.allParams.push(...condParams);
|
|
1950
|
+
// Check if this is a subquery join
|
|
1951
|
+
if (manualJoin.isSubquery && manualJoin.subquery) {
|
|
1952
|
+
const subqueryBuildContext = {
|
|
1953
|
+
paramCounter: context.paramCounter,
|
|
1954
|
+
params: context.allParams,
|
|
1955
|
+
};
|
|
1956
|
+
const subquerySql = manualJoin.subquery.buildSql(subqueryBuildContext);
|
|
1957
|
+
context.paramCounter = subqueryBuildContext.paramCounter;
|
|
1958
|
+
fromClause += `\n${joinTypeStr} (${subquerySql}) AS "${manualJoin.alias}" ON ${condSql}`;
|
|
1959
|
+
}
|
|
1960
|
+
else {
|
|
1961
|
+
fromClause += `\n${joinTypeStr} "${manualJoin.table}" AS "${manualJoin.alias}" ON ${condSql}`;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const sql = `SELECT ${aggregation}("${tableAlias}"."${fieldName}") as result\n${fromClause}\n${whereClause}`.trim();
|
|
1965
|
+
return {
|
|
1966
|
+
sql,
|
|
1967
|
+
params: context.allParams,
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Build count query
|
|
1972
|
+
*/
|
|
1973
|
+
buildCountQuery(context) {
|
|
1974
|
+
// Build WHERE clause
|
|
1975
|
+
let whereClause = '';
|
|
1976
|
+
if (this.whereCond) {
|
|
1977
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1978
|
+
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
1979
|
+
whereClause = `WHERE ${sql}`;
|
|
1980
|
+
context.paramCounter += params.length;
|
|
1981
|
+
context.allParams.push(...params);
|
|
1982
|
+
}
|
|
1983
|
+
// Build FROM clause with JOINs
|
|
1984
|
+
let fromClause = `FROM "${this.schema.name}"`;
|
|
1985
|
+
// Add manual JOINs
|
|
1986
|
+
for (const manualJoin of this.manualJoins) {
|
|
1987
|
+
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
1988
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1989
|
+
const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
|
|
1990
|
+
context.paramCounter += condParams.length;
|
|
1991
|
+
context.allParams.push(...condParams);
|
|
1992
|
+
// Check if this is a subquery join
|
|
1993
|
+
if (manualJoin.isSubquery && manualJoin.subquery) {
|
|
1994
|
+
const subqueryBuildContext = {
|
|
1995
|
+
paramCounter: context.paramCounter,
|
|
1996
|
+
params: context.allParams,
|
|
1997
|
+
};
|
|
1998
|
+
const subquerySql = manualJoin.subquery.buildSql(subqueryBuildContext);
|
|
1999
|
+
context.paramCounter = subqueryBuildContext.paramCounter;
|
|
2000
|
+
fromClause += `\n${joinTypeStr} (${subquerySql}) AS "${manualJoin.alias}" ON ${condSql}`;
|
|
2001
|
+
}
|
|
2002
|
+
else {
|
|
2003
|
+
fromClause += `\n${joinTypeStr} "${manualJoin.table}" AS "${manualJoin.alias}" ON ${condSql}`;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
const sql = `SELECT COUNT(*) as count\n${fromClause}\n${whereClause}`.trim();
|
|
2007
|
+
return {
|
|
2008
|
+
sql,
|
|
2009
|
+
params: context.allParams,
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Convert this query to a subquery that can be used in WHERE, SELECT, JOIN, or FROM clauses
|
|
2014
|
+
*
|
|
2015
|
+
* @template TMode - 'scalar' for single value, 'array' for column list, 'table' for full rows
|
|
2016
|
+
* @returns Subquery that maintains type safety
|
|
2017
|
+
*
|
|
2018
|
+
* @example
|
|
2019
|
+
* // Scalar subquery (returns single value)
|
|
2020
|
+
* const avgAge = db.users.select(u => u.age).asSubquery('scalar');
|
|
2021
|
+
*
|
|
2022
|
+
* // Array subquery (returns list of values for IN clause)
|
|
2023
|
+
* const activeUserIds = db.users
|
|
2024
|
+
* .where(u => eq(u.isActive, true))
|
|
2025
|
+
* .select(u => u.id)
|
|
2026
|
+
* .asSubquery('array');
|
|
2027
|
+
*
|
|
2028
|
+
* // Table subquery (returns rows for FROM or JOIN)
|
|
2029
|
+
* const activeUsers = db.users
|
|
2030
|
+
* .where(u => eq(u.isActive, true))
|
|
2031
|
+
* .select(u => ({ id: u.id, name: u.username }))
|
|
2032
|
+
* .asSubquery('table');
|
|
2033
|
+
*/
|
|
2034
|
+
asSubquery(mode = 'table') {
|
|
2035
|
+
// Create a function that builds the subquery SQL when called
|
|
2036
|
+
const sqlBuilder = (outerContext) => {
|
|
2037
|
+
// Create a fresh context for this subquery
|
|
2038
|
+
const context = {
|
|
2039
|
+
ctes: new Map(),
|
|
2040
|
+
cteCounter: 0,
|
|
2041
|
+
paramCounter: outerContext.paramCounter,
|
|
2042
|
+
allParams: outerContext.params,
|
|
2043
|
+
executor: this.executor,
|
|
2044
|
+
};
|
|
2045
|
+
// Analyze the selector to extract nested queries
|
|
2046
|
+
const mockRow = this.createMockRow();
|
|
2047
|
+
const selectionResult = this.selector(mockRow);
|
|
2048
|
+
// Build the query
|
|
2049
|
+
const { sql } = this.buildQuery(selectionResult, context);
|
|
2050
|
+
// Update the outer context's param counter
|
|
2051
|
+
outerContext.paramCounter = context.paramCounter;
|
|
2052
|
+
return sql;
|
|
2053
|
+
};
|
|
2054
|
+
// For table subqueries, preserve the selection metadata (includes SqlFragments with mappers)
|
|
2055
|
+
let selectionMetadata;
|
|
2056
|
+
if (mode === 'table') {
|
|
2057
|
+
const mockRow = this.createMockRow();
|
|
2058
|
+
selectionMetadata = this.selector(mockRow);
|
|
2059
|
+
}
|
|
2060
|
+
return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
exports.SelectQueryBuilder = SelectQueryBuilder;
|
|
2064
|
+
/**
|
|
2065
|
+
* Reference query builder for single navigation (many-to-one, one-to-one)
|
|
2066
|
+
*/
|
|
2067
|
+
class ReferenceQueryBuilder {
|
|
2068
|
+
constructor(relationName, targetTable, foreignKeys, matches, isMandatory, targetTableSchema, schemaRegistry) {
|
|
2069
|
+
this.relationName = relationName;
|
|
2070
|
+
this.targetTable = targetTable;
|
|
2071
|
+
this.foreignKeys = foreignKeys;
|
|
2072
|
+
this.matches = matches;
|
|
2073
|
+
this.isMandatory = isMandatory;
|
|
2074
|
+
this.targetTableSchema = targetTableSchema;
|
|
2075
|
+
this.schemaRegistry = schemaRegistry;
|
|
2076
|
+
// If targetTableSchema is not provided but we have a registry, look it up
|
|
2077
|
+
if (!this.targetTableSchema && this.schemaRegistry) {
|
|
2078
|
+
this.targetTableSchema = this.schemaRegistry.get(targetTable);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Get the alias to use for this reference in the query
|
|
2083
|
+
*/
|
|
2084
|
+
getAlias() {
|
|
2085
|
+
return this.relationName;
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Get target table name
|
|
2089
|
+
*/
|
|
2090
|
+
getTargetTable() {
|
|
2091
|
+
return this.targetTable;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Get foreign keys
|
|
2095
|
+
*/
|
|
2096
|
+
getForeignKeys() {
|
|
2097
|
+
return this.foreignKeys;
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Get matches
|
|
2101
|
+
*/
|
|
2102
|
+
getMatches() {
|
|
2103
|
+
return this.matches;
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Is this a mandatory relation (INNER JOIN vs LEFT JOIN)
|
|
2107
|
+
*/
|
|
2108
|
+
getIsMandatory() {
|
|
2109
|
+
return this.isMandatory;
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Get target table schema
|
|
2113
|
+
*/
|
|
2114
|
+
getTargetTableSchema() {
|
|
2115
|
+
return this.targetTableSchema;
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Create a mock object that exposes the target table's columns
|
|
2119
|
+
* This allows accessing related fields like: p.user.username
|
|
2120
|
+
*/
|
|
2121
|
+
createMockTargetRow() {
|
|
2122
|
+
if (this.targetTableSchema) {
|
|
2123
|
+
const mock = {};
|
|
2124
|
+
// Add columns
|
|
2125
|
+
for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
|
|
2126
|
+
const dbColumnName = colBuilder.build().name;
|
|
2127
|
+
Object.defineProperty(mock, colName, {
|
|
2128
|
+
get: () => ({
|
|
2129
|
+
__fieldName: colName,
|
|
2130
|
+
__dbColumnName: dbColumnName,
|
|
2131
|
+
__tableAlias: this.relationName, // Mark which table this belongs to
|
|
2132
|
+
}),
|
|
2133
|
+
enumerable: true,
|
|
2134
|
+
configurable: true,
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
// Add navigation properties (both collections and references)
|
|
2138
|
+
if (this.targetTableSchema.relations) {
|
|
2139
|
+
for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
|
|
2140
|
+
if (relConfig.type === 'many') {
|
|
2141
|
+
// Collection navigation
|
|
2142
|
+
Object.defineProperty(mock, relName, {
|
|
2143
|
+
get: () => {
|
|
2144
|
+
// Don't call build() - it returns schema without relations
|
|
2145
|
+
// Instead, pass undefined and let CollectionQueryBuilder look it up from registry
|
|
2146
|
+
const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
|
|
2147
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, undefined, // Don't pass schema, force registry lookup
|
|
2148
|
+
this.schemaRegistry // Pass schema registry for nested resolution
|
|
2149
|
+
);
|
|
2150
|
+
},
|
|
2151
|
+
enumerable: true,
|
|
2152
|
+
configurable: true,
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
else {
|
|
2156
|
+
// Reference navigation
|
|
2157
|
+
Object.defineProperty(mock, relName, {
|
|
2158
|
+
get: () => {
|
|
2159
|
+
// Don't call build() - it returns schema without relations
|
|
2160
|
+
// Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
|
|
2161
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
|
|
2162
|
+
this.schemaRegistry // Pass schema registry for nested resolution
|
|
2163
|
+
);
|
|
2164
|
+
return refBuilder.createMockTargetRow();
|
|
2165
|
+
},
|
|
2166
|
+
enumerable: true,
|
|
2167
|
+
configurable: true,
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return mock;
|
|
2173
|
+
}
|
|
2174
|
+
else {
|
|
2175
|
+
// Fallback: generic proxy
|
|
2176
|
+
const handler = {
|
|
2177
|
+
get: (target, prop) => ({
|
|
2178
|
+
__fieldName: prop,
|
|
2179
|
+
__dbColumnName: prop,
|
|
2180
|
+
__tableAlias: this.relationName,
|
|
2181
|
+
}),
|
|
2182
|
+
};
|
|
2183
|
+
return new Proxy({}, handler);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
exports.ReferenceQueryBuilder = ReferenceQueryBuilder;
|
|
2188
|
+
/**
|
|
2189
|
+
* Collection query builder for nested queries
|
|
2190
|
+
*/
|
|
2191
|
+
class CollectionQueryBuilder {
|
|
2192
|
+
constructor(relationName, targetTable, foreignKey, sourceTable, targetTableSchema, schemaRegistry) {
|
|
2193
|
+
this.orderByFields = [];
|
|
2194
|
+
this.isMarkedAsList = false;
|
|
2195
|
+
this.isDistinct = false;
|
|
2196
|
+
this.relationName = relationName;
|
|
2197
|
+
this.targetTable = targetTable;
|
|
2198
|
+
this.targetTableSchema = targetTableSchema;
|
|
2199
|
+
this.foreignKey = foreignKey;
|
|
2200
|
+
this.sourceTable = sourceTable;
|
|
2201
|
+
this.schemaRegistry = schemaRegistry;
|
|
2202
|
+
// If targetTableSchema is not provided but we have a registry, look it up
|
|
2203
|
+
if (!this.targetTableSchema && this.schemaRegistry) {
|
|
2204
|
+
this.targetTableSchema = this.schemaRegistry.get(targetTable);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Select specific fields from collection items
|
|
2209
|
+
*/
|
|
2210
|
+
select(selector) {
|
|
2211
|
+
const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema);
|
|
2212
|
+
newBuilder.selector = selector;
|
|
2213
|
+
newBuilder.whereCond = this.whereCond;
|
|
2214
|
+
newBuilder.limitValue = this.limitValue;
|
|
2215
|
+
newBuilder.offsetValue = this.offsetValue;
|
|
2216
|
+
newBuilder.orderByFields = this.orderByFields;
|
|
2217
|
+
newBuilder.asName = this.asName;
|
|
2218
|
+
newBuilder.isDistinct = this.isDistinct;
|
|
2219
|
+
return newBuilder;
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Select distinct fields from collection items
|
|
2223
|
+
*/
|
|
2224
|
+
selectDistinct(selector) {
|
|
2225
|
+
const newBuilder = this.select(selector);
|
|
2226
|
+
newBuilder.isDistinct = true;
|
|
2227
|
+
return newBuilder;
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Filter collection items
|
|
2231
|
+
*/
|
|
2232
|
+
where(condition) {
|
|
2233
|
+
// Create mock item with proper schema if available
|
|
2234
|
+
const mockItem = this.createMockItem();
|
|
2235
|
+
this.whereCond = condition(mockItem);
|
|
2236
|
+
return this;
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Create a mock item for the target table with proper typing
|
|
2240
|
+
*/
|
|
2241
|
+
createMockItem() {
|
|
2242
|
+
// Performance: Return cached mock if available
|
|
2243
|
+
if (this._cachedMockItem) {
|
|
2244
|
+
return this._cachedMockItem;
|
|
2245
|
+
}
|
|
2246
|
+
if (this.targetTableSchema) {
|
|
2247
|
+
// If we have schema information, create a properly typed mock
|
|
2248
|
+
const mock = {};
|
|
2249
|
+
// Performance: Build column configs once and cache them
|
|
2250
|
+
const columnEntries = Object.entries(this.targetTableSchema.columns);
|
|
2251
|
+
const columnConfigs = new Map();
|
|
2252
|
+
for (const [colName, colBuilder] of columnEntries) {
|
|
2253
|
+
columnConfigs.set(colName, colBuilder.build().name);
|
|
2254
|
+
}
|
|
2255
|
+
// Add columns
|
|
2256
|
+
for (const [colName, dbColumnName] of columnConfigs) {
|
|
2257
|
+
Object.defineProperty(mock, colName, {
|
|
2258
|
+
get: () => ({
|
|
2259
|
+
__fieldName: colName,
|
|
2260
|
+
__dbColumnName: dbColumnName,
|
|
2261
|
+
}),
|
|
2262
|
+
enumerable: true,
|
|
2263
|
+
configurable: true,
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
// Add navigation properties (both collections and references)
|
|
2267
|
+
if (this.targetTableSchema.relations) {
|
|
2268
|
+
for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
|
|
2269
|
+
if (relConfig.type === 'many') {
|
|
2270
|
+
// Collection navigation
|
|
2271
|
+
Object.defineProperty(mock, relName, {
|
|
2272
|
+
get: () => {
|
|
2273
|
+
// Don't call build() - it returns schema without relations
|
|
2274
|
+
const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
|
|
2275
|
+
return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, undefined, // Don't pass schema, force registry lookup
|
|
2276
|
+
this.schemaRegistry // Pass schema registry for nested resolution
|
|
2277
|
+
);
|
|
2278
|
+
},
|
|
2279
|
+
enumerable: true,
|
|
2280
|
+
configurable: true,
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
else {
|
|
2284
|
+
// Reference navigation
|
|
2285
|
+
Object.defineProperty(mock, relName, {
|
|
2286
|
+
get: () => {
|
|
2287
|
+
// Don't call build() - it returns schema without relations
|
|
2288
|
+
// Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
|
|
2289
|
+
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
|
|
2290
|
+
this.schemaRegistry // Pass schema registry for nested resolution
|
|
2291
|
+
);
|
|
2292
|
+
return refBuilder.createMockTargetRow();
|
|
2293
|
+
},
|
|
2294
|
+
enumerable: true,
|
|
2295
|
+
configurable: true,
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
// Cache the mock for reuse
|
|
2301
|
+
this._cachedMockItem = mock;
|
|
2302
|
+
return mock;
|
|
2303
|
+
}
|
|
2304
|
+
else {
|
|
2305
|
+
// Fallback: generic proxy (don't cache as it's dynamic)
|
|
2306
|
+
const handler = {
|
|
2307
|
+
get: (target, prop) => ({
|
|
2308
|
+
__fieldName: prop,
|
|
2309
|
+
__dbColumnName: prop,
|
|
2310
|
+
}),
|
|
2311
|
+
};
|
|
2312
|
+
return new Proxy({}, handler);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Limit collection items
|
|
2317
|
+
*/
|
|
2318
|
+
limit(count) {
|
|
2319
|
+
this.limitValue = count;
|
|
2320
|
+
return this;
|
|
2321
|
+
}
|
|
2322
|
+
/**
|
|
2323
|
+
* Offset collection items
|
|
2324
|
+
*/
|
|
2325
|
+
offset(count) {
|
|
2326
|
+
this.offsetValue = count;
|
|
2327
|
+
return this;
|
|
2328
|
+
}
|
|
2329
|
+
orderBy(selector) {
|
|
2330
|
+
const mockItem = this.createMockItem();
|
|
2331
|
+
const result = selector(mockItem);
|
|
2332
|
+
// Handle array of [field, direction] tuples
|
|
2333
|
+
if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
|
|
2334
|
+
for (const [fieldRef, direction] of result) {
|
|
2335
|
+
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
2336
|
+
this.orderByFields.push({
|
|
2337
|
+
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
2338
|
+
direction: direction || 'ASC'
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
// Handle array of fields (all ASC)
|
|
2344
|
+
else if (Array.isArray(result)) {
|
|
2345
|
+
for (const fieldRef of result) {
|
|
2346
|
+
if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
|
|
2347
|
+
this.orderByFields.push({
|
|
2348
|
+
field: fieldRef.__dbColumnName || fieldRef.__fieldName,
|
|
2349
|
+
direction: 'ASC'
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
// Handle single field
|
|
2355
|
+
else if (result && typeof result === 'object' && '__fieldName' in result) {
|
|
2356
|
+
this.orderByFields.push({
|
|
2357
|
+
field: result.__dbColumnName || result.__fieldName,
|
|
2358
|
+
direction: 'ASC'
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
return this;
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Get minimum value (supports magic SQL in selector)
|
|
2365
|
+
*/
|
|
2366
|
+
/**
|
|
2367
|
+
* Get minimum value (supports magic SQL in selector)
|
|
2368
|
+
* Returns SqlFragment for automatic type resolution in selectors
|
|
2369
|
+
*/
|
|
2370
|
+
min(selector) {
|
|
2371
|
+
if (selector && !this.selector) {
|
|
2372
|
+
const mockItem = this.createMockItem();
|
|
2373
|
+
this.selector = selector;
|
|
2374
|
+
}
|
|
2375
|
+
this.aggregationType = 'MIN';
|
|
2376
|
+
return this;
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Get maximum value (supports magic SQL in selector)
|
|
2380
|
+
* Returns SqlFragment for automatic type resolution in selectors
|
|
2381
|
+
*/
|
|
2382
|
+
max(selector) {
|
|
2383
|
+
if (selector && !this.selector) {
|
|
2384
|
+
const mockItem = this.createMockItem();
|
|
2385
|
+
this.selector = selector;
|
|
2386
|
+
}
|
|
2387
|
+
this.aggregationType = 'MAX';
|
|
2388
|
+
return this;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Get sum value (supports magic SQL in selector)
|
|
2392
|
+
* Returns SqlFragment for automatic type resolution in selectors
|
|
2393
|
+
*/
|
|
2394
|
+
sum(selector) {
|
|
2395
|
+
if (selector && !this.selector) {
|
|
2396
|
+
const mockItem = this.createMockItem();
|
|
2397
|
+
this.selector = selector;
|
|
2398
|
+
}
|
|
2399
|
+
this.aggregationType = 'SUM';
|
|
2400
|
+
return this;
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Get count of items
|
|
2404
|
+
* Returns SqlFragment for automatic type resolution in selectors
|
|
2405
|
+
*/
|
|
2406
|
+
count() {
|
|
2407
|
+
this.aggregationType = 'COUNT';
|
|
2408
|
+
return this;
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Flatten result to number array (for single-column selections)
|
|
2412
|
+
*/
|
|
2413
|
+
toNumberList(name) {
|
|
2414
|
+
if (name) {
|
|
2415
|
+
this.asName = name;
|
|
2416
|
+
}
|
|
2417
|
+
this.flattenResultType = 'number';
|
|
2418
|
+
this.isMarkedAsList = true;
|
|
2419
|
+
return this;
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Flatten result to string array (for single-column selections)
|
|
2423
|
+
*/
|
|
2424
|
+
toStringList(name) {
|
|
2425
|
+
if (name) {
|
|
2426
|
+
this.asName = name;
|
|
2427
|
+
}
|
|
2428
|
+
this.flattenResultType = 'string';
|
|
2429
|
+
this.isMarkedAsList = true;
|
|
2430
|
+
return this;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Specify the property name for the collection in the result
|
|
2434
|
+
* Marks this collection to be resolved as an array in the final result
|
|
2435
|
+
*/
|
|
2436
|
+
toList(name) {
|
|
2437
|
+
if (name) {
|
|
2438
|
+
this.asName = name;
|
|
2439
|
+
}
|
|
2440
|
+
this.isMarkedAsList = true;
|
|
2441
|
+
// Cast to CollectionResult for type inference
|
|
2442
|
+
// At runtime, this is still a CollectionQueryBuilder, but TypeScript sees it as CollectionResult
|
|
2443
|
+
return this;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Get target table schema
|
|
2447
|
+
*/
|
|
2448
|
+
getTargetTableSchema() {
|
|
2449
|
+
return this.targetTableSchema;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Check if this collection uses array aggregation (for flattened results)
|
|
2453
|
+
*/
|
|
2454
|
+
isArrayAggregation() {
|
|
2455
|
+
return this.flattenResultType !== undefined;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Check if this is a scalar aggregation (count, sum, max, min)
|
|
2459
|
+
*/
|
|
2460
|
+
isScalarAggregation() {
|
|
2461
|
+
return this.aggregationType !== undefined;
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Get the aggregation type
|
|
2465
|
+
*/
|
|
2466
|
+
getAggregationType() {
|
|
2467
|
+
return this.aggregationType;
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Get the flatten result type (for determining PostgreSQL array type)
|
|
2471
|
+
*/
|
|
2472
|
+
getFlattenResultType() {
|
|
2473
|
+
return this.flattenResultType;
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Build CTE for this collection query
|
|
2477
|
+
* Now delegates to collection strategy pattern
|
|
2478
|
+
*/
|
|
2479
|
+
buildCTE(context, client, parentIds) {
|
|
2480
|
+
// Determine strategy type - default to 'jsonb' if not specified
|
|
2481
|
+
const strategyType = context.collectionStrategy || 'jsonb';
|
|
2482
|
+
const strategy = collection_strategy_factory_1.CollectionStrategyFactory.getStrategy(strategyType);
|
|
2483
|
+
// Build selected fields configuration
|
|
2484
|
+
const selectedFieldConfigs = [];
|
|
2485
|
+
const localParams = [];
|
|
2486
|
+
// Step 1: Build field selection configuration
|
|
2487
|
+
if (this.selector) {
|
|
2488
|
+
const mockItem = this.createMockItem();
|
|
2489
|
+
const selectedFields = this.selector(mockItem);
|
|
2490
|
+
// Check if the selector returns a FieldRef directly (single field selection like p => p.title)
|
|
2491
|
+
if (typeof selectedFields === 'object' && selectedFields !== null && '__dbColumnName' in selectedFields) {
|
|
2492
|
+
// Single field selection - use the field name as both alias and expression
|
|
2493
|
+
const field = selectedFields;
|
|
2494
|
+
const dbColumnName = field.__dbColumnName;
|
|
2495
|
+
selectedFieldConfigs.push({
|
|
2496
|
+
alias: dbColumnName,
|
|
2497
|
+
expression: `"${dbColumnName}"`,
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
else {
|
|
2501
|
+
// Object selection - extract each field
|
|
2502
|
+
for (const [alias, field] of Object.entries(selectedFields)) {
|
|
2503
|
+
if (field instanceof conditions_1.SqlFragment) {
|
|
2504
|
+
// SQL Fragment - build the SQL expression
|
|
2505
|
+
const sqlBuildContext = {
|
|
2506
|
+
paramCounter: context.paramCounter,
|
|
2507
|
+
params: context.allParams,
|
|
2508
|
+
};
|
|
2509
|
+
const fragmentSql = field.buildSql(sqlBuildContext);
|
|
2510
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
2511
|
+
selectedFieldConfigs.push({
|
|
2512
|
+
alias,
|
|
2513
|
+
expression: fragmentSql,
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
2517
|
+
// FieldRef object - use database column name
|
|
2518
|
+
const dbColumnName = field.__dbColumnName;
|
|
2519
|
+
selectedFieldConfigs.push({
|
|
2520
|
+
alias,
|
|
2521
|
+
expression: `"${dbColumnName}"`,
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
else if (typeof field === 'string') {
|
|
2525
|
+
// Simple string reference (for backward compatibility)
|
|
2526
|
+
selectedFieldConfigs.push({
|
|
2527
|
+
alias,
|
|
2528
|
+
expression: `"${field}"`,
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
else {
|
|
2532
|
+
// Literal value or expression
|
|
2533
|
+
selectedFieldConfigs.push({
|
|
2534
|
+
alias,
|
|
2535
|
+
expression: `$${context.paramCounter++}`,
|
|
2536
|
+
});
|
|
2537
|
+
context.allParams.push(field);
|
|
2538
|
+
localParams.push(field);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
else {
|
|
2544
|
+
// No selector - select all fields (use * for now, strategy will handle it)
|
|
2545
|
+
selectedFieldConfigs.push({
|
|
2546
|
+
alias: '*',
|
|
2547
|
+
expression: '*',
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
// Step 2: Build WHERE clause SQL (without WHERE keyword)
|
|
2551
|
+
let whereClause;
|
|
2552
|
+
let whereParams;
|
|
2553
|
+
if (this.whereCond) {
|
|
2554
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
2555
|
+
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
2556
|
+
whereClause = sql;
|
|
2557
|
+
whereParams = params;
|
|
2558
|
+
context.paramCounter += params.length;
|
|
2559
|
+
localParams.push(...params);
|
|
2560
|
+
context.allParams.push(...params);
|
|
2561
|
+
}
|
|
2562
|
+
// Step 3: Build ORDER BY clause SQL (without ORDER BY keyword)
|
|
2563
|
+
let orderByClause;
|
|
2564
|
+
if (this.orderByFields.length > 0) {
|
|
2565
|
+
const orderParts = this.orderByFields.map(({ field, direction }) => {
|
|
2566
|
+
// Look up the database column name from the schema if available
|
|
2567
|
+
let dbColumnName = field;
|
|
2568
|
+
if (this.targetTableSchema && this.targetTableSchema.columns[field]) {
|
|
2569
|
+
const colBuilder = this.targetTableSchema.columns[field];
|
|
2570
|
+
dbColumnName = colBuilder.build().name;
|
|
2571
|
+
}
|
|
2572
|
+
return `"${dbColumnName}" ${direction}`;
|
|
2573
|
+
});
|
|
2574
|
+
orderByClause = orderParts.join(', ');
|
|
2575
|
+
}
|
|
2576
|
+
// Step 4: Determine aggregation type and field
|
|
2577
|
+
let aggregationType;
|
|
2578
|
+
let aggregateField;
|
|
2579
|
+
let arrayField;
|
|
2580
|
+
let defaultValue;
|
|
2581
|
+
if (this.aggregationType) {
|
|
2582
|
+
// Scalar aggregations: count, min, max, sum
|
|
2583
|
+
aggregationType = this.aggregationType.toLowerCase();
|
|
2584
|
+
// For aggregations other than COUNT, determine which field to aggregate
|
|
2585
|
+
if (this.aggregationType !== 'COUNT' && this.selector) {
|
|
2586
|
+
const mockItem = this.createMockItem();
|
|
2587
|
+
const selectedField = this.selector(mockItem);
|
|
2588
|
+
if (typeof selectedField === 'object' && selectedField !== null && '__dbColumnName' in selectedField) {
|
|
2589
|
+
aggregateField = selectedField.__dbColumnName;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
// Set default value based on aggregation type
|
|
2593
|
+
defaultValue = aggregationType === 'count' ? '0' : 'null';
|
|
2594
|
+
}
|
|
2595
|
+
else if (this.flattenResultType) {
|
|
2596
|
+
// Array aggregation for toNumberList/toStringList
|
|
2597
|
+
aggregationType = 'array';
|
|
2598
|
+
// Determine the field to aggregate from the selected fields
|
|
2599
|
+
if (selectedFieldConfigs.length > 0) {
|
|
2600
|
+
const firstField = selectedFieldConfigs[0];
|
|
2601
|
+
arrayField = firstField.alias;
|
|
2602
|
+
}
|
|
2603
|
+
// Use typed empty array literal (PostgreSQL will infer type from array_agg)
|
|
2604
|
+
defaultValue = "'{}'"; // Empty array literal
|
|
2605
|
+
}
|
|
2606
|
+
else {
|
|
2607
|
+
// JSONB aggregation (default)
|
|
2608
|
+
aggregationType = 'jsonb';
|
|
2609
|
+
defaultValue = "'[]'::jsonb";
|
|
2610
|
+
}
|
|
2611
|
+
// Step 5: Build CollectionAggregationConfig object
|
|
2612
|
+
const config = {
|
|
2613
|
+
relationName: this.relationName,
|
|
2614
|
+
targetTable: this.targetTable,
|
|
2615
|
+
foreignKey: this.foreignKey,
|
|
2616
|
+
sourceTable: this.sourceTable,
|
|
2617
|
+
parentIds, // Pass parent IDs for temp table strategy
|
|
2618
|
+
selectedFields: selectedFieldConfigs,
|
|
2619
|
+
whereClause,
|
|
2620
|
+
whereParams, // Pass WHERE clause parameters
|
|
2621
|
+
orderByClause,
|
|
2622
|
+
limitValue: this.limitValue,
|
|
2623
|
+
offsetValue: this.offsetValue,
|
|
2624
|
+
isDistinct: this.isDistinct,
|
|
2625
|
+
aggregationType,
|
|
2626
|
+
aggregateField,
|
|
2627
|
+
arrayField,
|
|
2628
|
+
defaultValue,
|
|
2629
|
+
counter: context.cteCounter++,
|
|
2630
|
+
};
|
|
2631
|
+
// Step 6: Call the strategy
|
|
2632
|
+
const result = strategy.buildAggregation(config, context, client);
|
|
2633
|
+
// Step 7: Return the result
|
|
2634
|
+
// For synchronous strategies (like JSONB), result is returned directly
|
|
2635
|
+
// For async strategies (like temp table), return the Promise
|
|
2636
|
+
// Callers need to handle both cases
|
|
2637
|
+
if (result instanceof Promise) {
|
|
2638
|
+
// Async strategy - return special marker with the promise
|
|
2639
|
+
return result;
|
|
2640
|
+
}
|
|
2641
|
+
// Synchronous strategy (JSONB/CTE)
|
|
2642
|
+
return {
|
|
2643
|
+
sql: result.sql,
|
|
2644
|
+
params: localParams,
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
exports.CollectionQueryBuilder = CollectionQueryBuilder;
|
|
2649
|
+
//# sourceMappingURL=query-builder.js.map
|