Skip to content

Commit

Permalink
feat(query-builder): add qb.getCount() method
Browse files Browse the repository at this point in the history
```ts
const qb = orm.em.createQueryBuilder(Test);
qb.select('*').limit(10, 20).where({ $and: [{ id: { $nin: [3, 4] } }, { id: { $gt: 2 } }] });

const count = await qb.getCount();
```

Closes #2066
  • Loading branch information
B4nan committed Aug 15, 2021
1 parent fb67ea6 commit f773736
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 4 deletions.
29 changes: 29 additions & 0 deletions docs/docs/query-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,35 @@ console.log(qb.getQuery());
// select `e0`.* from `test` as `e0` where (`e0`.`id` not in (?, ?) and `e0`.`id` > ?)
```

## Count queries

To create a count query, we can ue `qb.count()`, which will intialize a select clause
with `count()` function. By default, it will use the primary key.

```typescript
const qb = orm.em.createQueryBuilder(Test);
qb.count().where({ $and: [{ id: { $nin: [3, 4] } }, { id: { $gt: 2 } }] });

console.log(qb.getQuery());
// select count(`e0`.`id`) from `test` as `e0` where (`e0`.`id` not in (?, ?) and `e0`.`id` > ?)

// to get the count, we can use `qb.execute()`
const res = await qb.execute('get');
const count = res ? +res.count : 0;
```

To simplify this process, we can use `qb.getCount()` method. Following code is equivalent:

```typescript
const qb = orm.em.createQueryBuilder(Test);
qb.select('*').limit(10, 20).where({ $and: [{ id: { $nin: [3, 4] } }, { id: { $gt: 2 } }] });

const count = await qb.getCount();
```

This will also remove any existing limit and offset from the query (the QB will be
cloned under the hood, so calling `getCount()` does not mutate the original QB state).

## Using sub-queries

You can filter using sub-queries in where conditions:
Expand Down
4 changes: 1 addition & 3 deletions packages/knex/src/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,13 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
async count<T extends AnyEntity<T>>(entityName: string, where: any, options: CountOptions<T> = {}, ctx?: Transaction<Knex.Transaction>): Promise<number> {
const pks = this.metadata.find(entityName)!.primaryKeys;
const qb = this.createQueryBuilder(entityName, ctx, !!ctx, false)
.count(pks, true)
.groupBy(options.groupBy!)
.having(options.having!)
.populate(options.populate as PopulateOptions<T>[] ?? [])
.withSchema(options.schema)
.where(where);
const res = await this.rethrow(qb.execute('get', false));

return res ? +res.count : 0;
return this.rethrow(qb.getCount(pks, true));
}

async nativeInsert<T extends AnyEntity<T>>(entityName: string, data: EntityDictionary<T>, ctx?: Transaction<Knex.Transaction>, convertCustomTypes = true): Promise<QueryResult> {
Expand Down
11 changes: 11 additions & 0 deletions packages/knex/src/query/QueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,17 @@ export class QueryBuilder<T extends AnyEntity<T> = AnyEntity> {
return res[0] || null;
}

/**
* Executes count query (without offset and limit), returning total count of results
*/
async getCount(field?: string | string[], distinct = false): Promise<number> {
const qb = this.clone();
qb.count(field, distinct).limit(undefined).offset(undefined);
const res = await qb.execute<{ count: number }>('get', false);

return res ? +res.count : 0;
}

/**
* Returns knex instance with sub-query aliased with given alias.
* You can provide `EntityName.propName` as alias, then the field name will be used based on the metadata
Expand Down
30 changes: 29 additions & 1 deletion tests/EntityManager.sqlite2.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ArrayCollection, Collection, EntityManager, LockMode, Logger, MikroORM, QueryOrder, ValidationError, wrap } from '@mikro-orm/core';
import { SqliteDriver } from '@mikro-orm/sqlite';
import { initORMSqlite2, wipeDatabaseSqlite2 } from './bootstrap';
import { initORMSqlite2, mockLogger, wipeDatabaseSqlite2 } from './bootstrap';
import { Author4, Book4, BookTag4, FooBar4, IAuthor4, IPublisher4, ITest4, Publisher4, PublisherType, Test4 } from './entities-schema';

describe('EntityManagerSqlite2', () => {
Expand Down Expand Up @@ -1041,6 +1041,34 @@ describe('EntityManagerSqlite2', () => {
expect(res[0].count).toBe(1);
});

test('qb.getCount()`', async () => {
for (let i = 1; i <= 50; i++) {
const author = orm.em.create(Author4, {
name: `a${i}`,
email: `e${i}`,
termsAccepted: !(i % 2),
});
orm.em.persist(author);
}

await orm.em.flush();
orm.em.clear();

const mock = mockLogger(orm);
const count1 = await orm.em.createQueryBuilder(Author4).limit(10, 20).getCount();
expect(count1).toBe(50);
const count2 = await orm.em.createQueryBuilder(Author4).getCount('termsAccepted');
expect(count2).toBe(50);
const count3 = await orm.em.createQueryBuilder(Author4).getCount('termsAccepted', true);
expect(count3).toBe(2);
const count4 = await orm.em.createQueryBuilder(Author4).where({ email: '123' }).getCount();
expect(count4).toBe(0);
expect(mock.mock.calls[0][0]).toMatch('select count(`e0`.`id`) as `count` from `author4` as `e0`');
expect(mock.mock.calls[1][0]).toMatch('select count(`e0`.`terms_accepted`) as `count` from `author4` as `e0`');
expect(mock.mock.calls[2][0]).toMatch('select count(distinct `e0`.`terms_accepted`) as `count` from `author4` as `e0`');
expect(mock.mock.calls[3][0]).toMatch('select count(`e0`.`id`) as `count` from `author4` as `e0` where `e0`.`email` = \'123\'');
});

test('using collection methods with null/undefined (GH issue #1408)', async () => {
const e = orm.em.create(Author4, { name: 'name', email: 'email' });
expect(() => e.books.remove(null as any)).not.toThrow();
Expand Down

0 comments on commit f773736

Please sign in to comment.