From aebc7ebc67528ab5f980c7216d3f131d484f4e0d Mon Sep 17 00:00:00 2001 From: Lucian Mocanu Date: Tue, 1 Jul 2025 23:43:12 +0200 Subject: [PATCH] feat(sap): use the native driver for connection pooling (#11520) * feat(sap): use the native driver for connection pooling * Add pool error handler --- README-zh_CN.md | 2 - README.md | 3 - README_ko.md | 3 - .../docs/data-source/2-data-source-options.md | 18 ++ docs/docs/getting-started.md | 11 +- package-lock.json | 18 +- package.json | 7 +- src/driver/sap/SapConnectionOptions.ts | 49 ++++- src/driver/sap/SapDriver.ts | 188 ++++++++++-------- src/driver/sap/SapQueryRunner.ts | 82 ++++---- src/platform/PlatformTools.ts | 3 - test/functional/driver/sap/connection-pool.ts | 72 +++++++ 12 files changed, 278 insertions(+), 178 deletions(-) create mode 100644 test/functional/driver/sap/connection-pool.ts diff --git a/README-zh_CN.md b/README-zh_CN.md index 27179dba9..1652114df 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -203,9 +203,7 @@ await timber.remove() - **SAP Hana** ``` - npm config set @sap:registry https://npm.sap.com npm i @sap/hana-client - npm i hdb-pool ``` ##### TypeScript 配置 diff --git a/README.md b/README.md index ea118e475..ea495505e 100644 --- a/README.md +++ b/README.md @@ -215,11 +215,8 @@ await timber.remove() ``` npm install @sap/hana-client - npm install hdb-pool ``` - _SAP Hana support made possible by the sponsorship of [Neptune Software](https://www.neptune-software.com/)._ - - for **Google Cloud Spanner** ``` diff --git a/README_ko.md b/README_ko.md index 149ada4de..93cdd7de1 100644 --- a/README_ko.md +++ b/README_ko.md @@ -176,11 +176,8 @@ await timber.remove() ``` npm i @sap/hana-client - npm i hdb-pool ``` - _[Neptune Software](https://www.neptune-software.com/)의 후원으로 SAP Hana 지원이 가능해졌다._ - - **MongoDB** (experimental)의 경우 `npm install mongodb@^5.2.0 --save` diff --git a/docs/docs/data-source/2-data-source-options.md b/docs/docs/data-source/2-data-source-options.md index a01e11f9c..0ed6b70fa 100644 --- a/docs/docs/data-source/2-data-source-options.md +++ b/docs/docs/data-source/2-data-source-options.md @@ -544,6 +544,24 @@ The following TNS connection string will be used in the next explanations: - `sid` - The System Identifier (SID) identifies a specific database instance. For example, "sales". - `serviceName` - The Service Name is an identifier of a database service. For example, `sales.us.example.com`. +## `sap` data source options + +- `host` - The hostname of the SAP HANA server. For example, `"localhost"`. +- `port` - The port number of the SAP HANA server. For example, `30015`. +- `username` - The username to connect to the SAP HANA server. For example, `"SYSTEM"`. +- `password` - The password to connect to the SAP HANA server. For example, `"password"`. +- `database` - The name of the database to connect to. For example, `"HXE"`. +- `encrypt` - Whether to encrypt the connection. For example, `true`. +- `sslValidateCertificate` - Whether to validate the SSL certificate. For example, `true`. +- `key`, `cert` and `ca` - Private key, public certificate and certificate authority for the encrypted connection. +- `pool` — Connection pool configuration object: + - `maxConnectedOrPooled` (number) — Max active or idle connections in the pool (default: 10). + - `maxPooledIdleTime` (seconds) — Time before an idle connection is closed (default: 30). + - `pingCheck` (boolean) — Whether to validate connections before use (default: false). + - `poolCapacity` (number) — Maximum number of connections to be kept available (default: no limit). + +See the official documentation of SAP HANA Client for more details as well as the `extra` properties: [Node.js Connection Properties](https://help.sap.com/docs/SAP_HANA_CLIENT/f1b440ded6144a54ada97ff95dac7adf/4fe9978ebac44f35b9369ef5a4a26f4c.html). + ## Data Source Options example Here is a small example of data source options for mysql: diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 7a39757f9..f054bf83b 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -193,18 +193,11 @@ await timber.remove() - for **SAP Hana** - ``` - npm install @sap/hana-client - npm install hdb-pool - ``` - - _SAP Hana support made possible by the sponsorship of [Neptune Software](https://www.neptune-software.com/)._ + `npm install @sap/hana-client --save` - for **Google Cloud Spanner** - ``` - npm install @google-cloud/spanner --save - ``` + `npm install @google-cloud/spanner --save` Provide authentication credentials to your application code by setting the environment variable `GOOGLE_APPLICATION_CREDENTIALS`: diff --git a/package-lock.json b/package-lock.json index 1905d07b9..50e42df16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,6 @@ "gulp-sourcemaps": "^3.0.0", "gulp-typescript": "^6.0.0-alpha.1", "gulpclass": "^0.2.0", - "hdb-pool": "^0.1.6", "husky": "^9.1.7", "lint-staged": "^15.5.2", "mocha": "^10.8.2", @@ -95,9 +94,8 @@ }, "peerDependencies": { "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", - "@sap/hana-client": "^2.12.25", + "@sap/hana-client": "^2.14.22", "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", - "hdb-pool": "^0.1.6", "ioredis": "^5.0.4", "mongodb": "^5.8.0 || ^6.0.0", "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", @@ -123,9 +121,6 @@ "better-sqlite3": { "optional": true }, - "hdb-pool": { - "optional": true - }, "ioredis": { "optional": true }, @@ -8384,17 +8379,6 @@ "node": ">= 0.4" } }, - "node_modules/hdb-pool": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/hdb-pool/-/hdb-pool-0.1.6.tgz", - "integrity": "sha512-8VZOLn1EHamm1NmTFQj2iqjVcfonYIsD7F5DU2bz2N+gF+knp6/MbAVeRXkJtya717IBkPeA5iv0/1iPuYo4ZA==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", diff --git a/package.json b/package.json index 4a654030d..0b5319c22 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,6 @@ "gulp-sourcemaps": "^3.0.0", "gulp-typescript": "^6.0.0-alpha.1", "gulpclass": "^0.2.0", - "hdb-pool": "^0.1.6", "husky": "^9.1.7", "lint-staged": "^15.5.2", "mocha": "^10.8.2", @@ -167,9 +166,8 @@ }, "peerDependencies": { "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", - "@sap/hana-client": "^2.12.25", + "@sap/hana-client": "^2.14.22", "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", - "hdb-pool": "^0.1.6", "ioredis": "^5.0.4", "mongodb": "^5.8.0 || ^6.0.0", "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", @@ -195,9 +193,6 @@ "better-sqlite3": { "optional": true }, - "hdb-pool": { - "optional": true - }, "ioredis": { "optional": true }, diff --git a/src/driver/sap/SapConnectionOptions.ts b/src/driver/sap/SapConnectionOptions.ts index f2ccfd10d..a2100416a 100644 --- a/src/driver/sap/SapConnectionOptions.ts +++ b/src/driver/sap/SapConnectionOptions.ts @@ -19,13 +19,12 @@ export interface SapConnectionOptions /** * The driver objects - * This defaults to require("hdb-pool") + * This defaults to require("@sap/hana-client") */ readonly driver?: any /** - * The driver objects - * This defaults to require("@sap/hana-client") + * @deprecated Use {@link driver} instead. */ readonly hanaClientDriver?: any @@ -33,30 +32,64 @@ export interface SapConnectionOptions * Pool options. */ readonly pool?: { + /** + * Maximum number of open connections created by the pool, each of which + * may be in the pool waiting to be reused or may no longer be in the + * pool and actively being used (default: 10). + */ + readonly maxConnectedOrPooled?: number + + /** + * Defines the maximum time, in seconds, that connections are allowed to + * remain in the pool before being marked for eviction (default: 30). + */ + readonly maxPooledIdleTime?: number + + /** + * Determines whether or not the pooled connection should be tested for + * viability before being reused (default: false). + */ + readonly pingCheck?: boolean + + /** + * Maximum number of connections allowed to be in the pool, waiting to + * be reused (default: 0, no limit). + */ + readonly poolCapacity?: number + /** * Max number of connections. + * @deprecated Use {@link maxConnectedOrPooled} instead. */ readonly max?: number /** * Minimum number of connections. + * @deprecated Obsolete, no alternative exists. */ readonly min?: number /** - * Maximum number of waiting requests allowed. (default=0, no limit). + * Maximum number of waiting requests allowed. + * @deprecated Obsolete, no alternative exists. */ readonly maxWaitingRequests?: number + /** - * Max milliseconds a request will wait for a resource before timing out. (default=5000) + * Max milliseconds a request will wait for a resource before timing out. + * @deprecated Obsolete, no alternative exists. */ readonly requestTimeout?: number + /** - * How often to run resource timeout checks. (default=0, disabled) + * How often to run resource timeout checks. + * @deprecated Obsolete, no alternative exists. */ readonly checkInterval?: number + /** - * Idle timeout + * Idle timeout (in milliseconds). + * @deprecated Use {@link maxPooledIdleTime} (in seconds) instead . */ readonly idleTimeout?: number @@ -66,6 +99,4 @@ export interface SapConnectionOptions */ readonly poolErrorHandler?: (err: any) => any } - - readonly poolSize?: never } diff --git a/src/driver/sap/SapDriver.ts b/src/driver/sap/SapDriver.ts index e1128b5b4..1891d2420 100644 --- a/src/driver/sap/SapDriver.ts +++ b/src/driver/sap/SapDriver.ts @@ -1,3 +1,4 @@ +import { promisify } from "node:util" import { ColumnType, ConnectionIsNotSetError, @@ -13,21 +14,20 @@ import { TypeORMError } from "../../error/TypeORMError" import { ColumnMetadata } from "../../metadata/ColumnMetadata" import { PlatformTools } from "../../platform/PlatformTools" import { RdbmsSchemaBuilder } from "../../schema-builder/RdbmsSchemaBuilder" +import { View } from "../../schema-builder/view/View" import { ApplyValueTransformers } from "../../util/ApplyValueTransformers" import { DateUtils } from "../../util/DateUtils" +import { InstanceChecker } from "../../util/InstanceChecker" import { OrmUtils } from "../../util/OrmUtils" import { Driver } from "../Driver" +import { DriverUtils } from "../DriverUtils" import { CteCapabilities } from "../types/CteCapabilities" import { DataTypeDefaults } from "../types/DataTypeDefaults" import { MappedColumnTypes } from "../types/MappedColumnTypes" +import { ReplicationMode } from "../types/ReplicationMode" +import { UpsertType } from "../types/UpsertType" import { SapConnectionOptions } from "./SapConnectionOptions" import { SapQueryRunner } from "./SapQueryRunner" -import { ReplicationMode } from "../types/ReplicationMode" -import { DriverUtils } from "../DriverUtils" -import { View } from "../../schema-builder/view/View" -import { InstanceChecker } from "../../util/InstanceChecker" -import { UpsertType } from "../types/UpsertType" - /** * Organizes communication with SAP Hana DBMS. * @@ -44,24 +44,24 @@ export class SapDriver implements Driver { connection: DataSource /** - * Hana Pool instance. + * SAP HANA Client Pool instance. */ client: any /** - * Hana Client streaming extension. + * SAP HANA Client streaming extension. */ streamClient: any + /** * Pool for master database. */ master: any /** - * Pool for slave databases. - * Used in replication. + * Function handling errors thrown by drivers pool. */ - slaves: any[] = [] + poolErrorHandler: (error: any) => void // ------------------------------------------------------------------------- // Public Implemented Properties @@ -225,7 +225,7 @@ export class SapDriver implements Driver { /** * Max length allowed by SAP HANA for aliases (identifiers). - * @see https://help.sap.com/viewer/4fe29514fd584807ac9f2a04f6754767/2.0.03/en-US/20a760537519101497e3cfe07b348f3c.html + * @see https://help.sap.com/docs/hana-cloud-database/sap-hana-cloud-sap-hana-database-sql-reference-guide/system-limitations */ maxAliasLength = 128 @@ -259,54 +259,62 @@ export class SapDriver implements Driver { */ async connect(): Promise { // HANA connection info - const dbParams = { - hostName: this.options.host, + const connectionOptions: any = { + host: this.options.host, port: this.options.port, - userName: this.options.username, + user: this.options.username, password: this.options.password, - ...this.options.extra, + database: this.options.database, + currentSchema: this.options.schema, + encrypt: this.options.encrypt, + sslValidateCertificate: this.options.sslValidateCertificate, + key: this.options.key, + cert: this.options.cert, + ca: this.options.ca, } - - if (this.options.database) dbParams.databaseName = this.options.database - if (this.options.schema) dbParams.currentSchema = this.options.schema - if (this.options.encrypt) dbParams.encrypt = this.options.encrypt - if (this.options.sslValidateCertificate) - dbParams.validateCertificate = this.options.sslValidateCertificate - if (this.options.key) dbParams.key = this.options.key - if (this.options.cert) dbParams.cert = this.options.cert - if (this.options.ca) dbParams.ca = this.options.ca + Object.keys(connectionOptions).forEach((key) => { + if (connectionOptions[key] === undefined) { + delete connectionOptions[key] + } + }) + Object.assign(connectionOptions, this.options.extra ?? {}) // pool options - const options: any = { - min: - this.options.pool && this.options.pool.min - ? this.options.pool.min - : 1, - max: - this.options.pool && this.options.pool.max - ? this.options.pool.max - : 10, + const poolOptions: any = { + maxConnectedOrPooled: + this.options.pool?.maxConnectedOrPooled ?? + this.options.pool?.max ?? + this.options.poolSize ?? + 10, + maxPooledIdleTime: + this.options.pool?.maxPooledIdleTime ?? + (this.options.pool?.idleTimeout + ? this.options.pool.idleTimeout / 1000 + : 30), + } + if (this.options.pool?.pingCheck) { + poolOptions.pingCheck = this.options.pool.pingCheck + } + if (this.options.pool?.poolCapacity) { + poolOptions.poolCapacity = this.options.pool.poolCapacity } - if (this.options.pool && this.options.pool.checkInterval) - options.checkInterval = this.options.pool.checkInterval - if (this.options.pool && this.options.pool.maxWaitingRequests) - options.maxWaitingRequests = this.options.pool.maxWaitingRequests - if (this.options.pool && this.options.pool.requestTimeout) - options.requestTimeout = this.options.pool.requestTimeout - if (this.options.pool && this.options.pool.idleTimeout) - options.idleTimeout = this.options.pool.idleTimeout - - const { logger } = this.connection - - const poolErrorHandler = - options.poolErrorHandler || - ((error: any) => - logger.log("warn", `SAP Hana pool raised an error. ${error}`)) - this.client.eventEmitter.on("poolError", poolErrorHandler) + this.poolErrorHandler = + this.options.pool?.poolErrorHandler ?? + ((error: Error) => { + this.connection.logger.log( + "warn", + `SAP HANA pool raised an error: ${error}`, + ) + }) // create the pool - this.master = this.client.createPool(dbParams, options) + try { + this.master = this.client.createPool(connectionOptions, poolOptions) + } catch (error) { + this.poolErrorHandler(error) + throw error + } const queryRunner = this.createQueryRunner("master") @@ -338,7 +346,40 @@ export class SapDriver implements Driver { } this.master = undefined - await pool.clear() + try { + await promisify(pool.clear).call(pool) + } catch (error) { + this.poolErrorHandler(error) + throw error + } + } + + /** + * Obtains a new database connection to a master server. + * Used for replication. + * If replication is not setup then returns default connection's database connection. + */ + async obtainMasterConnection(): Promise { + const pool = this.master + if (!pool) { + throw new TypeORMError("Driver not Connected") + } + + try { + return await promisify(pool.getConnection).call(pool) + } catch (error) { + this.poolErrorHandler(error) + throw error + } + } + + /** + * Obtains a new database connection to a slave server. + * Used for replication. + * If replication is not setup then returns master (default) connection's database connection. + */ + async obtainSlaveConnection(): Promise { + return this.obtainMasterConnection() } /** @@ -724,28 +765,6 @@ export class SapDriver implements Driver { return type } - /** - * Obtains a new database connection to a master server. - * Used for replication. - * If replication is not setup then returns default connection's database connection. - */ - obtainMasterConnection(): Promise { - if (!this.master) { - throw new TypeORMError("Driver not Connected") - } - - return this.master.getConnection() - } - - /** - * Obtains a new database connection to a slave server. - * Used for replication. - * If replication is not setup then returns master (default) connection's database connection. - */ - obtainSlaveConnection(): Promise { - return this.obtainMasterConnection() - } - /** * Creates generated map of values generated or returned by database after INSERT query. */ @@ -851,22 +870,19 @@ export class SapDriver implements Driver { * If driver dependency is not given explicitly, then try to load it via "require". */ protected loadDependencies(): void { - try { - const client = this.options.driver || PlatformTools.load("hdb-pool") + const client = this.options.driver ?? this.options.hanaClientDriver + if (client) { this.client = client - } catch (e) { - // todo: better error for browser env - throw new DriverPackageNotInstalledError("SAP Hana", "hdb-pool") + + return } try { - if (!this.options.hanaClientDriver) { - PlatformTools.load("@sap/hana-client") - this.streamClient = PlatformTools.load( - "@sap/hana-client/extension/Stream", - ) - } - } catch (e) { + this.client = PlatformTools.load("@sap/hana-client") + this.streamClient = PlatformTools.load( + "@sap/hana-client/extension/Stream", + ) + } catch { // todo: better error for browser env throw new DriverPackageNotInstalledError( "SAP Hana", diff --git a/src/driver/sap/SapQueryRunner.ts b/src/driver/sap/SapQueryRunner.ts index e1e668298..420b88f77 100644 --- a/src/driver/sap/SapQueryRunner.ts +++ b/src/driver/sap/SapQueryRunner.ts @@ -1,4 +1,4 @@ -import { promisify } from "util" +import { promisify } from "node:util" import { ObjectLiteral } from "../../common/ObjectLiteral" import { QueryFailedError, TypeORMError } from "../../error" import { QueryRunnerAlreadyReleasedError } from "../../error/QueryRunnerAlreadyReleasedError" @@ -85,14 +85,20 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner { * Releases used database connection. * You cannot use query runner methods once its released. */ - release(): Promise { + async release(): Promise { this.isReleased = true if (this.databaseConnection) { - return this.driver.master.release(this.databaseConnection) + // return the connection back to the pool + try { + await promisify(this.databaseConnection.disconnect).call( + this.databaseConnection, + ) + } catch (error) { + this.driver.poolErrorHandler(error) + throw error + } } - - return Promise.resolve() } /** @@ -168,14 +174,12 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner { */ async setAutoCommit(options: { status: "on" | "off" }) { const connection = await this.connect() - - const execute = promisify(connection.exec.bind(connection)) - connection.setAutoCommit(options.status === "on") - const query = `SET TRANSACTION AUTOCOMMIT DDL ${options.status.toUpperCase()};` + const query = `SET TRANSACTION AUTOCOMMIT DDL ${options.status.toUpperCase()}` + this.driver.connection.logger.logQuery(query, [], this) try { - await execute(query) + await promisify(connection.exec).call(connection, query) } catch (error) { throw new QueryFailedError(query, [], error) } @@ -270,26 +274,20 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner { if (isInsertQuery) { const lastIdQuery = `SELECT CURRENT_IDENTITY_VALUE() FROM "SYS"."DUMMY"` this.driver.connection.logger.logQuery(lastIdQuery, [], this) - const identityValueResult = await new Promise( - (ok, fail) => { - databaseConnection.exec( - lastIdQuery, - (err: any, raw: any) => - err - ? fail( - new QueryFailedError( - lastIdQuery, - [], - err, - ), - ) - : ok(raw), - ) - }, - ) + try { + const identityValueResult: [ + { "CURRENT_IDENTITY_VALUE()": unknown }, + ] = await promisify(databaseConnection.exec).call( + databaseConnection, + lastIdQuery, + ) - result.raw = identityValueResult[0]["CURRENT_IDENTITY_VALUE()"] - result.records = identityValueResult + result.raw = + identityValueResult[0]["CURRENT_IDENTITY_VALUE()"] + result.records = identityValueResult + } catch (error) { + throw new QueryFailedError(lastIdQuery, [], error) + } } } catch (err) { this.driver.connection.logger.logQueryError( @@ -311,7 +309,7 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner { } finally { // Never forget to drop the statement we reserved if (statement?.drop) { - await new Promise((ok) => statement.drop(() => ok())) + await promisify(statement.drop).call(statement) } await broadcasterResult.wait() @@ -343,11 +341,15 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner { let resultSet: any const cleanup = async () => { - if (resultSet) { - await promisify(resultSet.close).call(resultSet) + const originalStatement = statement + const originalResultSet = resultSet + statement = null + resultSet = null + if (originalResultSet) { + await promisify(originalResultSet.close).call(originalResultSet) } - if (statement) { - await promisify(statement.drop).call(statement) + if (originalStatement) { + await promisify(originalStatement.drop).call(originalStatement) } release() } @@ -367,20 +369,20 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner { const stream = this.driver.streamClient.createObjectStream(resultSet) - stream.on("end", async () => { - await cleanup() - onEnd?.() - }) - stream.on("error", async (error: Error) => { + + if (onEnd) { + stream.on("end", onEnd) + } + stream.on("error", (error: Error) => { this.driver.connection.logger.logQueryError( error, query, parameters, this, ) - await cleanup() onError?.(error) }) + stream.on("close", cleanup) return stream } catch (error) { diff --git a/src/platform/PlatformTools.ts b/src/platform/PlatformTools.ts index 1ecb7fb36..72aaf9a2e 100644 --- a/src/platform/PlatformTools.ts +++ b/src/platform/PlatformTools.ts @@ -60,9 +60,6 @@ export class PlatformTools { case "@sap/hana-client/extension/Stream": return require("@sap/hana-client/extension/Stream") - case "hdb-pool": - return require("hdb-pool") - /** * mysql */ diff --git a/test/functional/driver/sap/connection-pool.ts b/test/functional/driver/sap/connection-pool.ts new file mode 100644 index 000000000..90527616a --- /dev/null +++ b/test/functional/driver/sap/connection-pool.ts @@ -0,0 +1,72 @@ +import "reflect-metadata" +import { + closeTestingConnections, + createTestingConnections, + reloadTestingDatabases, +} from "../../../utils/test-utils" +import { DataSource } from "../../../../src/data-source/DataSource" +import { expect } from "chai" +import { SapDriver } from "../../../../src/driver/sap/SapDriver" +import { QueryRunner } from "../../../../src" +import { ConnectionPool } from "@sap/hana-client" + +describe("driver > sap > connection pool", () => { + let dataSources: DataSource[] + before( + async () => + (dataSources = await createTestingConnections({ + enabledDrivers: ["sap"], + driverSpecific: { + pool: { + maxConnectedOrPooled: 3, + }, + }, + })), + ) + beforeEach(() => reloadTestingDatabases(dataSources)) + after(() => closeTestingConnections(dataSources)) + + it("should be managed correctly", () => + Promise.all( + dataSources.map(async (dataSource) => { + const poolClient = (dataSource.driver as SapDriver) + .master as ConnectionPool + expect(poolClient.getInUseCount()).to.equal(0) + expect(poolClient.getPooledCount()).to.be.at.most(3) + + const queryRunners: QueryRunner[] = [] + for (let i = 0; i < 3; i++) { + const queryRunner = dataSource.createQueryRunner() + queryRunners.push(queryRunner) + + // the QueryRunner takes a connection from the pool once the first query is executed + await queryRunner.sql`SELECT * FROM SYS.DUMMY` + } + expect(poolClient.getInUseCount()).to.equal(3) + expect(poolClient.getPooledCount()).to.equal(0) + + const newQueryRunner = dataSource.createQueryRunner() + await expect(newQueryRunner.connect()).to.be.rejectedWith( + "Unable to create connection, the maxConnectedOrPool limit has been reached", + ) + await newQueryRunner.release() + + const oldQueryRunner = queryRunners.pop()! + await oldQueryRunner.release() + expect(poolClient.getInUseCount()).to.equal(2) + expect(poolClient.getPooledCount()).to.equal(1) + + const queryRunner = dataSource.createQueryRunner() + queryRunners.push(queryRunner) + await expect(queryRunner.connect()).to.be.fulfilled + expect(poolClient.getInUseCount()).to.equal(3) + expect(poolClient.getPooledCount()).to.equal(0) + + for (const queryRunner of queryRunners) { + await queryRunner.release() + } + expect(poolClient.getInUseCount()).to.equal(0) + expect(poolClient.getPooledCount()).to.equal(3) + }), + )) +})