linkgress-orm 0.2.10 → 0.2.12

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.
@@ -13,6 +13,8 @@ const subquery_1 = require("./subquery");
13
13
  const grouped_query_1 = require("./grouped-query");
14
14
  const cte_builder_1 = require("./cte-builder");
15
15
  const collection_strategy_factory_1 = require("./collection-strategy.factory");
16
+ const union_builder_1 = require("./union-builder");
17
+ const future_query_1 = require("./future-query");
16
18
  /**
17
19
  * Field type categories for optimized result transformation
18
20
  * const enum is inlined at compile time for zero runtime overhead
@@ -1068,6 +1070,177 @@ class SelectQueryBuilder {
1068
1070
  : await this.client.query(sql, params);
1069
1071
  return result.rows[0]?.exists === true;
1070
1072
  }
1073
+ /**
1074
+ * Combine this query with another using UNION (removes duplicates)
1075
+ *
1076
+ * @param query Another SelectQueryBuilder with compatible selection type
1077
+ * @returns A UnionQueryBuilder for further chaining
1078
+ *
1079
+ * @example
1080
+ * ```typescript
1081
+ * const result = await db.users
1082
+ * .select(u => ({ id: u.id, name: u.name }))
1083
+ * .union(db.customers.select(c => ({ id: c.id, name: c.name })))
1084
+ * .orderBy(r => r.name)
1085
+ * .toList();
1086
+ * ```
1087
+ */
1088
+ union(query) {
1089
+ const unionBuilder = new union_builder_1.UnionQueryBuilder(this, this.client, this.executor);
1090
+ return unionBuilder.union(query);
1091
+ }
1092
+ /**
1093
+ * Combine this query with another using UNION ALL (keeps all rows including duplicates)
1094
+ *
1095
+ * @param query Another SelectQueryBuilder with compatible selection type
1096
+ * @returns A UnionQueryBuilder for further chaining
1097
+ *
1098
+ * @example
1099
+ * ```typescript
1100
+ * // UNION ALL is faster than UNION as it doesn't need to remove duplicates
1101
+ * const allLogs = await db.errorLogs
1102
+ * .select(l => ({ timestamp: l.createdAt, message: l.message }))
1103
+ * .unionAll(db.infoLogs.select(l => ({ timestamp: l.createdAt, message: l.message })))
1104
+ * .orderBy(r => r.timestamp)
1105
+ * .toList();
1106
+ * ```
1107
+ */
1108
+ unionAll(query) {
1109
+ const unionBuilder = new union_builder_1.UnionQueryBuilder(this, this.client, this.executor);
1110
+ return unionBuilder.unionAll(query);
1111
+ }
1112
+ /**
1113
+ * Build SQL for use in UNION queries (without ORDER BY, LIMIT, OFFSET)
1114
+ * @internal Used by UnionQueryBuilder
1115
+ */
1116
+ buildUnionSql(context) {
1117
+ const queryContext = {
1118
+ ctes: new Map(),
1119
+ cteCounter: 0,
1120
+ paramCounter: context.paramCounter,
1121
+ allParams: context.params,
1122
+ collectionStrategy: this.collectionStrategy,
1123
+ executor: this.executor,
1124
+ };
1125
+ const mockRow = this._createMockRow();
1126
+ const selectionResult = this.selector(mockRow);
1127
+ // Build query without ORDER BY, LIMIT, OFFSET for union component
1128
+ const { sql } = this.buildQueryCore(selectionResult, queryContext, false);
1129
+ // Update context's param counter
1130
+ context.paramCounter = queryContext.paramCounter;
1131
+ return sql;
1132
+ }
1133
+ /**
1134
+ * Create a future query that will be executed later.
1135
+ * Use with FutureQueryRunner.runAsync() for batch execution.
1136
+ *
1137
+ * @returns A FutureQuery that can be executed individually or in a batch
1138
+ *
1139
+ * @example
1140
+ * ```typescript
1141
+ * const q1 = db.users.select(u => ({ id: u.id, name: u.username })).future();
1142
+ * const q2 = db.posts.select(p => ({ title: p.title })).future();
1143
+ *
1144
+ * // Execute in a single roundtrip
1145
+ * const [users, posts] = await FutureQueryRunner.runAsync([q1, q2]);
1146
+ * ```
1147
+ */
1148
+ future() {
1149
+ const context = {
1150
+ ctes: new Map(),
1151
+ cteCounter: 0,
1152
+ paramCounter: 1,
1153
+ allParams: [],
1154
+ collectionStrategy: this.collectionStrategy,
1155
+ executor: this.executor,
1156
+ };
1157
+ const mockRow = this._createMockRow();
1158
+ const selectionResult = this.selector(mockRow);
1159
+ const { sql, params, nestedPaths } = this.buildQuery(selectionResult, context);
1160
+ // Create transform function that captures current state
1161
+ const transformFn = (rows) => {
1162
+ if (rows.length === 0)
1163
+ return [];
1164
+ // Reconstruct nested objects if needed
1165
+ let processedRows = rows;
1166
+ if (nestedPaths.size > 0) {
1167
+ processedRows = rows.map(row => this.reconstructNestedObjects(row, nestedPaths));
1168
+ }
1169
+ return this.transformResults(processedRows, selectionResult);
1170
+ };
1171
+ return new future_query_1.FutureQuery(sql, params, transformFn, this.client, this.executor);
1172
+ }
1173
+ /**
1174
+ * Create a future query that returns a single result or null.
1175
+ * Use with FutureQueryRunner.runAsync() for batch execution.
1176
+ *
1177
+ * @returns A FutureSingleQuery that resolves to a single result or null
1178
+ *
1179
+ * @example
1180
+ * ```typescript
1181
+ * const q1 = db.users.where(u => eq(u.id, 1)).select(u => u).futureFirstOrDefault();
1182
+ * const q2 = db.posts.where(p => eq(p.id, 5)).select(p => p).futureFirstOrDefault();
1183
+ *
1184
+ * const [user, post] = await FutureQueryRunner.runAsync([q1, q2]);
1185
+ * // user: User | null
1186
+ * // post: Post | null
1187
+ * ```
1188
+ */
1189
+ futureFirstOrDefault() {
1190
+ // Apply LIMIT 1 for efficiency
1191
+ const originalLimit = this.limitValue;
1192
+ this.limitValue = 1;
1193
+ const context = {
1194
+ ctes: new Map(),
1195
+ cteCounter: 0,
1196
+ paramCounter: 1,
1197
+ allParams: [],
1198
+ collectionStrategy: this.collectionStrategy,
1199
+ executor: this.executor,
1200
+ };
1201
+ const mockRow = this._createMockRow();
1202
+ const selectionResult = this.selector(mockRow);
1203
+ const { sql, params, nestedPaths } = this.buildQuery(selectionResult, context);
1204
+ // Restore original limit
1205
+ this.limitValue = originalLimit;
1206
+ // Create transform function
1207
+ const transformFn = (rows) => {
1208
+ if (rows.length === 0)
1209
+ return [];
1210
+ let processedRows = rows;
1211
+ if (nestedPaths.size > 0) {
1212
+ processedRows = rows.map(row => this.reconstructNestedObjects(row, nestedPaths));
1213
+ }
1214
+ return this.transformResults(processedRows, selectionResult);
1215
+ };
1216
+ return new future_query_1.FutureSingleQuery(sql, params, transformFn, this.client, this.executor);
1217
+ }
1218
+ /**
1219
+ * Create a future query that returns a count.
1220
+ * Use with FutureQueryRunner.runAsync() for batch execution.
1221
+ *
1222
+ * @returns A FutureCountQuery that resolves to a number
1223
+ *
1224
+ * @example
1225
+ * ```typescript
1226
+ * const q1 = db.users.futureCount();
1227
+ * const q2 = db.posts.futureCount();
1228
+ * const q3 = db.comments.where(c => eq(c.isPublished, true)).futureCount();
1229
+ *
1230
+ * const [userCount, postCount, commentCount] = await FutureQueryRunner.runAsync([q1, q2, q3]);
1231
+ * ```
1232
+ */
1233
+ futureCount() {
1234
+ const context = {
1235
+ ctes: new Map(),
1236
+ cteCounter: 0,
1237
+ paramCounter: 1,
1238
+ allParams: [],
1239
+ executor: this.executor,
1240
+ };
1241
+ const { sql, params } = this.buildAggregateQuery(context, 'count');
1242
+ return new future_query_1.FutureCountQuery(sql, params, this.client, this.executor);
1243
+ }
1071
1244
  /**
1072
1245
  * Execute query and return results as array
1073
1246
  * Collection results are automatically resolved to arrays
@@ -3412,6 +3585,237 @@ ${joinClauses.join('\n')}`;
3412
3585
  nestedPaths,
3413
3586
  };
3414
3587
  }
3588
+ /**
3589
+ * Build the core SQL query, optionally excluding ORDER BY, LIMIT, and OFFSET
3590
+ * Used by buildUnionSql to build component queries for UNION
3591
+ * @internal
3592
+ */
3593
+ buildQueryCore(selection, context, includeOrderLimitOffset = true) {
3594
+ // Handle user-defined CTEs first - their params need to come before main query params
3595
+ for (const cte of this.ctes) {
3596
+ context.allParams.push(...cte.params);
3597
+ context.paramCounter += cte.params.length;
3598
+ }
3599
+ const selectParts = [];
3600
+ const collectionFields = [];
3601
+ const joins = [];
3602
+ const nestedPaths = new Set();
3603
+ // Scan selection for navigation property references and add JOINs
3604
+ this.detectAndAddJoinsFromSelection(selection, joins);
3605
+ // Scan WHERE condition for navigation property references and add JOINs
3606
+ this.detectAndAddJoinsFromCondition(this.whereCond, joins);
3607
+ // Handle case where selection is a single value (not an object with properties)
3608
+ if (selection instanceof conditions_1.SqlFragment) {
3609
+ const sqlBuildContext = {
3610
+ paramCounter: context.paramCounter,
3611
+ params: context.allParams,
3612
+ };
3613
+ const fragmentSql = selection.buildSql(sqlBuildContext);
3614
+ context.paramCounter = sqlBuildContext.paramCounter;
3615
+ selectParts.push(fragmentSql);
3616
+ }
3617
+ else if (typeof selection === 'object' && selection !== null && '__dbColumnName' in selection) {
3618
+ const tableAlias = ('__tableAlias' in selection && selection.__tableAlias) ? selection.__tableAlias : this.schema.name;
3619
+ selectParts.push(`"${tableAlias}"."${selection.__dbColumnName}"`);
3620
+ }
3621
+ else if (selection instanceof CollectionQueryBuilder) {
3622
+ throw new Error('Cannot use CollectionQueryBuilder directly as selection');
3623
+ }
3624
+ else {
3625
+ // Process selection object properties
3626
+ for (const [key, value] of Object.entries(selection)) {
3627
+ if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
3628
+ // Collection fields are not supported in UNION queries for simplicity
3629
+ // Skip them - they would need complex handling
3630
+ continue;
3631
+ }
3632
+ else if (value instanceof subquery_1.Subquery || (value && typeof value === 'object' && 'buildSql' in value && typeof value.buildSql === 'function' && '__mode' in value)) {
3633
+ const sqlBuildContext = {
3634
+ paramCounter: context.paramCounter,
3635
+ params: context.allParams,
3636
+ };
3637
+ const subquerySql = value.buildSql(sqlBuildContext);
3638
+ context.paramCounter = sqlBuildContext.paramCounter;
3639
+ selectParts.push(`(${subquerySql}) as "${key}"`);
3640
+ }
3641
+ else if (value instanceof conditions_1.SqlFragment) {
3642
+ const sqlBuildContext = {
3643
+ paramCounter: context.paramCounter,
3644
+ params: context.allParams,
3645
+ };
3646
+ const fragmentSql = value.buildSql(sqlBuildContext);
3647
+ context.paramCounter = sqlBuildContext.paramCounter;
3648
+ selectParts.push(`${fragmentSql} as "${key}"`);
3649
+ }
3650
+ else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
3651
+ if ('__tableAlias' in value && value.__tableAlias && typeof value.__tableAlias === 'string') {
3652
+ const tableAlias = value.__tableAlias;
3653
+ const columnName = value.__dbColumnName;
3654
+ const relConfig = this.schema.relations[tableAlias];
3655
+ if (relConfig && !joins.find(j => j.alias === tableAlias)) {
3656
+ let targetSchema;
3657
+ if (relConfig.targetTableBuilder) {
3658
+ const targetTableSchema = relConfig.targetTableBuilder.build();
3659
+ targetSchema = targetTableSchema.schema;
3660
+ }
3661
+ joins.push({
3662
+ alias: tableAlias,
3663
+ targetTable: relConfig.targetTable,
3664
+ targetSchema,
3665
+ foreignKeys: relConfig.foreignKeys || [relConfig.foreignKey || ''],
3666
+ matches: relConfig.matches || [],
3667
+ isMandatory: relConfig.isMandatory ?? false,
3668
+ });
3669
+ }
3670
+ const cteJoin = this.manualJoins.find(j => j.cte && j.cte.name === tableAlias);
3671
+ if (cteJoin && cteJoin.cte && cteJoin.cte.isAggregationColumn(columnName)) {
3672
+ selectParts.push(`COALESCE("${tableAlias}"."${columnName}", '[]'::json) as "${key}"`);
3673
+ }
3674
+ else {
3675
+ selectParts.push(`"${tableAlias}"."${columnName}" as "${key}"`);
3676
+ }
3677
+ }
3678
+ else {
3679
+ selectParts.push(`"${this.schema.name}"."${value.__dbColumnName}" as "${key}"`);
3680
+ }
3681
+ }
3682
+ else if (typeof value === 'string') {
3683
+ selectParts.push(`"${this.schema.name}"."${value}" as "${key}"`);
3684
+ }
3685
+ else if (typeof value === 'object' && value !== null) {
3686
+ if (!('__dbColumnName' in value)) {
3687
+ if (Array.isArray(value)) {
3688
+ continue;
3689
+ }
3690
+ if (value instanceof CollectionQueryBuilder) {
3691
+ continue;
3692
+ }
3693
+ else if (value instanceof ReferenceQueryBuilder) {
3694
+ continue; // Skip ReferenceQueryBuilder in union queries
3695
+ }
3696
+ }
3697
+ selectParts.push(`$${context.paramCounter++} as "${key}"`);
3698
+ context.allParams.push(value);
3699
+ }
3700
+ else if (value === undefined) {
3701
+ continue;
3702
+ }
3703
+ else {
3704
+ selectParts.push(`$${context.paramCounter++} as "${key}"`);
3705
+ context.allParams.push(value);
3706
+ }
3707
+ }
3708
+ }
3709
+ // Build WHERE clause
3710
+ let whereClause = '';
3711
+ if (this.whereCond) {
3712
+ const condBuilder = new conditions_1.ConditionBuilder();
3713
+ const { sql, params, placeholders, paramCounter: newParamCounter } = condBuilder.build(this.whereCond, context.paramCounter, context.placeholders);
3714
+ whereClause = `WHERE ${sql}`;
3715
+ context.paramCounter = newParamCounter;
3716
+ context.allParams.push(...params);
3717
+ if (placeholders) {
3718
+ context.placeholders = placeholders;
3719
+ }
3720
+ }
3721
+ // Build ORDER BY clause (only if includeOrderLimitOffset is true)
3722
+ let orderByClause = '';
3723
+ if (includeOrderLimitOffset && this.orderByFields.length > 0) {
3724
+ const colNameMap = getColumnNameMapForSchema(this.schema);
3725
+ const orderParts = this.orderByFields.map(({ field, direction }) => {
3726
+ if (selection && typeof selection === 'object' && !Array.isArray(selection) && field in selection) {
3727
+ return `"${field}" ${direction}`;
3728
+ }
3729
+ else {
3730
+ const dbColumnName = colNameMap.get(field) ?? field;
3731
+ return `"${this.schema.name}"."${dbColumnName}" ${direction}`;
3732
+ }
3733
+ });
3734
+ orderByClause = `ORDER BY ${orderParts.join(', ')}`;
3735
+ }
3736
+ // Build LIMIT/OFFSET (only if includeOrderLimitOffset is true)
3737
+ let limitClause = '';
3738
+ if (includeOrderLimitOffset) {
3739
+ if (this.limitValue !== undefined) {
3740
+ limitClause = `LIMIT ${this.limitValue}`;
3741
+ }
3742
+ if (this.offsetValue !== undefined) {
3743
+ limitClause += ` OFFSET ${this.offsetValue}`;
3744
+ }
3745
+ }
3746
+ // Build final query with CTEs
3747
+ let finalQuery = '';
3748
+ const allCtes = [];
3749
+ for (const cte of this.ctes) {
3750
+ allCtes.push(`"${cte.name}" AS (${cte.query})`);
3751
+ }
3752
+ if (context.ctes.size > 0) {
3753
+ for (const [cteName, { sql }] of context.ctes.entries()) {
3754
+ allCtes.push(`"${cteName}" AS (${sql})`);
3755
+ }
3756
+ }
3757
+ if (allCtes.length > 0) {
3758
+ finalQuery = `WITH ${allCtes.join(', ')}\n`;
3759
+ }
3760
+ // Build main query
3761
+ const qualifiedTableName = this.getQualifiedTableName(this.schema.name, this.schema.schema);
3762
+ let fromClause = `FROM ${qualifiedTableName}`;
3763
+ // Add manual JOINs
3764
+ for (const manualJoin of this.manualJoins) {
3765
+ const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
3766
+ const condBuilder = new conditions_1.ConditionBuilder();
3767
+ const { sql: condSql, params: condParams, placeholders: joinPlaceholders, paramCounter: newParamCounter } = condBuilder.build(manualJoin.condition, context.paramCounter, context.placeholders);
3768
+ context.paramCounter = newParamCounter;
3769
+ context.allParams.push(...condParams);
3770
+ if (joinPlaceholders) {
3771
+ context.placeholders = joinPlaceholders;
3772
+ }
3773
+ if (manualJoin.cte) {
3774
+ fromClause += `\n${joinTypeStr} "${manualJoin.cte.name}" ON ${condSql}`;
3775
+ }
3776
+ else if (manualJoin.isSubquery && manualJoin.subquery) {
3777
+ const subqueryBuildContext = {
3778
+ paramCounter: context.paramCounter,
3779
+ params: context.allParams,
3780
+ };
3781
+ const subquerySql = manualJoin.subquery.buildSql(subqueryBuildContext);
3782
+ context.paramCounter = subqueryBuildContext.paramCounter;
3783
+ fromClause += `\n${joinTypeStr} (${subquerySql}) AS "${manualJoin.alias}" ON ${condSql}`;
3784
+ }
3785
+ else {
3786
+ fromClause += `\n${joinTypeStr} "${manualJoin.table}" AS "${manualJoin.alias}" ON ${condSql}`;
3787
+ }
3788
+ }
3789
+ // Add JOINs for single navigation
3790
+ for (const join of joins) {
3791
+ const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
3792
+ const sourceTable = join.sourceAlias || this.schema.name;
3793
+ const onConditions = [];
3794
+ for (let i = 0; i < join.foreignKeys.length; i++) {
3795
+ const fk = join.foreignKeys[i];
3796
+ const match = join.matches[i];
3797
+ onConditions.push(`"${sourceTable}"."${fk}" = "${join.alias}"."${match}"`);
3798
+ }
3799
+ const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
3800
+ fromClause += `\n${joinType} ${joinTableName} AS "${join.alias}" ON ${onConditions.join(' AND ')}`;
3801
+ }
3802
+ // Add DISTINCT if needed
3803
+ const distinctClause = this.isDistinct ? 'DISTINCT ' : '';
3804
+ // Build final SQL
3805
+ const queryParts = [`SELECT ${distinctClause}${selectParts.join(', ')}`, fromClause];
3806
+ if (whereClause)
3807
+ queryParts.push(whereClause);
3808
+ if (orderByClause)
3809
+ queryParts.push(orderByClause);
3810
+ if (limitClause)
3811
+ queryParts.push(limitClause);
3812
+ finalQuery += queryParts.join('\n').trim();
3813
+ return {
3814
+ sql: finalQuery,
3815
+ params: context.allParams,
3816
+ nestedPaths,
3817
+ };
3818
+ }
3415
3819
  /**
3416
3820
  * Transform database results
3417
3821
  */