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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +196 -0
  3. package/dist/database/database-client.interface.d.ts +45 -0
  4. package/dist/database/database-client.interface.d.ts.map +1 -0
  5. package/dist/database/database-client.interface.js +20 -0
  6. package/dist/database/database-client.interface.js.map +1 -0
  7. package/dist/database/index.d.ts +5 -0
  8. package/dist/database/index.d.ts.map +1 -0
  9. package/dist/database/index.js +10 -0
  10. package/dist/database/index.js.map +1 -0
  11. package/dist/database/pg-client.d.ts +30 -0
  12. package/dist/database/pg-client.d.ts.map +1 -0
  13. package/dist/database/pg-client.js +76 -0
  14. package/dist/database/pg-client.js.map +1 -0
  15. package/dist/database/postgres-client.d.ts +44 -0
  16. package/dist/database/postgres-client.d.ts.map +1 -0
  17. package/dist/database/postgres-client.js +111 -0
  18. package/dist/database/postgres-client.js.map +1 -0
  19. package/dist/database/types.d.ts +200 -0
  20. package/dist/database/types.d.ts.map +1 -0
  21. package/dist/database/types.js +8 -0
  22. package/dist/database/types.js.map +1 -0
  23. package/dist/entity/base-entity.d.ts +21 -0
  24. package/dist/entity/base-entity.d.ts.map +1 -0
  25. package/dist/entity/base-entity.js +27 -0
  26. package/dist/entity/base-entity.js.map +1 -0
  27. package/dist/entity/db-column.d.ts +61 -0
  28. package/dist/entity/db-column.d.ts.map +1 -0
  29. package/dist/entity/db-column.js +35 -0
  30. package/dist/entity/db-column.js.map +1 -0
  31. package/dist/entity/db-context.d.ts +665 -0
  32. package/dist/entity/db-context.d.ts.map +1 -0
  33. package/dist/entity/db-context.js +1463 -0
  34. package/dist/entity/db-context.js.map +1 -0
  35. package/dist/entity/entity-base.d.ts +76 -0
  36. package/dist/entity/entity-base.d.ts.map +1 -0
  37. package/dist/entity/entity-base.js +42 -0
  38. package/dist/entity/entity-base.js.map +1 -0
  39. package/dist/entity/entity-builder.d.ts +171 -0
  40. package/dist/entity/entity-builder.d.ts.map +1 -0
  41. package/dist/entity/entity-builder.js +376 -0
  42. package/dist/entity/entity-builder.js.map +1 -0
  43. package/dist/entity/model-config.d.ts +18 -0
  44. package/dist/entity/model-config.d.ts.map +1 -0
  45. package/dist/entity/model-config.js +157 -0
  46. package/dist/entity/model-config.js.map +1 -0
  47. package/dist/index.d.ts +27 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +142 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/migration/db-schema-manager.d.ts +228 -0
  52. package/dist/migration/db-schema-manager.d.ts.map +1 -0
  53. package/dist/migration/db-schema-manager.js +1055 -0
  54. package/dist/migration/db-schema-manager.js.map +1 -0
  55. package/dist/migration/enum-migrator.d.ts +29 -0
  56. package/dist/migration/enum-migrator.d.ts.map +1 -0
  57. package/dist/migration/enum-migrator.js +137 -0
  58. package/dist/migration/enum-migrator.js.map +1 -0
  59. package/dist/query/collection-strategy.factory.d.ts +16 -0
  60. package/dist/query/collection-strategy.factory.d.ts.map +1 -0
  61. package/dist/query/collection-strategy.factory.js +37 -0
  62. package/dist/query/collection-strategy.factory.js.map +1 -0
  63. package/dist/query/collection-strategy.interface.d.ts +146 -0
  64. package/dist/query/collection-strategy.interface.d.ts.map +1 -0
  65. package/dist/query/collection-strategy.interface.js +3 -0
  66. package/dist/query/collection-strategy.interface.js.map +1 -0
  67. package/dist/query/conditions.d.ts +222 -0
  68. package/dist/query/conditions.d.ts.map +1 -0
  69. package/dist/query/conditions.js +446 -0
  70. package/dist/query/conditions.js.map +1 -0
  71. package/dist/query/cte-builder.d.ts +95 -0
  72. package/dist/query/cte-builder.d.ts.map +1 -0
  73. package/dist/query/cte-builder.js +172 -0
  74. package/dist/query/cte-builder.js.map +1 -0
  75. package/dist/query/grouped-query.d.ts +186 -0
  76. package/dist/query/grouped-query.d.ts.map +1 -0
  77. package/dist/query/grouped-query.js +588 -0
  78. package/dist/query/grouped-query.js.map +1 -0
  79. package/dist/query/join-builder.d.ts +106 -0
  80. package/dist/query/join-builder.d.ts.map +1 -0
  81. package/dist/query/join-builder.js +275 -0
  82. package/dist/query/join-builder.js.map +1 -0
  83. package/dist/query/query-builder.d.ts +543 -0
  84. package/dist/query/query-builder.d.ts.map +1 -0
  85. package/dist/query/query-builder.js +2649 -0
  86. package/dist/query/query-builder.js.map +1 -0
  87. package/dist/query/strategies/jsonb-collection-strategy.d.ts +51 -0
  88. package/dist/query/strategies/jsonb-collection-strategy.d.ts.map +1 -0
  89. package/dist/query/strategies/jsonb-collection-strategy.js +210 -0
  90. package/dist/query/strategies/jsonb-collection-strategy.js.map +1 -0
  91. package/dist/query/strategies/temptable-collection-strategy.d.ts +95 -0
  92. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -0
  93. package/dist/query/strategies/temptable-collection-strategy.js +456 -0
  94. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -0
  95. package/dist/query/subquery.d.ts +152 -0
  96. package/dist/query/subquery.d.ts.map +1 -0
  97. package/dist/query/subquery.js +206 -0
  98. package/dist/query/subquery.js.map +1 -0
  99. package/dist/schema/column-builder.d.ts +127 -0
  100. package/dist/schema/column-builder.d.ts.map +1 -0
  101. package/dist/schema/column-builder.js +184 -0
  102. package/dist/schema/column-builder.js.map +1 -0
  103. package/dist/schema/inference.d.ts +26 -0
  104. package/dist/schema/inference.d.ts.map +1 -0
  105. package/dist/schema/inference.js +3 -0
  106. package/dist/schema/inference.js.map +1 -0
  107. package/dist/schema/navigation.d.ts +215 -0
  108. package/dist/schema/navigation.d.ts.map +1 -0
  109. package/dist/schema/navigation.js +233 -0
  110. package/dist/schema/navigation.js.map +1 -0
  111. package/dist/schema/row-type.d.ts +26 -0
  112. package/dist/schema/row-type.d.ts.map +1 -0
  113. package/dist/schema/row-type.js +3 -0
  114. package/dist/schema/row-type.js.map +1 -0
  115. package/dist/schema/sequence-builder.d.ts +87 -0
  116. package/dist/schema/sequence-builder.d.ts.map +1 -0
  117. package/dist/schema/sequence-builder.js +123 -0
  118. package/dist/schema/sequence-builder.js.map +1 -0
  119. package/dist/schema/table-builder.d.ts +122 -0
  120. package/dist/schema/table-builder.d.ts.map +1 -0
  121. package/dist/schema/table-builder.js +132 -0
  122. package/dist/schema/table-builder.js.map +1 -0
  123. package/dist/schema/typed-schema.d.ts +22 -0
  124. package/dist/schema/typed-schema.d.ts.map +1 -0
  125. package/dist/schema/typed-schema.js +28 -0
  126. package/dist/schema/typed-schema.js.map +1 -0
  127. package/dist/types/column-types.d.ts +20 -0
  128. package/dist/types/column-types.d.ts.map +1 -0
  129. package/dist/types/column-types.js +14 -0
  130. package/dist/types/column-types.js.map +1 -0
  131. package/dist/types/custom-types.d.ts +85 -0
  132. package/dist/types/custom-types.d.ts.map +1 -0
  133. package/dist/types/custom-types.js +132 -0
  134. package/dist/types/custom-types.js.map +1 -0
  135. package/dist/types/enum-builder.d.ts +31 -0
  136. package/dist/types/enum-builder.d.ts.map +1 -0
  137. package/dist/types/enum-builder.js +46 -0
  138. package/dist/types/enum-builder.js.map +1 -0
  139. package/dist/types/metadata.d.ts +67 -0
  140. package/dist/types/metadata.d.ts.map +1 -0
  141. package/dist/types/metadata.js +57 -0
  142. package/dist/types/metadata.js.map +1 -0
  143. package/dist/types/type-mapper.d.ts +49 -0
  144. package/dist/types/type-mapper.d.ts.map +1 -0
  145. package/dist/types/type-mapper.js +49 -0
  146. package/dist/types/type-mapper.js.map +1 -0
  147. 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