linkgress-orm 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/entity/db-context.d.ts +15 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +18 -0
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +24 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/conditions.d.ts +45 -13
- package/dist/query/conditions.d.ts.map +1 -1
- package/dist/query/conditions.js +68 -4
- package/dist/query/conditions.js.map +1 -1
- package/dist/query/prepared-query.d.ts +66 -0
- package/dist/query/prepared-query.d.ts.map +1 -0
- package/dist/query/prepared-query.js +86 -0
- package/dist/query/prepared-query.js.map +1 -0
- package/dist/query/query-builder.d.ts +50 -0
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +299 -24
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts +18 -0
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +98 -2
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +5 -0
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +96 -2
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +21 -8
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/package.json +3 -2
|
@@ -6,6 +6,7 @@ exports.getRelationEntriesForSchema = getRelationEntriesForSchema;
|
|
|
6
6
|
exports.getTargetSchemaForRelation = getTargetSchemaForRelation;
|
|
7
7
|
exports.createNestedFieldRefProxy = createNestedFieldRefProxy;
|
|
8
8
|
const conditions_1 = require("./conditions");
|
|
9
|
+
const prepared_query_1 = require("./prepared-query");
|
|
9
10
|
const db_context_1 = require("../entity/db-context");
|
|
10
11
|
const query_utils_1 = require("./query-utils");
|
|
11
12
|
const subquery_1 = require("./subquery");
|
|
@@ -1200,7 +1201,28 @@ class SelectQueryBuilder {
|
|
|
1200
1201
|
for (const collection of collections) {
|
|
1201
1202
|
const resultMap = collectionResults.get(collection.name);
|
|
1202
1203
|
const parentId = baseRow.__pk_id;
|
|
1203
|
-
const
|
|
1204
|
+
const rawData = resultMap?.get(parentId);
|
|
1205
|
+
// Check if this is a single result (firstOrDefault) - return first item or null
|
|
1206
|
+
const isSingleResult = collection.builder.isSingleResult();
|
|
1207
|
+
let collectionData;
|
|
1208
|
+
if (isSingleResult) {
|
|
1209
|
+
// For single results, rawData might already be an object (from temp table strategy)
|
|
1210
|
+
// or might be an array (from other strategies) that we need to extract the first item from
|
|
1211
|
+
if (rawData === undefined || rawData === null) {
|
|
1212
|
+
collectionData = null;
|
|
1213
|
+
}
|
|
1214
|
+
else if (Array.isArray(rawData)) {
|
|
1215
|
+
collectionData = rawData.length > 0 ? rawData[0] : null;
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
// Already a single object
|
|
1219
|
+
collectionData = rawData;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
// For list results, ensure we return an array
|
|
1224
|
+
collectionData = rawData || this.getDefaultValueForCollection(collection.builder);
|
|
1225
|
+
}
|
|
1204
1226
|
// Handle nested paths
|
|
1205
1227
|
if (collection.path.length > 0) {
|
|
1206
1228
|
// Navigate to the nested object and set the collection there
|
|
@@ -1348,7 +1370,28 @@ class SelectQueryBuilder {
|
|
|
1348
1370
|
for (const collection of collections) {
|
|
1349
1371
|
const resultMap = collectionResults.get(collection.name);
|
|
1350
1372
|
const parentId = baseRow.__pk_id;
|
|
1351
|
-
const
|
|
1373
|
+
const rawData = resultMap?.get(parentId);
|
|
1374
|
+
// Check if this is a single result (firstOrDefault) - return first item or null
|
|
1375
|
+
const isSingleResult = collection.builder.isSingleResult();
|
|
1376
|
+
let collectionData;
|
|
1377
|
+
if (isSingleResult) {
|
|
1378
|
+
// For single results, rawData might already be an object (from temp table strategy)
|
|
1379
|
+
// or might be an array (from other strategies) that we need to extract the first item from
|
|
1380
|
+
if (rawData === undefined || rawData === null) {
|
|
1381
|
+
collectionData = null;
|
|
1382
|
+
}
|
|
1383
|
+
else if (Array.isArray(rawData)) {
|
|
1384
|
+
collectionData = rawData.length > 0 ? rawData[0] : null;
|
|
1385
|
+
}
|
|
1386
|
+
else {
|
|
1387
|
+
// Already a single object
|
|
1388
|
+
collectionData = rawData;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
else {
|
|
1392
|
+
// For list results, ensure we return an array
|
|
1393
|
+
collectionData = rawData || [];
|
|
1394
|
+
}
|
|
1352
1395
|
// Handle nested paths
|
|
1353
1396
|
if (collection.path.length > 0) {
|
|
1354
1397
|
// Navigate to the nested object and set the collection there
|
|
@@ -1577,6 +1620,74 @@ class SelectQueryBuilder {
|
|
|
1577
1620
|
}
|
|
1578
1621
|
return result;
|
|
1579
1622
|
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Create a prepared query for efficient reusable parameterized execution.
|
|
1625
|
+
*
|
|
1626
|
+
* Prepared queries build the SQL once and allow multiple executions
|
|
1627
|
+
* with different parameter values. This is useful for:
|
|
1628
|
+
* 1. Query building optimization - Build SQL once, execute many times
|
|
1629
|
+
* 2. Type-safe placeholders - Named parameters with validation
|
|
1630
|
+
* 3. Developer ergonomics - Cleaner API for reusable queries
|
|
1631
|
+
*
|
|
1632
|
+
* @param name - A name for the prepared query (for debugging)
|
|
1633
|
+
* @returns PreparedQuery that can be executed multiple times with different parameters
|
|
1634
|
+
*
|
|
1635
|
+
* @example
|
|
1636
|
+
* ```typescript
|
|
1637
|
+
* // Create a prepared query with a placeholder
|
|
1638
|
+
* const getUserById = db.users
|
|
1639
|
+
* .where(u => eq(u.id, sql.placeholder('userId')))
|
|
1640
|
+
* .prepare('getUserById');
|
|
1641
|
+
*
|
|
1642
|
+
* // Execute multiple times with different values
|
|
1643
|
+
* const user1 = await getUserById.execute({ userId: 10 });
|
|
1644
|
+
* const user2 = await getUserById.execute({ userId: 20 });
|
|
1645
|
+
* ```
|
|
1646
|
+
*
|
|
1647
|
+
* @example
|
|
1648
|
+
* ```typescript
|
|
1649
|
+
* // Multiple placeholders
|
|
1650
|
+
* const searchUsers = db.users
|
|
1651
|
+
* .where(u => and(
|
|
1652
|
+
* gt(u.age, sql.placeholder('minAge')),
|
|
1653
|
+
* like(u.name, sql.placeholder('namePattern'))
|
|
1654
|
+
* ))
|
|
1655
|
+
* .prepare('searchUsers');
|
|
1656
|
+
*
|
|
1657
|
+
* await searchUsers.execute({ minAge: 18, namePattern: '%john%' });
|
|
1658
|
+
* ```
|
|
1659
|
+
*/
|
|
1660
|
+
prepare(name) {
|
|
1661
|
+
// Build query with placeholder tracking
|
|
1662
|
+
const context = {
|
|
1663
|
+
ctes: new Map(),
|
|
1664
|
+
cteCounter: 0,
|
|
1665
|
+
paramCounter: 1,
|
|
1666
|
+
allParams: [],
|
|
1667
|
+
placeholders: new Map(),
|
|
1668
|
+
collectionStrategy: this.collectionStrategy,
|
|
1669
|
+
executor: this.executor,
|
|
1670
|
+
};
|
|
1671
|
+
// Analyze the selector to extract nested queries
|
|
1672
|
+
const mockRow = this._createMockRow();
|
|
1673
|
+
const selectionResult = this.selector(mockRow);
|
|
1674
|
+
// Build the query - this populates context.placeholders
|
|
1675
|
+
const { sql, nestedPaths } = this.buildQuery(selectionResult, context);
|
|
1676
|
+
// Create transform function (closure over schema, selection, nestedPaths)
|
|
1677
|
+
const transformFn = (rows) => {
|
|
1678
|
+
// If rawResult is enabled, return raw rows without any processing
|
|
1679
|
+
if (this.executor?.getOptions().rawResult) {
|
|
1680
|
+
return rows;
|
|
1681
|
+
}
|
|
1682
|
+
// Reconstruct nested objects from flat row data (if any)
|
|
1683
|
+
if (nestedPaths.size > 0) {
|
|
1684
|
+
rows = rows.map(row => this.reconstructNestedObjects(row, nestedPaths));
|
|
1685
|
+
}
|
|
1686
|
+
// Transform results
|
|
1687
|
+
return this.transformResults(rows, selectionResult);
|
|
1688
|
+
};
|
|
1689
|
+
return new prepared_query_1.PreparedQuery(sql, context.placeholders || new Map(), context.paramCounter - 1, this.client, transformFn, name);
|
|
1690
|
+
}
|
|
1580
1691
|
/**
|
|
1581
1692
|
* Delete records matching the current WHERE condition
|
|
1582
1693
|
* Returns a fluent builder that can be awaited directly or chained with .returning()
|
|
@@ -2702,10 +2813,14 @@ class SelectQueryBuilder {
|
|
|
2702
2813
|
let whereClause = '';
|
|
2703
2814
|
if (this.whereCond) {
|
|
2704
2815
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
2705
|
-
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
2816
|
+
const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
|
|
2706
2817
|
whereClause = `WHERE ${sql}`;
|
|
2707
|
-
context.paramCounter
|
|
2818
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
2708
2819
|
context.allParams.push(...params);
|
|
2820
|
+
// Update placeholders from the condition builder (for prepared statements)
|
|
2821
|
+
if (placeholders) {
|
|
2822
|
+
context.placeholders = placeholders;
|
|
2823
|
+
}
|
|
2709
2824
|
}
|
|
2710
2825
|
// Build ORDER BY clause
|
|
2711
2826
|
let orderByClause = '';
|
|
@@ -2761,9 +2876,12 @@ class SelectQueryBuilder {
|
|
|
2761
2876
|
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
2762
2877
|
// Build ON condition
|
|
2763
2878
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
2764
|
-
const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
|
|
2765
|
-
context.paramCounter
|
|
2879
|
+
const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
|
|
2880
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
2766
2881
|
context.allParams.push(...condParams);
|
|
2882
|
+
if (joinPlaceholders) {
|
|
2883
|
+
context.placeholders = joinPlaceholders;
|
|
2884
|
+
}
|
|
2767
2885
|
// Check if this is a CTE join
|
|
2768
2886
|
if (manualJoin.cte) {
|
|
2769
2887
|
// Join with CTE - use CTE name directly
|
|
@@ -2998,14 +3116,18 @@ class SelectQueryBuilder {
|
|
|
2998
3116
|
break;
|
|
2999
3117
|
}
|
|
3000
3118
|
case 9 /* FieldType.COLLECTION_SINGLE */: {
|
|
3001
|
-
// firstOrDefault() - return
|
|
3002
|
-
|
|
3003
|
-
if (
|
|
3004
|
-
|
|
3119
|
+
// firstOrDefault() - return single object or null
|
|
3120
|
+
// With CTE/LATERAL single result, rawValue is already a single object (not array)
|
|
3121
|
+
if (rawValue === null || rawValue === undefined) {
|
|
3122
|
+
result[key] = null;
|
|
3123
|
+
}
|
|
3124
|
+
else if (config.collectionBuilder) {
|
|
3125
|
+
// Transform the single item using collection mapper if available
|
|
3126
|
+
const transformedItems = this.transformCollectionItems([rawValue], config.collectionBuilder);
|
|
3005
3127
|
result[key] = transformedItems[0] ?? null;
|
|
3006
3128
|
}
|
|
3007
3129
|
else {
|
|
3008
|
-
result[key] =
|
|
3130
|
+
result[key] = rawValue;
|
|
3009
3131
|
}
|
|
3010
3132
|
break;
|
|
3011
3133
|
}
|
|
@@ -3123,6 +3245,18 @@ class SelectQueryBuilder {
|
|
|
3123
3245
|
}
|
|
3124
3246
|
}
|
|
3125
3247
|
}
|
|
3248
|
+
// Build cache of nested collection info (fields that are themselves nested collections)
|
|
3249
|
+
// This is used for recursive transformation of nested collection results
|
|
3250
|
+
const nestedCollectionCache = new Map();
|
|
3251
|
+
if (selectedFieldConfigs) {
|
|
3252
|
+
for (const field of selectedFieldConfigs) {
|
|
3253
|
+
if (field.nestedCollectionInfo) {
|
|
3254
|
+
nestedCollectionCache.set(field.alias, field.nestedCollectionInfo);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
// Get schema registry for nested collection transformation
|
|
3259
|
+
const schemaRegistry = collectionBuilder.getSchemaRegistry() || this.schemaRegistry;
|
|
3126
3260
|
// Transform items using pre-built mapper cache
|
|
3127
3261
|
const results = new Array(items.length);
|
|
3128
3262
|
let i = items.length;
|
|
@@ -3136,13 +3270,114 @@ class SelectQueryBuilder {
|
|
|
3136
3270
|
transformedItem[key] = mapper.fromDriver(value);
|
|
3137
3271
|
}
|
|
3138
3272
|
else {
|
|
3139
|
-
|
|
3273
|
+
// Check if this field is a nested collection that needs recursive transformation
|
|
3274
|
+
const nestedInfo = nestedCollectionCache.get(key);
|
|
3275
|
+
if (nestedInfo && value !== null && value !== undefined && schemaRegistry) {
|
|
3276
|
+
transformedItem[key] = this.transformNestedCollectionValue(value, nestedInfo, schemaRegistry);
|
|
3277
|
+
}
|
|
3278
|
+
else {
|
|
3279
|
+
transformedItem[key] = value;
|
|
3280
|
+
}
|
|
3140
3281
|
}
|
|
3141
3282
|
}
|
|
3142
3283
|
results[i] = transformedItem;
|
|
3143
3284
|
}
|
|
3144
3285
|
return results;
|
|
3145
3286
|
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Transform a nested collection value (from firstOrDefault or toList inside another collection)
|
|
3289
|
+
* Applies custom mappers to fields within the nested collection result.
|
|
3290
|
+
*/
|
|
3291
|
+
transformNestedCollectionValue(value, nestedInfo, schemaRegistry) {
|
|
3292
|
+
const nestedSchema = schemaRegistry.get(nestedInfo.targetTable);
|
|
3293
|
+
if (!nestedSchema?.columnMetadataCache) {
|
|
3294
|
+
return value; // No schema info, return as-is
|
|
3295
|
+
}
|
|
3296
|
+
const columnCache = nestedSchema.columnMetadataCache;
|
|
3297
|
+
const selectedFieldConfigs = nestedInfo.selectedFieldConfigs;
|
|
3298
|
+
// Build mapper cache for nested collection fields
|
|
3299
|
+
const mapperCache = new Map();
|
|
3300
|
+
// First, add mappers from selected field configs (for aliased/navigation fields)
|
|
3301
|
+
if (selectedFieldConfigs) {
|
|
3302
|
+
for (const field of selectedFieldConfigs) {
|
|
3303
|
+
if (field.propertyName) {
|
|
3304
|
+
let mapper = null;
|
|
3305
|
+
if (field.sourceTable) {
|
|
3306
|
+
// Navigation field - look up from source table's schema
|
|
3307
|
+
const navSchema = schemaRegistry.get(field.sourceTable);
|
|
3308
|
+
if (navSchema?.columnMetadataCache) {
|
|
3309
|
+
const cached = navSchema.columnMetadataCache.get(field.propertyName);
|
|
3310
|
+
if (cached?.hasMapper) {
|
|
3311
|
+
mapper = cached.mapper;
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
else {
|
|
3316
|
+
// Regular field from target schema
|
|
3317
|
+
const cached = columnCache.get(field.propertyName);
|
|
3318
|
+
if (cached?.hasMapper) {
|
|
3319
|
+
mapper = cached.mapper;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
if (mapper) {
|
|
3323
|
+
mapperCache.set(field.alias, mapper);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
// Also add direct property matches from target schema
|
|
3329
|
+
for (const [propertyName, cached] of columnCache) {
|
|
3330
|
+
if (!mapperCache.has(propertyName) && cached.hasMapper) {
|
|
3331
|
+
mapperCache.set(propertyName, cached.mapper);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
// Build cache for any deeply nested collections
|
|
3335
|
+
const deeplyNestedCache = new Map();
|
|
3336
|
+
if (selectedFieldConfigs) {
|
|
3337
|
+
for (const field of selectedFieldConfigs) {
|
|
3338
|
+
if (field.nestedCollectionInfo) {
|
|
3339
|
+
deeplyNestedCache.set(field.alias, field.nestedCollectionInfo);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
// Transform the value(s)
|
|
3344
|
+
const transformItem = (item) => {
|
|
3345
|
+
if (item === null || item === undefined) {
|
|
3346
|
+
return item;
|
|
3347
|
+
}
|
|
3348
|
+
const transformedItem = {};
|
|
3349
|
+
for (const key in item) {
|
|
3350
|
+
const fieldValue = item[key];
|
|
3351
|
+
const mapper = mapperCache.get(key);
|
|
3352
|
+
if (mapper) {
|
|
3353
|
+
transformedItem[key] = mapper.fromDriver(fieldValue);
|
|
3354
|
+
}
|
|
3355
|
+
else {
|
|
3356
|
+
// Check for deeply nested collections
|
|
3357
|
+
const deepNestedInfo = deeplyNestedCache.get(key);
|
|
3358
|
+
if (deepNestedInfo && fieldValue !== null && fieldValue !== undefined) {
|
|
3359
|
+
transformedItem[key] = this.transformNestedCollectionValue(fieldValue, deepNestedInfo, schemaRegistry);
|
|
3360
|
+
}
|
|
3361
|
+
else {
|
|
3362
|
+
transformedItem[key] = fieldValue;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
return transformedItem;
|
|
3367
|
+
};
|
|
3368
|
+
if (nestedInfo.isSingleResult) {
|
|
3369
|
+
// Single item (firstOrDefault)
|
|
3370
|
+
return transformItem(value);
|
|
3371
|
+
}
|
|
3372
|
+
else if (Array.isArray(value)) {
|
|
3373
|
+
// Array of items (toList)
|
|
3374
|
+
return value.map(transformItem);
|
|
3375
|
+
}
|
|
3376
|
+
else {
|
|
3377
|
+
// Single object that should be treated as single result
|
|
3378
|
+
return transformItem(value);
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3146
3381
|
/**
|
|
3147
3382
|
* Transform CTE aggregation items applying fromDriver mappers from selection metadata
|
|
3148
3383
|
*/
|
|
@@ -3233,10 +3468,13 @@ class SelectQueryBuilder {
|
|
|
3233
3468
|
let whereClause = '';
|
|
3234
3469
|
if (this.whereCond) {
|
|
3235
3470
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3236
|
-
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
3471
|
+
const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
|
|
3237
3472
|
whereClause = `WHERE ${sql}`;
|
|
3238
|
-
context.paramCounter
|
|
3473
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
3239
3474
|
context.allParams.push(...params);
|
|
3475
|
+
if (placeholders) {
|
|
3476
|
+
context.placeholders = placeholders;
|
|
3477
|
+
}
|
|
3240
3478
|
}
|
|
3241
3479
|
// Build FROM clause with JOINs
|
|
3242
3480
|
let fromClause = `FROM "${this.schema.name}"`;
|
|
@@ -3244,9 +3482,12 @@ class SelectQueryBuilder {
|
|
|
3244
3482
|
for (const manualJoin of this.manualJoins) {
|
|
3245
3483
|
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
3246
3484
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3247
|
-
const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
|
|
3248
|
-
context.paramCounter
|
|
3485
|
+
const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newJoinParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
|
|
3486
|
+
context.paramCounter = newJoinParamCounter; // Use returned counter (handles both params and placeholders)
|
|
3249
3487
|
context.allParams.push(...condParams);
|
|
3488
|
+
if (joinPlaceholders) {
|
|
3489
|
+
context.placeholders = joinPlaceholders;
|
|
3490
|
+
}
|
|
3250
3491
|
// Check if this is a subquery join
|
|
3251
3492
|
if (manualJoin.isSubquery && manualJoin.subquery) {
|
|
3252
3493
|
const subqueryBuildContext = {
|
|
@@ -3275,10 +3516,13 @@ class SelectQueryBuilder {
|
|
|
3275
3516
|
let whereClause = '';
|
|
3276
3517
|
if (this.whereCond) {
|
|
3277
3518
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3278
|
-
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
3519
|
+
const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
|
|
3279
3520
|
whereClause = `WHERE ${sql}`;
|
|
3280
|
-
context.paramCounter
|
|
3521
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
3281
3522
|
context.allParams.push(...params);
|
|
3523
|
+
if (placeholders) {
|
|
3524
|
+
context.placeholders = placeholders;
|
|
3525
|
+
}
|
|
3282
3526
|
}
|
|
3283
3527
|
// Build FROM clause with JOINs
|
|
3284
3528
|
let fromClause = `FROM "${this.schema.name}"`;
|
|
@@ -3286,9 +3530,12 @@ class SelectQueryBuilder {
|
|
|
3286
3530
|
for (const manualJoin of this.manualJoins) {
|
|
3287
3531
|
const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
|
|
3288
3532
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
3289
|
-
const { sql: condSql, params: condParams } = condBuilder.build(manualJoin.condition, context.paramCounter);
|
|
3290
|
-
context.paramCounter
|
|
3533
|
+
const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newJoinParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
|
|
3534
|
+
context.paramCounter = newJoinParamCounter; // Use returned counter (handles both params and placeholders)
|
|
3291
3535
|
context.allParams.push(...condParams);
|
|
3536
|
+
if (joinPlaceholders) {
|
|
3537
|
+
context.placeholders = joinPlaceholders;
|
|
3538
|
+
}
|
|
3292
3539
|
// Check if this is a subquery join
|
|
3293
3540
|
if (manualJoin.isSubquery && manualJoin.subquery) {
|
|
3294
3541
|
const subqueryBuildContext = {
|
|
@@ -3334,12 +3581,13 @@ class SelectQueryBuilder {
|
|
|
3334
3581
|
asSubquery(mode = 'table') {
|
|
3335
3582
|
// Create a function that builds the subquery SQL when called
|
|
3336
3583
|
const sqlBuilder = (outerContext) => {
|
|
3337
|
-
// Create a fresh context for this subquery
|
|
3584
|
+
// Create a fresh context for this subquery, inheriting placeholders from outer context
|
|
3338
3585
|
const context = {
|
|
3339
3586
|
ctes: new Map(),
|
|
3340
3587
|
cteCounter: 0,
|
|
3341
3588
|
paramCounter: outerContext.paramCounter,
|
|
3342
3589
|
allParams: outerContext.params,
|
|
3590
|
+
placeholders: outerContext.placeholders, // Pass placeholders through for prepared statements
|
|
3343
3591
|
executor: this.executor,
|
|
3344
3592
|
};
|
|
3345
3593
|
// Analyze the selector to extract nested queries
|
|
@@ -3829,6 +4077,12 @@ class CollectionQueryBuilder {
|
|
|
3829
4077
|
getTargetTableSchema() {
|
|
3830
4078
|
return this.targetTableSchema;
|
|
3831
4079
|
}
|
|
4080
|
+
/**
|
|
4081
|
+
* Get target table name
|
|
4082
|
+
*/
|
|
4083
|
+
getTargetTable() {
|
|
4084
|
+
return this.targetTable;
|
|
4085
|
+
}
|
|
3832
4086
|
/**
|
|
3833
4087
|
* Get selected field configs (for mapper lookup during transformation)
|
|
3834
4088
|
*/
|
|
@@ -4107,6 +4361,7 @@ class CollectionQueryBuilder {
|
|
|
4107
4361
|
const sqlBuildContext = {
|
|
4108
4362
|
paramCounter: context.paramCounter,
|
|
4109
4363
|
params: context.allParams,
|
|
4364
|
+
placeholders: context.placeholders, // Pass placeholders for prepared statements
|
|
4110
4365
|
};
|
|
4111
4366
|
const fragmentSql = field.buildSql(sqlBuildContext);
|
|
4112
4367
|
context.paramCounter = sqlBuildContext.paramCounter;
|
|
@@ -4153,10 +4408,25 @@ class CollectionQueryBuilder {
|
|
|
4153
4408
|
cteName: nestedResult.tableName,
|
|
4154
4409
|
joinClause: nestedJoinClause,
|
|
4155
4410
|
},
|
|
4411
|
+
// Store nested collection info for recursive mapper transformation
|
|
4412
|
+
nestedCollectionInfo: {
|
|
4413
|
+
targetTable: field.getTargetTable(),
|
|
4414
|
+
selectedFieldConfigs: field.getSelectedFieldConfigs(),
|
|
4415
|
+
isSingleResult: field.isSingleResult(),
|
|
4416
|
+
},
|
|
4156
4417
|
};
|
|
4157
4418
|
}
|
|
4158
4419
|
// The nested collection becomes a correlated subquery in SELECT
|
|
4159
|
-
return {
|
|
4420
|
+
return {
|
|
4421
|
+
alias,
|
|
4422
|
+
expression: nestedResult.selectExpression || nestedResult.sql,
|
|
4423
|
+
// Store nested collection info for recursive mapper transformation
|
|
4424
|
+
nestedCollectionInfo: {
|
|
4425
|
+
targetTable: field.getTargetTable(),
|
|
4426
|
+
selectedFieldConfigs: field.getSelectedFieldConfigs(),
|
|
4427
|
+
isSingleResult: field.isSingleResult(),
|
|
4428
|
+
},
|
|
4429
|
+
};
|
|
4160
4430
|
}
|
|
4161
4431
|
else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
4162
4432
|
// FieldRef object - use database column name with optional table alias
|
|
@@ -4241,12 +4511,15 @@ class CollectionQueryBuilder {
|
|
|
4241
4511
|
let whereParams;
|
|
4242
4512
|
if (this.whereCond) {
|
|
4243
4513
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
4244
|
-
const { sql, params } = condBuilder.build(this.whereCond, context.paramCounter);
|
|
4514
|
+
const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
|
|
4245
4515
|
whereClause = sql;
|
|
4246
4516
|
whereParams = params;
|
|
4247
|
-
context.paramCounter
|
|
4517
|
+
context.paramCounter = newParamCounter; // Use returned counter (handles both params and placeholders)
|
|
4248
4518
|
localParams.push(...params);
|
|
4249
4519
|
context.allParams.push(...params);
|
|
4520
|
+
if (placeholders) {
|
|
4521
|
+
context.placeholders = placeholders;
|
|
4522
|
+
}
|
|
4250
4523
|
}
|
|
4251
4524
|
// Step 3: Build ORDER BY clauses SQL (without ORDER BY keyword)
|
|
4252
4525
|
// We need two versions:
|
|
@@ -4337,9 +4610,11 @@ class CollectionQueryBuilder {
|
|
|
4337
4610
|
whereParams, // Pass WHERE clause parameters
|
|
4338
4611
|
orderByClause,
|
|
4339
4612
|
orderByClauseAlias, // For json_agg ORDER BY which uses aliases
|
|
4613
|
+
orderByFields: this.orderByFields.length > 0 ? this.orderByFields : undefined, // For including ORDER BY columns in inner SELECT
|
|
4340
4614
|
limitValue: this.limitValue,
|
|
4341
4615
|
offsetValue: this.offsetValue,
|
|
4342
4616
|
isDistinct: this.isDistinct,
|
|
4617
|
+
isSingleResult: this.isSingleResult(), // For firstOrDefault() - returns single object instead of array
|
|
4343
4618
|
aggregationType,
|
|
4344
4619
|
aggregateField,
|
|
4345
4620
|
arrayField,
|