mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: support for Common Table Expressions (#8534)
* feat: support for Common Table Expressions Resolves #1116 #5899 * test: disable test for #4753 if no MySQL is present in ormconfig.json
This commit is contained in:
parent
a641c5dff8
commit
7cc1848fd4
@ -1155,3 +1155,37 @@ const users = await dataSource
|
||||
```
|
||||
|
||||
You will get all the rows, including the ones which are deleted.
|
||||
|
||||
## Common table expressions
|
||||
|
||||
`QueryBuilder` instances
|
||||
support [common table expressions](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL#Common_table_expression)
|
||||
, if minimal supported version of your database supports them. Common table expressions aren't supported for Oracle yet.
|
||||
|
||||
```typescript
|
||||
const users = await connection.getRepository(User)
|
||||
.createQueryBuilder('user')
|
||||
.select("user.id", 'id')
|
||||
.addCommonTableExpression(`
|
||||
SELECT "userId" FROM "post"
|
||||
`, 'post_users_ids')
|
||||
.where(`user.id IN (SELECT "userId" FROM 'post_users_ids')`)
|
||||
.getMany();
|
||||
```
|
||||
|
||||
Result values of `InsertQueryBuilder` or `UpdateQueryBuilder` can be used in Postgres:
|
||||
|
||||
```typescript
|
||||
const insertQueryBuilder = await connection.getRepository(User)
|
||||
.createQueryBuilder()
|
||||
.insert({
|
||||
name: 'John Smith'
|
||||
})
|
||||
.returning(['id']);
|
||||
|
||||
const users = await connection.getRepository(User)
|
||||
.createQueryBuilder('user')
|
||||
.addCommonTableExpression(insertQueryBuilder, 'insert_results')
|
||||
.where(`user.id IN (SELECT "id" FROM 'insert_results')`)
|
||||
.getMany();
|
||||
```
|
||||
|
||||
@ -2,6 +2,7 @@ import { QueryRunner } from "../query-runner/QueryRunner"
|
||||
import { ColumnMetadata } from "../metadata/ColumnMetadata"
|
||||
import { ObjectLiteral } from "../common/ObjectLiteral"
|
||||
import { ColumnType } from "./types/ColumnTypes"
|
||||
import { CteCapabilities } from "./types/CteCapabilities"
|
||||
import { MappedColumnTypes } from "./types/MappedColumnTypes"
|
||||
import { SchemaBuilder } from "../schema-builder/SchemaBuilder"
|
||||
import { DataTypeDefaults } from "./types/DataTypeDefaults"
|
||||
@ -99,6 +100,8 @@ export interface Driver {
|
||||
*/
|
||||
maxAliasLength?: number
|
||||
|
||||
cteCapabilities: CteCapabilities
|
||||
|
||||
/**
|
||||
* Performs connection to the database.
|
||||
* Depend on driver type it may create a connection pool.
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Driver } from "../Driver"
|
||||
import { DriverUtils } from "../DriverUtils"
|
||||
import { CteCapabilities } from "../types/CteCapabilities"
|
||||
import { AuroraMysqlQueryRunner } from "./AuroraMysqlQueryRunner"
|
||||
import { ObjectLiteral } from "../../common/ObjectLiteral"
|
||||
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
|
||||
@ -310,6 +311,10 @@ export class AuroraMysqlDriver implements Driver {
|
||||
*/
|
||||
maxAliasLength = 63
|
||||
|
||||
cteCapabilities: CteCapabilities = {
|
||||
enabled: false,
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -4,6 +4,7 @@ import { ObjectLiteral } from "../../common/ObjectLiteral"
|
||||
import { DriverPackageNotInstalledError } from "../../error/DriverPackageNotInstalledError"
|
||||
import { DriverUtils } from "../DriverUtils"
|
||||
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
|
||||
import { CteCapabilities } from "../types/CteCapabilities"
|
||||
import { CockroachConnectionCredentialsOptions } from "./CockroachConnectionCredentialsOptions"
|
||||
import { CockroachConnectionOptions } from "./CockroachConnectionOptions"
|
||||
import { DateUtils } from "../../util/DateUtils"
|
||||
@ -230,6 +231,13 @@ export class CockroachDriver implements Driver {
|
||||
*/
|
||||
maxAliasLength?: number
|
||||
|
||||
cteCapabilities: CteCapabilities = {
|
||||
enabled: true,
|
||||
writable: true,
|
||||
materializedHint: true,
|
||||
requiresRecursiveHint: true,
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Driver } from "../Driver"
|
||||
import { ConnectionIsNotSetError } from "../../error/ConnectionIsNotSetError"
|
||||
import { DriverPackageNotInstalledError } from "../../error/DriverPackageNotInstalledError"
|
||||
import { CteCapabilities } from "../types/CteCapabilities"
|
||||
import { MongoQueryRunner } from "./MongoQueryRunner"
|
||||
import { ObjectLiteral } from "../../common/ObjectLiteral"
|
||||
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
|
||||
@ -212,6 +213,10 @@ export class MongoDriver implements Driver {
|
||||
"retryWrites",
|
||||
]
|
||||
|
||||
cteCapabilities: CteCapabilities = {
|
||||
enabled: false,
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -2,6 +2,7 @@ import { Driver, ReturningType } from "../Driver"
|
||||
import { ConnectionIsNotSetError } from "../../error/ConnectionIsNotSetError"
|
||||
import { DriverPackageNotInstalledError } from "../../error/DriverPackageNotInstalledError"
|
||||
import { DriverUtils } from "../DriverUtils"
|
||||
import { CteCapabilities } from "../types/CteCapabilities"
|
||||
import { MysqlQueryRunner } from "./MysqlQueryRunner"
|
||||
import { ObjectLiteral } from "../../common/ObjectLiteral"
|
||||
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
|
||||
@ -309,6 +310,11 @@ export class MysqlDriver implements Driver {
|
||||
*/
|
||||
maxAliasLength = 63
|
||||
|
||||
cteCapabilities: CteCapabilities = {
|
||||
enabled: false,
|
||||
requiresRecursiveHint: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported returning types
|
||||
*/
|
||||
@ -391,18 +397,25 @@ export class MysqlDriver implements Driver {
|
||||
await queryRunner.release()
|
||||
}
|
||||
|
||||
if (this.options.type === "mariadb") {
|
||||
const result = (await this.createQueryRunner("master").query(
|
||||
`SELECT VERSION() AS \`version\``,
|
||||
)) as { version: string }[]
|
||||
const dbVersion = result[0].version
|
||||
const result = (await this.createQueryRunner("master").query(
|
||||
`SELECT VERSION() AS \`version\``,
|
||||
)) as { version: string }[]
|
||||
const dbVersion = result[0].version
|
||||
|
||||
if (this.options.type === "mariadb") {
|
||||
if (VersionUtils.isGreaterOrEqual(dbVersion, "10.0.5")) {
|
||||
this._isReturningSqlSupported.delete = true
|
||||
}
|
||||
if (VersionUtils.isGreaterOrEqual(dbVersion, "10.5.0")) {
|
||||
this._isReturningSqlSupported.insert = true
|
||||
}
|
||||
if (VersionUtils.isGreaterOrEqual(dbVersion, "10.2.0")) {
|
||||
this.cteCapabilities.enabled = true
|
||||
}
|
||||
} else if (this.options.type === "mysql") {
|
||||
if (VersionUtils.isGreaterOrEqual(dbVersion, "8.0.0")) {
|
||||
this.cteCapabilities.enabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Driver } from "../Driver"
|
||||
import { ConnectionIsNotSetError } from "../../error/ConnectionIsNotSetError"
|
||||
import { DriverPackageNotInstalledError } from "../../error/DriverPackageNotInstalledError"
|
||||
import { CteCapabilities } from "../types/CteCapabilities"
|
||||
import { OracleQueryRunner } from "./OracleQueryRunner"
|
||||
import { ObjectLiteral } from "../../common/ObjectLiteral"
|
||||
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
|
||||
@ -220,6 +221,10 @@ export class OracleDriver implements Driver {
|
||||
*/
|
||||
maxAliasLength = 29
|
||||
|
||||
cteCapabilities: CteCapabilities = {
|
||||
enabled: false, // TODO: enable
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -13,6 +13,7 @@ import { DateUtils } from "../../util/DateUtils"
|
||||
import { OrmUtils } from "../../util/OrmUtils"
|
||||
import { Driver } from "../Driver"
|
||||
import { ColumnType } from "../types/ColumnTypes"
|
||||
import { CteCapabilities } from "../types/CteCapabilities"
|
||||
import { DataTypeDefaults } from "../types/DataTypeDefaults"
|
||||
import { MappedColumnTypes } from "../types/MappedColumnTypes"
|
||||
import { ReplicationMode } from "../types/ReplicationMode"
|
||||
@ -272,6 +273,13 @@ export class PostgresDriver implements Driver {
|
||||
|
||||
isGeneratedColumnsSupported: boolean = false
|
||||
|
||||
cteCapabilities: CteCapabilities = {
|
||||
enabled: true,
|
||||
writable: true,
|
||||
requiresRecursiveHint: true,
|
||||
materializedHint: true,
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -16,6 +16,7 @@ import { ApplyValueTransformers } from "../../util/ApplyValueTransformers"
|
||||
import { DateUtils } from "../../util/DateUtils"
|
||||
import { OrmUtils } from "../../util/OrmUtils"
|
||||
import { Driver } from "../Driver"
|
||||
import { CteCapabilities } from "../types/CteCapabilities"
|
||||
import { DataTypeDefaults } from "../types/DataTypeDefaults"
|
||||
import { MappedColumnTypes } from "../types/MappedColumnTypes"
|
||||
import { SapConnectionOptions } from "./SapConnectionOptions"
|
||||
@ -204,6 +205,10 @@ export class SapDriver implements Driver {
|
||||
*/
|
||||
maxAliasLength = 128
|
||||
|
||||
cteCapabilities: CteCapabilities = {
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -4,6 +4,7 @@ import { ColumnMetadata } from "../../metadata/ColumnMetadata"
|
||||
import { DateUtils } from "../../util/DateUtils"
|
||||
import { DataSource } from "../../data-source/DataSource"
|
||||
import { RdbmsSchemaBuilder } from "../../schema-builder/RdbmsSchemaBuilder"
|
||||
import { CteCapabilities } from "../types/CteCapabilities"
|
||||
import { MappedColumnTypes } from "../types/MappedColumnTypes"
|
||||
import { ColumnType } from "../types/ColumnTypes"
|
||||
import { QueryRunner } from "../../query-runner/QueryRunner"
|
||||
@ -223,6 +224,11 @@ export abstract class AbstractSqliteDriver implements Driver {
|
||||
*/
|
||||
maxAliasLength?: number
|
||||
|
||||
cteCapabilities: CteCapabilities = {
|
||||
enabled: true,
|
||||
requiresRecursiveHint: true,
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -2,6 +2,7 @@ import { Driver } from "../Driver"
|
||||
import { ConnectionIsNotSetError } from "../../error/ConnectionIsNotSetError"
|
||||
import { DriverPackageNotInstalledError } from "../../error/DriverPackageNotInstalledError"
|
||||
import { DriverUtils } from "../DriverUtils"
|
||||
import { CteCapabilities } from "../types/CteCapabilities"
|
||||
import { SqlServerQueryRunner } from "./SqlServerQueryRunner"
|
||||
import { ObjectLiteral } from "../../common/ObjectLiteral"
|
||||
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
|
||||
@ -222,6 +223,12 @@ export class SqlServerDriver implements Driver {
|
||||
datetimeoffset: { precision: 7 },
|
||||
}
|
||||
|
||||
cteCapabilities: CteCapabilities = {
|
||||
enabled: true,
|
||||
// todo: enable it for SQL Server - it's partially supported, but there are issues with generation of non-standard OUTPUT clause
|
||||
writable: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* Max length allowed by MSSQL Server for aliases (identifiers).
|
||||
* @see https://docs.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server
|
||||
|
||||
20
src/driver/types/CteCapabilities.ts
Normal file
20
src/driver/types/CteCapabilities.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export interface CteCapabilities {
|
||||
/**
|
||||
* Are CTEs supported at all?
|
||||
*/
|
||||
enabled: boolean
|
||||
|
||||
/**
|
||||
* Are RETURNING clauses supported in CTEs?
|
||||
*/
|
||||
writable?: boolean
|
||||
/**
|
||||
* Is RECURSIVE clause required for recursive CTEs?
|
||||
*/
|
||||
requiresRecursiveHint?: boolean
|
||||
|
||||
/**
|
||||
* Is MATERIALIZED clause supported?
|
||||
*/
|
||||
materializedHint?: boolean
|
||||
}
|
||||
@ -39,6 +39,7 @@ export class DeleteQueryBuilder<Entity>
|
||||
*/
|
||||
getQuery(): string {
|
||||
let sql = this.createComment()
|
||||
sql += this.createCteExpression()
|
||||
sql += this.createDeleteExpression()
|
||||
return sql.trim()
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
|
||||
*/
|
||||
getQuery(): string {
|
||||
let sql = this.createComment()
|
||||
sql += this.createCteExpression()
|
||||
sql += this.createInsertExpression()
|
||||
return sql.trim()
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ObjectLiteral } from "../common/ObjectLiteral"
|
||||
import { QueryRunner } from "../query-runner/QueryRunner"
|
||||
import { DataSource } from "../data-source/DataSource"
|
||||
import { QueryBuilderCteOptions } from "./QueryBuilderCte"
|
||||
import { QueryExpressionMap } from "./QueryExpressionMap"
|
||||
import { SelectQueryBuilder } from "./SelectQueryBuilder"
|
||||
import { UpdateQueryBuilder } from "./UpdateQueryBuilder"
|
||||
@ -596,6 +597,22 @@ export abstract class QueryBuilder<Entity> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds CTE to query
|
||||
*/
|
||||
addCommonTableExpression(
|
||||
queryBuilder: QueryBuilder<any> | string,
|
||||
alias: string,
|
||||
options?: QueryBuilderCteOptions,
|
||||
): this {
|
||||
this.expressionMap.commonTableExpressions.push({
|
||||
queryBuilder,
|
||||
alias,
|
||||
options: options || {},
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
@ -1048,6 +1065,80 @@ export abstract class QueryBuilder<Entity> {
|
||||
)
|
||||
}
|
||||
|
||||
protected createCteExpression(): string {
|
||||
if (!this.hasCommonTableExpressions()) {
|
||||
return ""
|
||||
}
|
||||
const databaseRequireRecusiveHint =
|
||||
this.connection.driver.cteCapabilities.requiresRecursiveHint
|
||||
|
||||
const cteStrings = this.expressionMap.commonTableExpressions.map(
|
||||
(cte) => {
|
||||
const cteBodyExpression =
|
||||
typeof cte.queryBuilder === "string"
|
||||
? cte.queryBuilder
|
||||
: cte.queryBuilder.getQuery()
|
||||
if (typeof cte.queryBuilder !== "string") {
|
||||
if (cte.queryBuilder.hasCommonTableExpressions()) {
|
||||
throw new TypeORMError(
|
||||
`Nested CTEs aren't supported (CTE: ${cte.alias})`,
|
||||
)
|
||||
}
|
||||
if (
|
||||
!this.connection.driver.cteCapabilities.writable &&
|
||||
!InstanceChecker.isSelectQueryBuilder(cte.queryBuilder)
|
||||
) {
|
||||
throw new TypeORMError(
|
||||
`Only select queries are supported in CTEs in ${this.connection.options.type} (CTE: ${cte.alias})`,
|
||||
)
|
||||
}
|
||||
this.setParameters(cte.queryBuilder.getParameters())
|
||||
}
|
||||
let cteHeader = this.escape(cte.alias)
|
||||
if (cte.options.columnNames) {
|
||||
const escapedColumnNames = cte.options.columnNames.map(
|
||||
(column) => this.escape(column),
|
||||
)
|
||||
if (
|
||||
InstanceChecker.isSelectQueryBuilder(cte.queryBuilder)
|
||||
) {
|
||||
if (
|
||||
cte.queryBuilder.expressionMap.selects.length &&
|
||||
cte.options.columnNames.length !==
|
||||
cte.queryBuilder.expressionMap.selects.length
|
||||
) {
|
||||
throw new TypeORMError(
|
||||
`cte.options.columnNames length (${cte.options.columnNames.length}) doesn't match subquery select list length ${cte.queryBuilder.expressionMap.selects.length} (CTE: ${cte.alias})`,
|
||||
)
|
||||
}
|
||||
}
|
||||
cteHeader += `(${escapedColumnNames.join(", ")})`
|
||||
}
|
||||
const recursiveClause =
|
||||
cte.options.recursive && databaseRequireRecusiveHint
|
||||
? "RECURSIVE"
|
||||
: ""
|
||||
const materializeClause =
|
||||
cte.options.materialized &&
|
||||
this.connection.driver.cteCapabilities.materializedHint
|
||||
? "MATERIALIZED"
|
||||
: ""
|
||||
|
||||
return [
|
||||
recursiveClause,
|
||||
cteHeader,
|
||||
materializeClause,
|
||||
"AS",
|
||||
`(${cteBodyExpression})`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
},
|
||||
)
|
||||
|
||||
return "WITH " + cteStrings.join(", ") + " "
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates "WHERE" condition for an in-ids condition.
|
||||
*/
|
||||
@ -1459,4 +1550,8 @@ export abstract class QueryBuilder<Entity> {
|
||||
protected obtainQueryRunner() {
|
||||
return this.queryRunner || this.connection.createQueryRunner()
|
||||
}
|
||||
|
||||
protected hasCommonTableExpressions(): boolean {
|
||||
return this.expressionMap.commonTableExpressions.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
17
src/query-builder/QueryBuilderCte.ts
Normal file
17
src/query-builder/QueryBuilderCte.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export interface QueryBuilderCteOptions {
|
||||
/**
|
||||
* Supported only by Postgres currently
|
||||
* Oracle users should use query with undocumented materialize hint
|
||||
*/
|
||||
materialized?: boolean
|
||||
/**
|
||||
* Supported by Postgres, SQLite, MySQL and MariaDB
|
||||
* SQL Server automatically detects recursive queries
|
||||
*/
|
||||
recursive?: boolean
|
||||
/**
|
||||
* Overwrite column names
|
||||
* If number of columns returned doesn't work, it throws
|
||||
*/
|
||||
columnNames?: string[]
|
||||
}
|
||||
@ -2,6 +2,8 @@ import { Alias } from "./Alias"
|
||||
import { ObjectLiteral } from "../common/ObjectLiteral"
|
||||
import { OrderByCondition } from "../find-options/OrderByCondition"
|
||||
import { JoinAttribute } from "./JoinAttribute"
|
||||
import { QueryBuilder } from "./QueryBuilder"
|
||||
import { QueryBuilderCteOptions } from "./QueryBuilderCte"
|
||||
import { RelationIdAttribute } from "./relation-id/RelationIdAttribute"
|
||||
import { RelationCountAttribute } from "./relation-count/RelationCountAttribute"
|
||||
import { DataSource } from "../data-source/DataSource"
|
||||
@ -320,6 +322,12 @@ export class QueryExpressionMap {
|
||||
*/
|
||||
locallyGenerated: { [key: number]: ObjectLiteral } = {}
|
||||
|
||||
commonTableExpressions: {
|
||||
queryBuilder: QueryBuilder<any> | string
|
||||
alias: string
|
||||
options: QueryBuilderCteOptions
|
||||
}[] = []
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
@ -504,6 +512,16 @@ export class QueryExpressionMap {
|
||||
map.useTransaction = this.useTransaction
|
||||
map.nativeParameters = Object.assign({}, this.nativeParameters)
|
||||
map.comment = this.comment
|
||||
map.commonTableExpressions = this.commonTableExpressions.map(
|
||||
(cteOptions) => ({
|
||||
alias: cteOptions.alias,
|
||||
queryBuilder:
|
||||
typeof cteOptions.queryBuilder === "string"
|
||||
? cteOptions.queryBuilder
|
||||
: cteOptions.queryBuilder.clone(),
|
||||
options: cteOptions.options,
|
||||
}),
|
||||
)
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +80,7 @@ export class SelectQueryBuilder<Entity>
|
||||
*/
|
||||
getQuery(): string {
|
||||
let sql = this.createComment()
|
||||
sql += this.createCteExpression()
|
||||
sql += this.createSelectExpression()
|
||||
sql += this.createJoinExpression()
|
||||
sql += this.createWhereExpression()
|
||||
|
||||
@ -46,6 +46,7 @@ export class SoftDeleteQueryBuilder<Entity>
|
||||
*/
|
||||
getQuery(): string {
|
||||
let sql = this.createUpdateExpression()
|
||||
sql += this.createCteExpression()
|
||||
sql += this.createOrderByExpression()
|
||||
sql += this.createLimitExpression()
|
||||
return sql.trim()
|
||||
|
||||
@ -49,6 +49,7 @@ export class UpdateQueryBuilder<Entity>
|
||||
*/
|
||||
getQuery(): string {
|
||||
let sql = this.createComment()
|
||||
sql += this.createCteExpression()
|
||||
sql += this.createUpdateExpression()
|
||||
sql += this.createOrderByExpression()
|
||||
sql += this.createLimitExpression()
|
||||
|
||||
12
test/functional/query-builder/cte/entity/foo.ts
Normal file
12
test/functional/query-builder/cte/entity/foo.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { PrimaryColumn } from "../../../../../src"
|
||||
import { Entity } from "../../../../../src/decorator/entity/Entity"
|
||||
import { Column } from "../../../../../src/decorator/columns/Column"
|
||||
|
||||
@Entity()
|
||||
export class Foo {
|
||||
@PrimaryColumn()
|
||||
id: number
|
||||
|
||||
@Column("varchar")
|
||||
bar: string
|
||||
}
|
||||
9
test/functional/query-builder/cte/helpers.ts
Normal file
9
test/functional/query-builder/cte/helpers.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Connection } from "../../../../src"
|
||||
import { CteCapabilities } from "../../../../src/driver/types/CteCapabilities"
|
||||
|
||||
export function filterByCteCapabilities(
|
||||
capability: keyof CteCapabilities,
|
||||
equalsTo: boolean = true,
|
||||
): (conn: Connection) => boolean {
|
||||
return (conn) => conn.driver.cteCapabilities[capability] === equalsTo
|
||||
}
|
||||
49
test/functional/query-builder/cte/recursive-cte.ts
Normal file
49
test/functional/query-builder/cte/recursive-cte.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { expect } from "chai"
|
||||
import { Connection } from "../../../../src"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../utils/test-utils"
|
||||
import { filterByCteCapabilities } from "./helpers"
|
||||
|
||||
describe("query builder > cte > recursive", () => {
|
||||
let connections: Connection[]
|
||||
before(
|
||||
async () =>
|
||||
(connections = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
schemaCreate: true,
|
||||
dropSchema: true,
|
||||
})),
|
||||
)
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
it("should work with simple recursive query", () =>
|
||||
Promise.all(
|
||||
connections
|
||||
.filter(filterByCteCapabilities("enabled"))
|
||||
.map(async (connection) => {
|
||||
const qb = await connection
|
||||
.createQueryBuilder()
|
||||
.select([])
|
||||
.from("cte", "cte")
|
||||
.addCommonTableExpression(
|
||||
`
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT cte.foo + 1
|
||||
FROM cte
|
||||
WHERE cte.foo < 10
|
||||
`,
|
||||
"cte",
|
||||
{ recursive: true, columnNames: ["foo"] },
|
||||
)
|
||||
.addSelect("cte.foo", "foo")
|
||||
.getRawMany<{ foo: number }>()
|
||||
|
||||
expect(qb).to.have.length(10)
|
||||
}),
|
||||
))
|
||||
})
|
||||
146
test/functional/query-builder/cte/simple-cte.ts
Normal file
146
test/functional/query-builder/cte/simple-cte.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import "reflect-metadata"
|
||||
import { expect } from "chai"
|
||||
import {
|
||||
createTestingConnections,
|
||||
closeTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../utils/test-utils"
|
||||
import { Connection } from "../../../../src/connection/Connection"
|
||||
import { Foo } from "./entity/foo"
|
||||
import { filterByCteCapabilities } from "./helpers"
|
||||
|
||||
describe("query builder > cte > simple", () => {
|
||||
let connections: Connection[]
|
||||
before(
|
||||
async () =>
|
||||
(connections = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
schemaCreate: true,
|
||||
dropSchema: true,
|
||||
})),
|
||||
)
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
it("show allow select from CTE", () =>
|
||||
Promise.all(
|
||||
connections
|
||||
.filter(filterByCteCapabilities("enabled"))
|
||||
.map(async (connection) => {
|
||||
await connection
|
||||
.getRepository(Foo)
|
||||
.insert(
|
||||
[1, 2, 3].map((i) => ({ id: i, bar: String(i) })),
|
||||
)
|
||||
const cteQuery = connection
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.addSelect(`foo.bar`)
|
||||
.from(Foo, "foo")
|
||||
.where(`foo.bar = :value`, { value: "2" })
|
||||
|
||||
const qb = await connection
|
||||
.createQueryBuilder()
|
||||
.addCommonTableExpression(cteQuery, "qaz", {
|
||||
columnNames: ["raz"],
|
||||
})
|
||||
.from("qaz", "qaz")
|
||||
.select([])
|
||||
.addSelect("qaz.raz", "raz")
|
||||
|
||||
expect(await qb.getRawMany()).to.deep.equal([{ raz: "2" }])
|
||||
}),
|
||||
))
|
||||
|
||||
it("should allow join with CTE", () =>
|
||||
Promise.all(
|
||||
connections
|
||||
.filter(filterByCteCapabilities("enabled"))
|
||||
.map(async (connection) => {
|
||||
await connection
|
||||
.getRepository(Foo)
|
||||
.insert(
|
||||
[1, 2, 3].map((i) => ({ id: i, bar: String(i) })),
|
||||
)
|
||||
const cteQuery = connection
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.addSelect("bar")
|
||||
.from(Foo, "foo")
|
||||
.where(`foo.bar = '2'`)
|
||||
|
||||
const results = await connection
|
||||
.createQueryBuilder(Foo, "foo")
|
||||
.addCommonTableExpression(cteQuery, "qaz", {
|
||||
columnNames: ["raz"],
|
||||
})
|
||||
.innerJoin("qaz", "qaz", "qaz.raz = foo.bar")
|
||||
.getMany()
|
||||
|
||||
expect(results).to.have.length(1)
|
||||
|
||||
expect(results[0]).to.include({
|
||||
bar: "2",
|
||||
})
|
||||
}),
|
||||
))
|
||||
|
||||
it("should allow to use INSERT with RETURNING clause in CTE", () =>
|
||||
Promise.all(
|
||||
connections
|
||||
.filter(filterByCteCapabilities("writable"))
|
||||
.map(async (connection) => {
|
||||
const bar = Math.random().toString()
|
||||
const cteQuery = connection
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(Foo)
|
||||
.values({
|
||||
id: 7,
|
||||
bar,
|
||||
})
|
||||
.returning(["id", "bar"])
|
||||
|
||||
const results = await connection
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.addCommonTableExpression(cteQuery, "insert_result")
|
||||
.from("insert_result", "insert_result")
|
||||
.getRawMany()
|
||||
|
||||
expect(results).to.have.length(1)
|
||||
|
||||
expect(results[0]).to.include({
|
||||
bar,
|
||||
})
|
||||
}),
|
||||
))
|
||||
|
||||
it("should allow string for CTE", () =>
|
||||
Promise.all(
|
||||
connections
|
||||
.filter(filterByCteCapabilities("enabled"))
|
||||
.map(async (connection) => {
|
||||
const results = await connection
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.addCommonTableExpression(
|
||||
`
|
||||
SELECT 1
|
||||
UNION
|
||||
SELECT 2
|
||||
`,
|
||||
"cte",
|
||||
{ columnNames: ["foo"] },
|
||||
)
|
||||
.from("cte", "cte")
|
||||
.addSelect("foo", "row")
|
||||
.getRawMany<{ row: any }>()
|
||||
|
||||
const [rowWithOne, rowWithTwo] = results
|
||||
|
||||
expect(String(rowWithOne.row)).to.equal("1")
|
||||
expect(String(rowWithTwo.row)).to.equal("2")
|
||||
}),
|
||||
))
|
||||
})
|
||||
@ -1,31 +1,53 @@
|
||||
import { getConnectionManager } from "../../../src"
|
||||
import { DataSource } from "../../../src/data-source/DataSource"
|
||||
import { closeTestingConnections } from "../../utils/test-utils"
|
||||
import { MysqlConnectionOptions } from "../../../src/driver/mysql/MysqlConnectionOptions"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
getTypeOrmConfig,
|
||||
TestingConnectionOptions,
|
||||
} from "../../utils/test-utils"
|
||||
import { User } from "./entity/User"
|
||||
|
||||
function isMySql(v: TestingConnectionOptions): v is MysqlConnectionOptions {
|
||||
return v.type === "mysql"
|
||||
}
|
||||
|
||||
describe("github issues > #4753 MySQL Replication Config broken", () => {
|
||||
let connections: DataSource[] = []
|
||||
after(() => closeTestingConnections(connections))
|
||||
|
||||
it("should connect without error when using replication", async () => {
|
||||
const connection = getConnectionManager().create({
|
||||
const connectionOptions: MysqlConnectionOptions | undefined =
|
||||
getTypeOrmConfig()
|
||||
.filter((v) => !v.skip)
|
||||
.find(isMySql)
|
||||
|
||||
if (!connectionOptions) {
|
||||
// Skip if MySQL tests aren't enabled at all
|
||||
return
|
||||
}
|
||||
const connectionManager = getConnectionManager()
|
||||
const connection = connectionManager.create({
|
||||
type: "mysql",
|
||||
replication: {
|
||||
master: {
|
||||
username: "test",
|
||||
password: "test",
|
||||
database: "test",
|
||||
host: connectionOptions.host,
|
||||
username: connectionOptions.username,
|
||||
password: connectionOptions.password,
|
||||
database: connectionOptions.database,
|
||||
},
|
||||
slaves: [
|
||||
{
|
||||
username: "test",
|
||||
password: "test",
|
||||
database: "test",
|
||||
host: connectionOptions.host,
|
||||
username: connectionOptions.username,
|
||||
password: connectionOptions.password,
|
||||
database: connectionOptions.database,
|
||||
},
|
||||
],
|
||||
},
|
||||
entities: [User],
|
||||
})
|
||||
|
||||
connections.push(connection)
|
||||
await connection.connect()
|
||||
connection.isInitialized.should.be.true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user