mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: support for SQL aggregate functions SUM, AVG, MIN, and MAX to the Repository API (#9737)
* feat: Add support for SQL aggregate functions SUM, AVG, MIN, and MAX to the Repository API * rename field name to make tests work in oracle * fix the comments * update the docs * escape column name * address PR comment * format the code
This commit is contained in:
parent
4555211bcb
commit
7d1f1d6958
@ -273,6 +273,30 @@ const count = await repository.count({
|
||||
const count = await repository.countBy({ firstName: "Timber" })
|
||||
```
|
||||
|
||||
- `sum` - Returns the sum of a numeric field for all entities that match `FindOptionsWhere`.
|
||||
|
||||
```typescript
|
||||
const sum = await repository.sum("age", { firstName: "Timber" })
|
||||
```
|
||||
|
||||
- `average` - Returns the average of a numeric field for all entities that match `FindOptionsWhere`.
|
||||
|
||||
```typescript
|
||||
const average = await repository.average("age", { firstName: "Timber" })
|
||||
```
|
||||
|
||||
- `minimum` - Returns the minimum of a numeric field for all entities that match `FindOptionsWhere`.
|
||||
|
||||
```typescript
|
||||
const minimum = await repository.minimum("age", { firstName: "Timber" })
|
||||
```
|
||||
|
||||
- `maximum` - Returns the maximum of a numeric field for all entities that match `FindOptionsWhere`.
|
||||
|
||||
```typescript
|
||||
const maximum = await repository.maximum("age", { firstName: "Timber" })
|
||||
```
|
||||
|
||||
- `find` - Finds entities that match given `FindOptions`.
|
||||
|
||||
```typescript
|
||||
|
||||
7
src/common/PickKeysByType.ts
Normal file
7
src/common/PickKeysByType.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Pick only the keys that match the Type `U`
|
||||
*/
|
||||
export type PickKeysByType<T, U> = string &
|
||||
keyof {
|
||||
[P in keyof T as T[P] extends U ? P : never]: T[P]
|
||||
}
|
||||
@ -37,6 +37,7 @@ import { getMetadataArgsStorage } from "../globals"
|
||||
import { UpsertOptions } from "../repository/UpsertOptions"
|
||||
import { InstanceChecker } from "../util/InstanceChecker"
|
||||
import { ObjectLiteral } from "../common/ObjectLiteral"
|
||||
import { PickKeysByType } from "../common/PickKeysByType"
|
||||
|
||||
/**
|
||||
* Entity manager supposed to work with any entity, automatically find its repository and call its methods,
|
||||
@ -1001,6 +1002,69 @@ export class EntityManager {
|
||||
.getCount()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SUM of a column
|
||||
*/
|
||||
sum<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
return this.callAggregateFun(entityClass, "SUM", columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the AVG of a column
|
||||
*/
|
||||
average<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
return this.callAggregateFun(entityClass, "AVG", columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MIN of a column
|
||||
*/
|
||||
minimum<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
return this.callAggregateFun(entityClass, "MIN", columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MAX of a column
|
||||
*/
|
||||
maximum<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
return this.callAggregateFun(entityClass, "MAX", columnName, where)
|
||||
}
|
||||
|
||||
private async callAggregateFun<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
fnName: "SUM" | "AVG" | "MIN" | "MAX",
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[] = {},
|
||||
): Promise<number | null> {
|
||||
const metadata = this.connection.getMetadata(entityClass)
|
||||
const result = await this.createQueryBuilder(entityClass, metadata.name)
|
||||
.setFindOptions({ where })
|
||||
.select(
|
||||
`${fnName}(${this.connection.driver.escape(
|
||||
String(columnName),
|
||||
)})`,
|
||||
fnName,
|
||||
)
|
||||
.getRawOne()
|
||||
return result[fnName] === null ? null : parseFloat(result[fnName])
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entities that match given find options.
|
||||
*/
|
||||
|
||||
@ -15,6 +15,7 @@ import { ObjectUtils } from "../util/ObjectUtils"
|
||||
import { QueryDeepPartialEntity } from "../query-builder/QueryPartialEntity"
|
||||
import { UpsertOptions } from "./UpsertOptions"
|
||||
import { EntityTarget } from "../common/EntityTarget"
|
||||
import { PickKeysByType } from "../common/PickKeysByType"
|
||||
|
||||
/**
|
||||
* Base abstract entity for all entities, used in ActiveRecord patterns.
|
||||
@ -408,6 +409,50 @@ export class BaseEntity {
|
||||
return this.getRepository<T>().countBy(where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SUM of a column
|
||||
*/
|
||||
static sum<T extends BaseEntity>(
|
||||
this: { new (): T } & typeof BaseEntity,
|
||||
columnName: PickKeysByType<T, number>,
|
||||
where: FindOptionsWhere<T>,
|
||||
): Promise<number | null> {
|
||||
return this.getRepository<T>().sum(columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the AVG of a column
|
||||
*/
|
||||
static average<T extends BaseEntity>(
|
||||
this: { new (): T } & typeof BaseEntity,
|
||||
columnName: PickKeysByType<T, number>,
|
||||
where: FindOptionsWhere<T>,
|
||||
): Promise<number | null> {
|
||||
return this.getRepository<T>().average(columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MIN of a column
|
||||
*/
|
||||
static minimum<T extends BaseEntity>(
|
||||
this: { new (): T } & typeof BaseEntity,
|
||||
columnName: PickKeysByType<T, number>,
|
||||
where: FindOptionsWhere<T>,
|
||||
): Promise<number | null> {
|
||||
return this.getRepository<T>().minimum(columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MAX of a column
|
||||
*/
|
||||
static maximum<T extends BaseEntity>(
|
||||
this: { new (): T } & typeof BaseEntity,
|
||||
columnName: PickKeysByType<T, number>,
|
||||
where: FindOptionsWhere<T>,
|
||||
): Promise<number | null> {
|
||||
return this.getRepository<T>().maximum(columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entities that match given options.
|
||||
*/
|
||||
|
||||
@ -15,6 +15,7 @@ import { ObjectID } from "../driver/mongodb/typings"
|
||||
import { FindOptionsWhere } from "../find-options/FindOptionsWhere"
|
||||
import { UpsertOptions } from "./UpsertOptions"
|
||||
import { EntityTarget } from "../common/EntityTarget"
|
||||
import { PickKeysByType } from "../common/PickKeysByType"
|
||||
|
||||
/**
|
||||
* Repository is supposed to work with your entity objects. Find entities, insert, update, delete, etc.
|
||||
@ -476,6 +477,46 @@ export class Repository<Entity extends ObjectLiteral> {
|
||||
return this.manager.countBy(this.metadata.target, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SUM of a column
|
||||
*/
|
||||
sum(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
return this.manager.sum(this.metadata.target, columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the AVG of a column
|
||||
*/
|
||||
average(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
return this.manager.average(this.metadata.target, columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MIN of a column
|
||||
*/
|
||||
minimum(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
return this.manager.minimum(this.metadata.target, columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MAX of a column
|
||||
*/
|
||||
maximum(
|
||||
columnName: PickKeysByType<Entity, number>,
|
||||
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
|
||||
): Promise<number | null> {
|
||||
return this.manager.maximum(this.metadata.target, columnName, where)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entities that match given find options.
|
||||
*/
|
||||
|
||||
12
test/functional/repository/aggregate-methods/entity/Post.ts
Normal file
12
test/functional/repository/aggregate-methods/entity/Post.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Entity } from "../../../../../src/decorator/entity/Entity"
|
||||
import { Column } from "../../../../../src/decorator/columns/Column"
|
||||
import { PrimaryColumn } from "../../../../../src/decorator/columns/PrimaryColumn"
|
||||
|
||||
@Entity()
|
||||
export class Post {
|
||||
@PrimaryColumn()
|
||||
id: number
|
||||
|
||||
@Column()
|
||||
counter: number
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
import "reflect-metadata"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
} from "../../../utils/test-utils"
|
||||
import { Repository } from "../../../../src/repository/Repository"
|
||||
import { DataSource } from "../../../../src/data-source/DataSource"
|
||||
import { Post } from "./entity/Post"
|
||||
import { LessThan } from "../../../../src"
|
||||
import { expect } from "chai"
|
||||
|
||||
describe("repository > aggregate methods", () => {
|
||||
debugger
|
||||
let connections: DataSource[]
|
||||
let repository: Repository<Post>
|
||||
|
||||
before(async () => {
|
||||
connections = await createTestingConnections({
|
||||
entities: [Post],
|
||||
schemaCreate: true,
|
||||
dropSchema: true,
|
||||
})
|
||||
repository = connections[0].getRepository(Post)
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const post = new Post()
|
||||
post.id = i
|
||||
post.counter = i + 1
|
||||
await repository.save(post)
|
||||
}
|
||||
})
|
||||
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
describe("sum", () => {
|
||||
it("should return the aggregate sum", async () => {
|
||||
const sum = await repository.sum("counter")
|
||||
expect(sum).to.equal(5050)
|
||||
})
|
||||
|
||||
it("should return null when 0 rows match the query", async () => {
|
||||
const sum = await repository.sum("counter", { id: LessThan(0) })
|
||||
expect(sum).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe("average", () => {
|
||||
it("should return the aggregate average", async () => {
|
||||
const average = await repository.average("counter")
|
||||
expect(average).to.equal(50.5)
|
||||
})
|
||||
|
||||
it("should return null when 0 rows match the query", async () => {
|
||||
const average = await repository.average("counter", {
|
||||
id: LessThan(0),
|
||||
})
|
||||
expect(average).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe("minimum", () => {
|
||||
it("should return the aggregate minimum", async () => {
|
||||
const minimum = await repository.minimum("counter")
|
||||
expect(minimum).to.equal(1)
|
||||
})
|
||||
|
||||
it("should return null when 0 rows match the query", async () => {
|
||||
const minimum = await repository.minimum("counter", {
|
||||
id: LessThan(0),
|
||||
})
|
||||
expect(minimum).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe("maximum", () => {
|
||||
it("should return the aggregate maximum", async () => {
|
||||
const maximum = await repository.maximum("counter")
|
||||
expect(maximum).to.equal(100)
|
||||
})
|
||||
|
||||
it("should return null when 0 rows match the query", async () => {
|
||||
const maximum = await repository.maximum("counter", {
|
||||
id: LessThan(0),
|
||||
})
|
||||
expect(maximum).to.be.null
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user