@mikro-orm/sql 7.0.7-dev.6 → 7.0.7-dev.7

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.
@@ -36,6 +36,8 @@ export declare abstract class AbstractSqlPlatform extends Platform {
36
36
  quoteJsonKey(key: string): string;
37
37
  getJsonIndexDefinition(index: IndexDef): string[];
38
38
  supportsUnionWhere(): boolean;
39
+ /** Whether the platform supports `count(distinct col1, col2)` with multiple columns. If false, a subquery wrapper is used instead. */
40
+ supportsMultiColumnCountDistinct(): boolean;
39
41
  supportsSchemas(): boolean;
40
42
  /** @inheritDoc */
41
43
  generateCustomOrder(escapedColumn: string, values: unknown[]): string;
@@ -91,6 +91,10 @@ export class AbstractSqlPlatform extends Platform {
91
91
  supportsUnionWhere() {
92
92
  return true;
93
93
  }
94
+ /** Whether the platform supports `count(distinct col1, col2)` with multiple columns. If false, a subquery wrapper is used instead. */
95
+ supportsMultiColumnCountDistinct() {
96
+ return false;
97
+ }
94
98
  supportsSchemas() {
95
99
  return false;
96
100
  }
@@ -137,13 +137,18 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
137
137
  this.parts[this.parts.length - 1] += ';';
138
138
  }
139
139
  compileSelect() {
140
+ const wrapCountSubquery = this.needsCountSubquery();
141
+ if (wrapCountSubquery) {
142
+ this.parts.push(`select count(*) as ${this.quote('count')} from (`);
143
+ }
140
144
  this.parts.push('select');
141
- if (this.options.limit != null && this.options.offset == null) {
145
+ // skip top(?) inside the count subquery it would limit the distinct rows before counting
146
+ if (this.options.limit != null && this.options.offset == null && !wrapCountSubquery) {
142
147
  this.parts.push(`top (?)`);
143
148
  this.params.push(this.options.limit);
144
149
  }
145
150
  this.addHintComment();
146
- this.parts.push(`${this.getFields()} from ${this.getTableName()}`);
151
+ this.parts.push(`${this.getFields(wrapCountSubquery)} from ${this.getTableName()}`);
147
152
  this.addLockClause();
148
153
  if (this.options.joins) {
149
154
  for (const join of this.options.joins) {
@@ -163,21 +168,27 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
163
168
  this.parts.push(`having ${this.options.having.sql}`);
164
169
  this.params.push(...this.options.having.params);
165
170
  }
166
- if (this.options.orderBy) {
167
- this.parts.push(`order by ${this.options.orderBy}`);
168
- }
169
- if (this.options.offset != null) {
170
- /* v8 ignore next */
171
- if (!this.options.orderBy) {
172
- throw new Error('Order by clause is required for pagination');
171
+ if (!wrapCountSubquery) {
172
+ if (this.options.orderBy) {
173
+ this.parts.push(`order by ${this.options.orderBy}`);
173
174
  }
174
- this.parts.push(`offset ? rows`);
175
- this.params.push(this.options.offset);
176
- if (this.options.limit != null) {
177
- this.parts.push(`fetch next ? rows only`);
178
- this.params.push(this.options.limit);
175
+ if (this.options.offset != null) {
176
+ /* v8 ignore next */
177
+ if (!this.options.orderBy) {
178
+ throw new Error('Order by clause is required for pagination');
179
+ }
180
+ this.parts.push(`offset ? rows`);
181
+ this.params.push(this.options.offset);
182
+ if (this.options.limit != null) {
183
+ this.parts.push(`fetch next ? rows only`);
184
+ this.params.push(this.options.limit);
185
+ }
179
186
  }
180
187
  }
188
+ if (wrapCountSubquery) {
189
+ const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
190
+ this.parts.push(`)${asKeyword}${this.quote('dcnt')}`);
191
+ }
181
192
  }
182
193
  addLockClause() {
183
194
  if (!this.options.lockMode ||
@@ -14,6 +14,7 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
14
14
  readonly "desc nulls first": "is not null";
15
15
  readonly "desc nulls last": "is null";
16
16
  };
17
+ supportsMultiColumnCountDistinct(): boolean;
17
18
  /** @internal */
18
19
  createNativeQueryBuilder(): MySqlNativeQueryBuilder;
19
20
  getDefaultCharset(): string;
@@ -18,6 +18,9 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
18
18
  [QueryOrder.desc_nulls_first]: 'is not null',
19
19
  [QueryOrder.desc_nulls_last]: 'is null',
20
20
  };
21
+ supportsMultiColumnCountDistinct() {
22
+ return true;
23
+ }
21
24
  /** @internal */
22
25
  createNativeQueryBuilder() {
23
26
  return new MySqlNativeQueryBuilder(this);
@@ -213,9 +213,13 @@ export class OracleNativeQueryBuilder extends NativeQueryBuilder {
213
213
  }
214
214
  }
215
215
  compileSelect() {
216
+ const wrapCountSubquery = this.needsCountSubquery();
217
+ if (wrapCountSubquery) {
218
+ this.parts.push(`select count(*) as ${this.quote('count')} from (`);
219
+ }
216
220
  this.parts.push('select');
217
221
  this.addHintComment();
218
- this.parts.push(`${this.getFields()} from ${this.getTableName()}`);
222
+ this.parts.push(`${this.getFields(wrapCountSubquery)} from ${this.getTableName()}`);
219
223
  if (this.options.joins) {
220
224
  for (const join of this.options.joins) {
221
225
  this.parts.push(join.sql);
@@ -234,16 +238,22 @@ export class OracleNativeQueryBuilder extends NativeQueryBuilder {
234
238
  this.parts.push(`having ${this.options.having.sql}`);
235
239
  this.params.push(...this.options.having.params);
236
240
  }
237
- if (this.options.orderBy) {
238
- this.parts.push(`order by ${this.options.orderBy}`);
239
- }
240
- if (this.options.offset != null) {
241
- this.parts.push(`offset ? rows`);
242
- this.params.push(this.options.offset);
241
+ if (!wrapCountSubquery) {
242
+ if (this.options.orderBy) {
243
+ this.parts.push(`order by ${this.options.orderBy}`);
244
+ }
245
+ if (this.options.offset != null) {
246
+ this.parts.push(`offset ? rows`);
247
+ this.params.push(this.options.offset);
248
+ }
249
+ if (this.options.limit != null) {
250
+ this.parts.push(`fetch next ? rows only`);
251
+ this.params.push(this.options.limit);
252
+ }
243
253
  }
244
- if (this.options.limit != null) {
245
- this.parts.push(`fetch next ? rows only`);
246
- this.params.push(this.options.limit);
254
+ if (wrapCountSubquery) {
255
+ const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
256
+ this.parts.push(`)${asKeyword}${this.quote('dcnt')}`);
247
257
  }
248
258
  }
249
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.0.7-dev.6",
3
+ "version": "7.0.7-dev.7",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -53,7 +53,7 @@
53
53
  "@mikro-orm/core": "^7.0.6"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.0.7-dev.6"
56
+ "@mikro-orm/core": "7.0.7-dev.7"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -121,7 +121,9 @@ export declare class NativeQueryBuilder implements Subquery {
121
121
  as(alias: string): this;
122
122
  toRaw(): RawQueryFragment;
123
123
  protected compileSelect(): void;
124
- protected getFields(): string;
124
+ /** Whether this COUNT query needs a subquery wrapper for multi-column distinct. */
125
+ protected needsCountSubquery(): boolean;
126
+ protected getFields(countSubquery?: boolean): string;
125
127
  protected compileInsert(): void;
126
128
  protected addOutputClause(type: 'inserted' | 'deleted'): void;
127
129
  protected processInsertData(): string[];
@@ -281,9 +281,13 @@ export class NativeQueryBuilder {
281
281
  return raw(sql, params);
282
282
  }
283
283
  compileSelect() {
284
+ const wrapCountSubquery = this.needsCountSubquery();
285
+ if (wrapCountSubquery) {
286
+ this.parts.push(`select count(*) as ${this.quote('count')} from (`);
287
+ }
284
288
  this.parts.push('select');
285
289
  this.addHintComment();
286
- this.parts.push(`${this.getFields()} from ${this.getTableName()}`);
290
+ this.parts.push(`${this.getFields(wrapCountSubquery)} from ${this.getTableName()}`);
287
291
  if (this.options.joins) {
288
292
  for (const join of this.options.joins) {
289
293
  this.parts.push(join.sql);
@@ -302,23 +306,40 @@ export class NativeQueryBuilder {
302
306
  this.parts.push(`having ${this.options.having.sql}`);
303
307
  this.params.push(...this.options.having.params);
304
308
  }
305
- if (this.options.orderBy) {
306
- this.parts.push(`order by ${this.options.orderBy}`);
307
- }
308
- if (this.options.limit != null) {
309
- this.parts.push(`limit ?`);
310
- this.params.push(this.options.limit);
309
+ if (!wrapCountSubquery) {
310
+ if (this.options.orderBy) {
311
+ this.parts.push(`order by ${this.options.orderBy}`);
312
+ }
313
+ if (this.options.limit != null) {
314
+ this.parts.push(`limit ?`);
315
+ this.params.push(this.options.limit);
316
+ }
317
+ if (this.options.offset != null) {
318
+ this.parts.push(`offset ?`);
319
+ this.params.push(this.options.offset);
320
+ }
311
321
  }
312
- if (this.options.offset != null) {
313
- this.parts.push(`offset ?`);
314
- this.params.push(this.options.offset);
322
+ if (wrapCountSubquery) {
323
+ const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
324
+ this.parts.push(`)${asKeyword}${this.quote('dcnt')}`);
315
325
  }
316
326
  }
317
- getFields() {
327
+ /** Whether this COUNT query needs a subquery wrapper for multi-column distinct. */
328
+ needsCountSubquery() {
329
+ return (this.type === QueryType.COUNT &&
330
+ !!this.options.distinct &&
331
+ this.options.select.length > 1 &&
332
+ !this.platform.supportsMultiColumnCountDistinct());
333
+ }
334
+ getFields(countSubquery) {
318
335
  if (!this.options.select || this.options.select.length === 0) {
319
336
  throw new Error('No fields selected');
320
337
  }
321
338
  let fields = this.options.select.map(field => this.quote(field)).join(', ');
339
+ // count subquery emits just `distinct col1, col2` — the outer wrapper adds count(*)
340
+ if (countSubquery) {
341
+ return `distinct ${fields}`;
342
+ }
322
343
  if (this.options.distinct) {
323
344
  fields = `distinct ${fields}`;
324
345
  }