@spinajs/orm-mysql 2.0.481 → 2.0.482
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.
- package/README.md +96 -4
- package/lib/cjs/index.d.ts +36 -7
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +228 -133
- package/lib/cjs/index.js.map +1 -1
- package/lib/mjs/index.d.ts +36 -7
- package/lib/mjs/index.d.ts.map +1 -1
- package/lib/mjs/index.js +230 -135
- package/lib/mjs/index.js.map +1 -1
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- package/package.json +17 -16
package/README.md
CHANGED
|
@@ -1,11 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@spinajs/orm-mysql`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The MySQL / MariaDB dialect driver for [`@spinajs/orm`](../orm), built on
|
|
4
|
+
[`@spinajs/orm-sql`](../orm-sql). Ships two drivers: `MySqlOrmDriver`, and `MySqlSSHOrmDriver`
|
|
5
|
+
which tunnels the connection over SSH.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @spinajs/orm @spinajs/orm-mysql
|
|
11
|
+
```
|
|
4
12
|
|
|
5
13
|
## Usage
|
|
6
14
|
|
|
15
|
+
```ts
|
|
16
|
+
import { DI } from '@spinajs/di';
|
|
17
|
+
import { Orm } from '@spinajs/orm';
|
|
18
|
+
import { MySqlOrmDriver } from '@spinajs/orm-mysql';
|
|
19
|
+
|
|
20
|
+
DI.register(MySqlOrmDriver).as('orm-driver-mysql');
|
|
21
|
+
await DI.resolve(Orm);
|
|
7
22
|
```
|
|
8
|
-
const ormMysql = require('orm-mysql');
|
|
9
23
|
|
|
10
|
-
|
|
24
|
+
```ts
|
|
25
|
+
// configuration
|
|
26
|
+
{
|
|
27
|
+
db: {
|
|
28
|
+
DefaultConnection: 'mysql',
|
|
29
|
+
Connections: [
|
|
30
|
+
{
|
|
31
|
+
Name: 'mysql',
|
|
32
|
+
Driver: 'orm-driver-mysql',
|
|
33
|
+
Host: '127.0.0.1',
|
|
34
|
+
Port: 3306,
|
|
35
|
+
User: 'app',
|
|
36
|
+
Password: 'secret',
|
|
37
|
+
Database: 'app',
|
|
38
|
+
Encoding: 'utf8mb4',
|
|
39
|
+
Pool: { Min: 2, Max: 20 },
|
|
40
|
+
Migration: { OnStartup: true },
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
11
45
|
```
|
|
46
|
+
|
|
47
|
+
## What is distinctive
|
|
48
|
+
|
|
49
|
+
- **`insertIdIsFirstOfBatch: true`** — the only driver where it holds. For a *simple* insert
|
|
50
|
+
(`INSERT ... VALUES (...), (...)`, the only shape the builder produces) InnoDB reserves one
|
|
51
|
+
contiguous block of auto-increment values and `LAST_INSERT_ID()` reports the first, so a
|
|
52
|
+
multi-row insert's keys can be read as `LAST_INSERT_ID() + index`. The ORM applies six guards
|
|
53
|
+
before relying on it.
|
|
54
|
+
- **No `RETURNING`** — `InsertQueryBuilder.returning()` throws `NotSupported` rather than
|
|
55
|
+
silently doing nothing.
|
|
56
|
+
- **Database events are supported**, and **all four isolation levels** are honoured.
|
|
57
|
+
- **Retries are suppressed inside a transaction** — replaying a statement after reconnecting
|
|
58
|
+
would apply it outside the transaction.
|
|
59
|
+
- **DDL is not transactional.** `Migration.Transaction.Mode = PerMigration` cannot roll back a
|
|
60
|
+
`CREATE TABLE`.
|
|
61
|
+
|
|
62
|
+
Only two compiler overrides are needed (`TableExistsCompiler`, `ServerResponseMapper`), which is
|
|
63
|
+
a fair measure of how closely the generic SQL layer tracks MySQL.
|
|
64
|
+
|
|
65
|
+
## SSH tunnelling
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
DI.register(MySqlSSHOrmDriver).as('orm-driver-mysql-ssh');
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
{
|
|
73
|
+
Name: 'remote',
|
|
74
|
+
Driver: 'orm-driver-mysql-ssh',
|
|
75
|
+
Host: '10.0.0.5', Port: 3306,
|
|
76
|
+
User: 'app', Password: 'secret', Database: 'app',
|
|
77
|
+
SSH: { Host: 'bastion.example.com', Port: 22, User: 'deploy', PrivateKey: '/home/deploy/.ssh/id_rsa' },
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The forward uses local port `12345`, so a second tunnelled connection in the same process will
|
|
82
|
+
collide, and the private key must be unencrypted.
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
Full documentation lives in **[docs/](docs/)**.
|
|
87
|
+
|
|
88
|
+
| | Page |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| 01 | [Configuration](docs/01-configuration.md) |
|
|
91
|
+
| 02 | [Dialect notes](docs/02-dialect-notes.md) |
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm test # unit suite, no server needed
|
|
97
|
+
|
|
98
|
+
docker compose --profile test up -d mysql # from the repo root
|
|
99
|
+
npm run test:integration
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The container publishes MySQL on host port **3900**, deliberately not 3306, so it cannot collide
|
|
103
|
+
with a locally installed MySQL. See the [repository README](../../README.md).
|
package/lib/cjs/index.d.ts
CHANGED
|
@@ -1,31 +1,60 @@
|
|
|
1
|
-
import { QueryContext, OrmDriver, IColumnDescriptor,
|
|
1
|
+
import { QueryContext, OrmDriver, IColumnDescriptor, ServerResponseMapper, ISupportedFeature, IsolationLevel, ITransactionContext, ITransactionOptions, IPoolMetrics } from '@spinajs/orm';
|
|
2
2
|
import { SqlDriver } from '@spinajs/orm-sql';
|
|
3
3
|
import * as mysql from 'mysql2';
|
|
4
4
|
import { PoolConnection } from 'mysql2';
|
|
5
5
|
import { Client as SSHClient } from 'ssh2';
|
|
6
|
-
|
|
7
|
-
export interface IMySqlTransactionContext {
|
|
6
|
+
export interface IMySqlTransactionContext extends ITransactionContext {
|
|
8
7
|
connection: PoolConnection;
|
|
9
8
|
}
|
|
10
9
|
export declare class MysqlServerResponseMapper extends ServerResponseMapper {
|
|
11
10
|
read(data: any): {
|
|
12
11
|
LastInsertId: any;
|
|
13
12
|
RowsAffected: any;
|
|
13
|
+
Returning: any[];
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
export declare class MySqlOrmDriver extends SqlDriver {
|
|
17
17
|
protected Pool: mysql.Pool;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
/**
|
|
19
|
+
* MySQL/InnoDB honours all four standard levels.
|
|
20
|
+
*/
|
|
21
|
+
readonly SupportedIsolationLevels: IsolationLevel[];
|
|
21
22
|
executeOnDb(stmt: string, params: any[], context: QueryContext): Promise<any>;
|
|
23
|
+
/**
|
|
24
|
+
* True when the error means the transport died rather than the statement being wrong.
|
|
25
|
+
*/
|
|
26
|
+
protected isRetryableError(err: unknown): boolean;
|
|
27
|
+
protected _executeOnDbOnce(stmt: string, params: any[], context: QueryContext): Promise<any>;
|
|
22
28
|
supportedFeatures(): ISupportedFeature;
|
|
23
29
|
resolve(): void;
|
|
30
|
+
/**
|
|
31
|
+
* mysql2 keeps its pool bookkeeping on the internal `_allConnections` / `_freeConnections` /
|
|
32
|
+
* `_connectionQueue` lists. They are not public API, so every read is guarded — a mysql2
|
|
33
|
+
* upgrade that renames them degrades to zeros rather than crashing the health check.
|
|
34
|
+
*/
|
|
35
|
+
poolMetrics(): IPoolMetrics;
|
|
24
36
|
ping(): Promise<boolean>;
|
|
25
37
|
connect(): Promise<OrmDriver>;
|
|
26
38
|
disconnect(): Promise<OrmDriver>;
|
|
27
39
|
tableInfo(name: string, schema?: string): Promise<IColumnDescriptor[]>;
|
|
28
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Pulls the pooled connection out of a transaction context. The base class only ever hands
|
|
42
|
+
* us contexts this driver's own `_begin` produced, so the cast is safe.
|
|
43
|
+
*/
|
|
44
|
+
private txConnection;
|
|
45
|
+
/**
|
|
46
|
+
* Runs a statement on the transaction's own connection, bypassing the ambient-context lookup
|
|
47
|
+
* in `executeOnDb`. Transaction control statements must never land on a pooled connection
|
|
48
|
+
* other than their own.
|
|
49
|
+
*/
|
|
50
|
+
private runOnConnection;
|
|
51
|
+
protected _begin(options?: ITransactionOptions): Promise<ITransactionContext>;
|
|
52
|
+
protected _commit(ctx: ITransactionContext): Promise<void>;
|
|
53
|
+
protected _rollback(ctx: ITransactionContext): Promise<void>;
|
|
54
|
+
protected _savepoint(ctx: ITransactionContext, name: string): Promise<void>;
|
|
55
|
+
protected _releaseSavepoint(ctx: ITransactionContext, name: string): Promise<void>;
|
|
56
|
+
protected _rollbackToSavepoint(ctx: ITransactionContext, name: string): Promise<void>;
|
|
57
|
+
protected _dispose(ctx: ITransactionContext): Promise<void>;
|
|
29
58
|
}
|
|
30
59
|
export declare class MySqlSSHOrmDriver extends MySqlOrmDriver {
|
|
31
60
|
protected SshClient: SSHClient;
|
package/lib/cjs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAqC,oBAAoB,EAAE,iBAAiB,EAAE,cAAc,EAAE,mBAAmB,EAAE,mBAAmB,EAAmB,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/O,OAAO,EAAoB,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,KAAK,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAY,cAAc,EAAe,MAAM,QAAQ,CAAC;AAG/D,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAG3C,MAAM,WAAW,wBAAyB,SAAQ,mBAAmB;IACnE,UAAU,EAAE,cAAc,CAAC;CAC5B;AAED,qBAAa,yBAA0B,SAAQ,oBAAoB;IAC1D,IAAI,CAAC,IAAI,EAAE,GAAG;;;mBAKA,GAAG,EAAE;;CAG3B;AAED,qBAEa,cAAe,SAAQ,SAAS;IAC3C,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC;IAK3B;;OAEG;IACH,SAAgB,wBAAwB,EAAE,cAAc,EAAE,CAA6E;IAEhI,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC;IAMpF;;OAEG;IACH,SAAS,CAAC,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO;IA0BjD,SAAS,CAAC,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC;IA6FrF,iBAAiB,IAAI,iBAAiB;IAQtC,OAAO;IAOd;;;;OAIG;IACI,WAAW,IAAI,YAAY;IAarB,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IAW9B,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC;IAmD7B,UAAU,IAAI,OAAO,CAAC,SAAS,CAAC;IAqB1B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IA8DnF;;;OAGG;IACH,OAAO,CAAC,YAAY;IAIpB;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAMvB,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAkC7E,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAM1D,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5D,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3E,SAAS,CAAC,iBAAiB,CAAC,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlF,SAAS,CAAC,oBAAoB,CAAC,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;cAIrE,QAAQ,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlE;AAED,qBAEa,iBAAkB,SAAQ,cAAc;IACnD,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC;IAExB,OAAO;IAYD,UAAU;IAUhB,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC;CA4CrC"}
|
package/lib/cjs/index.js
CHANGED
|
@@ -45,106 +45,175 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
45
45
|
exports.MySqlSSHOrmDriver = exports.MySqlOrmDriver = exports.MysqlServerResponseMapper = void 0;
|
|
46
46
|
/* eslint-disable promise/no-promise-in-callback */
|
|
47
47
|
const di_1 = require("@spinajs/di");
|
|
48
|
-
|
|
48
|
+
// No LogLevel import: master moved per-query timing out of every driver and into a single
|
|
49
|
+
// `Perf.measure('orm.query', ...)` around SqlDriver.execute, so duplicating it here would
|
|
50
|
+
// emit the same query twice. QueryBuilder / TransactionCallback / ITransaction are gone with
|
|
51
|
+
// the old `{ commit, rollback }` transaction shape this branch replaced. ConnectionState /
|
|
52
|
+
// IPoolMetrics are orm-infra's connection-resilience + pool-telemetry work.
|
|
49
53
|
const orm_1 = require("@spinajs/orm");
|
|
50
54
|
const orm_sql_1 = require("@spinajs/orm-sql");
|
|
51
55
|
const mysql = __importStar(require("mysql2"));
|
|
52
56
|
const compilers_js_1 = require("./compilers.js");
|
|
53
57
|
const ssh2_1 = require("ssh2");
|
|
54
58
|
const fs_1 = __importDefault(require("fs"));
|
|
55
|
-
const async_hooks_1 = require("async_hooks");
|
|
56
59
|
class MysqlServerResponseMapper extends orm_1.ServerResponseMapper {
|
|
57
60
|
read(data) {
|
|
58
|
-
|
|
61
|
+
// MySQL has no RETURNING; the identity value is all it reports.
|
|
62
|
+
return {
|
|
63
|
+
LastInsertId: data?.LastInsertId ?? 0,
|
|
64
|
+
RowsAffected: data?.RowsAffected ?? 0,
|
|
65
|
+
Returning: [],
|
|
66
|
+
};
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
exports.MysqlServerResponseMapper = MysqlServerResponseMapper;
|
|
62
70
|
let MySqlOrmDriver = class MySqlOrmDriver extends orm_sql_1.SqlDriver {
|
|
63
71
|
constructor() {
|
|
64
72
|
super(...arguments);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
// `_executionId` went with the per-driver query timing master centralised.
|
|
74
|
+
// `TransactionStorage` is no longer declared here either — it moved up to OrmDriver so
|
|
75
|
+
// ambient-connection propagation is part of the contract rather than a MySQL detail.
|
|
76
|
+
/**
|
|
77
|
+
* MySQL/InnoDB honours all four standard levels.
|
|
78
|
+
*/
|
|
79
|
+
this.SupportedIsolationLevels = ['READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ', 'SERIALIZABLE'];
|
|
71
80
|
}
|
|
72
81
|
executeOnDb(stmt, params, context) {
|
|
82
|
+
// Reads and writes are both retried: `withReconnect` only re-runs on transport failures,
|
|
83
|
+
// where the statement provably never reached the server.
|
|
84
|
+
return this.withReconnect(() => this._executeOnDbOnce(stmt, params, context));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* True when the error means the transport died rather than the statement being wrong.
|
|
88
|
+
*/
|
|
89
|
+
isRetryableError(err) {
|
|
90
|
+
// Inside a transaction the connection carried uncommitted state. Reconnecting and replaying
|
|
91
|
+
// one statement would silently apply it OUTSIDE the transaction, so the error must surface.
|
|
92
|
+
if (this.TransactionStorage.getStore()) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (super.isRetryableError(err)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
// mysql2 marks connection-level failures fatal; a fatal error means the connection is gone
|
|
99
|
+
// regardless of which code came with it.
|
|
100
|
+
let current = err;
|
|
101
|
+
let depth = 0;
|
|
102
|
+
while (current && depth < 5) {
|
|
103
|
+
if (current.fatal === true) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
current = current.inner ?? current.cause;
|
|
107
|
+
depth++;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
_executeOnDbOnce(stmt, params, context) {
|
|
73
112
|
const self = this;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
113
|
+
// Check if we're inside a transaction context and use that connection.
|
|
114
|
+
// The context comes from the base driver, so `connection` is typed loosely there; only
|
|
115
|
+
// this driver's `_begin` ever populates it, and it always puts a PoolConnection in.
|
|
77
116
|
const txContext = this.TransactionStorage.getStore();
|
|
78
|
-
const queryable = txContext?.connection ?? this.Pool;
|
|
79
117
|
return new Promise((resolve, reject) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
118
|
+
const fail = (err) => reject(new orm_1.OrmException(`Error executing orm command `, {
|
|
119
|
+
Host: self.Options.Host,
|
|
120
|
+
User: self.Options.User,
|
|
121
|
+
Name: self.Options.Name,
|
|
122
|
+
}, stmt, params, err));
|
|
123
|
+
const run = (queryable, done) => {
|
|
124
|
+
try {
|
|
125
|
+
queryable.query(stmt, params, function (err, results) {
|
|
126
|
+
done();
|
|
127
|
+
if (err) {
|
|
128
|
+
return fail(err);
|
|
129
|
+
}
|
|
130
|
+
switch (context) {
|
|
131
|
+
case orm_1.QueryContext.Update:
|
|
132
|
+
case orm_1.QueryContext.Delete:
|
|
133
|
+
resolve({
|
|
134
|
+
RowsAffected: results.affectedRows,
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
case orm_1.QueryContext.Insert:
|
|
138
|
+
case orm_1.QueryContext.Upsert:
|
|
139
|
+
resolve({
|
|
140
|
+
RowsAffected: results.affectedRows,
|
|
141
|
+
LastInsertId: results.insertId,
|
|
142
|
+
Returning: [],
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
resolve(results);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
87
150
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
RowsAffected: results.affectedRows,
|
|
93
|
-
});
|
|
94
|
-
break;
|
|
95
|
-
case orm_1.QueryContext.Insert:
|
|
96
|
-
case orm_1.QueryContext.Upsert:
|
|
97
|
-
resolve({
|
|
98
|
-
RowsAffected: results.affectedRows,
|
|
99
|
-
LastInsertId: results.insertId,
|
|
100
|
-
});
|
|
101
|
-
break;
|
|
102
|
-
default:
|
|
103
|
-
resolve(results);
|
|
104
|
-
break;
|
|
151
|
+
catch (err) {
|
|
152
|
+
// A synchronous throw would otherwise strand the connection outside the pool.
|
|
153
|
+
done();
|
|
154
|
+
fail(err);
|
|
105
155
|
}
|
|
156
|
+
};
|
|
157
|
+
if (txContext?.connection) {
|
|
158
|
+
// A transaction owns its connection for its whole lifetime — releasing it after one
|
|
159
|
+
// statement would hand the rest of the transaction to a different connection.
|
|
160
|
+
run(txContext.connection, () => undefined);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Acquiring is the part that queues when the pool is saturated, so it is the part worth
|
|
164
|
+
// timing. Taking the connection explicitly, instead of letting `Pool.query` do it out of
|
|
165
|
+
// sight, is what makes `orm_pool_acquire_seconds` a real number instead of always zero.
|
|
166
|
+
const acquireStart = process.hrtime.bigint();
|
|
167
|
+
this.Pool.getConnection((err, connection) => {
|
|
168
|
+
const seconds = Number(process.hrtime.bigint() - acquireStart) / 1e9;
|
|
169
|
+
this.observeAcquireSeconds(seconds);
|
|
170
|
+
if (err) {
|
|
171
|
+
fail(err);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
let released = false;
|
|
175
|
+
run(connection, () => {
|
|
176
|
+
if (released) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
released = true;
|
|
180
|
+
connection.release();
|
|
181
|
+
});
|
|
106
182
|
});
|
|
107
|
-
})
|
|
108
|
-
.then((val) => {
|
|
109
|
-
const tDiff = this.Log.timeEnd(`query-${tName}`);
|
|
110
|
-
void this.Log.write({
|
|
111
|
-
Level: log_1.LogLevel.Trace,
|
|
112
|
-
Variables: {
|
|
113
|
-
error: undefined,
|
|
114
|
-
message: `Executed: ${stmt}, bindings: ${params ? params.join(',') : 'none'}`,
|
|
115
|
-
logger: this.Log.Name,
|
|
116
|
-
level: 'TRACE',
|
|
117
|
-
duration: tDiff,
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
return val;
|
|
121
|
-
})
|
|
122
|
-
.catch((err) => {
|
|
123
|
-
const tDiff = this.Log.timeEnd(`query-${tName}`);
|
|
124
|
-
void this.Log.write({
|
|
125
|
-
Level: log_1.LogLevel.Error,
|
|
126
|
-
Variables: {
|
|
127
|
-
error: err,
|
|
128
|
-
message: `Failed: ${stmt}, bindings: ${params ? params.join(',') : 'none'}`,
|
|
129
|
-
logger: this.Log.Name,
|
|
130
|
-
level: 'Error',
|
|
131
|
-
duration: tDiff,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
throw err;
|
|
135
183
|
});
|
|
136
184
|
}
|
|
137
185
|
supportedFeatures() {
|
|
138
|
-
|
|
186
|
+
// insertIdIsFirstOfBatch: a multi-row `INSERT ... VALUES` is a *simple insert* to InnoDB
|
|
187
|
+
// ( row count known before execution ), so it reserves one contiguous block of
|
|
188
|
+
// auto-increment values and LAST_INSERT_ID() reports the first of them. True even under
|
|
189
|
+
// innodb_autoinc_lock_mode = 2, the MySQL 8 default.
|
|
190
|
+
return { events: true, insertReturning: false, insertIdIsFirstOfBatch: true };
|
|
139
191
|
}
|
|
140
192
|
resolve() {
|
|
141
193
|
super.resolve();
|
|
142
194
|
this.Container.register(compilers_js_1.MySqlTableExistsCompiler).as(orm_1.TableExistsCompiler);
|
|
143
195
|
this.Container.register(MysqlServerResponseMapper).as(orm_1.ServerResponseMapper);
|
|
144
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* mysql2 keeps its pool bookkeeping on the internal `_allConnections` / `_freeConnections` /
|
|
199
|
+
* `_connectionQueue` lists. They are not public API, so every read is guarded — a mysql2
|
|
200
|
+
* upgrade that renames them degrades to zeros rather than crashing the health check.
|
|
201
|
+
*/
|
|
202
|
+
poolMetrics() {
|
|
203
|
+
const pool = this.Pool;
|
|
204
|
+
const size = pool?._allConnections?.length ?? 0;
|
|
205
|
+
const free = pool?._freeConnections?.length ?? 0;
|
|
206
|
+
return {
|
|
207
|
+
Size: size,
|
|
208
|
+
InUse: Math.max(size - free, 0),
|
|
209
|
+
Waiting: pool?._connectionQueue?.length ?? 0,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
145
212
|
async ping() {
|
|
146
213
|
try {
|
|
147
|
-
|
|
214
|
+
// deliberately bypasses `withReconnect` — a health probe that reconnects on its own
|
|
215
|
+
// would turn one dead connection into a reconnect storm on every tick.
|
|
216
|
+
await this._executeOnDbOnce('SELECT 1', [], orm_1.QueryContext.Select);
|
|
148
217
|
return true;
|
|
149
218
|
}
|
|
150
219
|
catch {
|
|
@@ -154,6 +223,7 @@ let MySqlOrmDriver = class MySqlOrmDriver extends orm_sql_1.SqlDriver {
|
|
|
154
223
|
connect() {
|
|
155
224
|
return new Promise((resolve, reject) => {
|
|
156
225
|
try {
|
|
226
|
+
const pool = this.resolvedPoolOptions();
|
|
157
227
|
this.Pool = mysql.createPool({
|
|
158
228
|
host: this.Options.Host,
|
|
159
229
|
user: this.Options.User,
|
|
@@ -161,7 +231,14 @@ let MySqlOrmDriver = class MySqlOrmDriver extends orm_sql_1.SqlDriver {
|
|
|
161
231
|
port: this.Options.Port,
|
|
162
232
|
database: this.Options.Database,
|
|
163
233
|
waitForConnections: true,
|
|
164
|
-
connectionLimit:
|
|
234
|
+
connectionLimit: pool.Max,
|
|
235
|
+
// mysql2's `maxIdle` is a CEILING on idle connections, not a floor, and it never
|
|
236
|
+
// pre-warms the pool — so there is no direct equivalent of `Pool.Min`. Passing Min
|
|
237
|
+
// straight through would set maxIdle to 0 by default and make mysql2 destroy every
|
|
238
|
+
// released connection, i.e. disable pooling. Instead: an explicit Min becomes the
|
|
239
|
+
// number of connections we let sit idle; Min = 0 keeps mysql2's own default (Max).
|
|
240
|
+
maxIdle: pool.Min > 0 ? pool.Min : pool.Max,
|
|
241
|
+
idleTimeout: pool.IdleTimeout,
|
|
165
242
|
queueLimit: 0,
|
|
166
243
|
});
|
|
167
244
|
// Test the pool connection
|
|
@@ -175,6 +252,7 @@ let MySqlOrmDriver = class MySqlOrmDriver extends orm_sql_1.SqlDriver {
|
|
|
175
252
|
}
|
|
176
253
|
// Release the test connection
|
|
177
254
|
connection.release();
|
|
255
|
+
this.setState(orm_1.ConnectionState.Connected);
|
|
178
256
|
resolve(this);
|
|
179
257
|
});
|
|
180
258
|
}
|
|
@@ -192,6 +270,8 @@ let MySqlOrmDriver = class MySqlOrmDriver extends orm_sql_1.SqlDriver {
|
|
|
192
270
|
});
|
|
193
271
|
}
|
|
194
272
|
disconnect() {
|
|
273
|
+
this.stopHealthCheck();
|
|
274
|
+
this.setState(orm_1.ConnectionState.Disconnected);
|
|
195
275
|
return new Promise((resolve, reject) => {
|
|
196
276
|
if (!this.Pool) {
|
|
197
277
|
resolve(this);
|
|
@@ -209,21 +289,31 @@ let MySqlOrmDriver = class MySqlOrmDriver extends orm_sql_1.SqlDriver {
|
|
|
209
289
|
});
|
|
210
290
|
}
|
|
211
291
|
async tableInfo(name, schema) {
|
|
212
|
-
const
|
|
213
|
-
|
|
292
|
+
const dbSchema = schema ?? this.Options.Database;
|
|
293
|
+
if (!dbSchema) {
|
|
294
|
+
throw new orm_1.OrmException(`Cannot read table info for '${name}': no schema/database configured for this connection ( pass a schema or set Options.Database )`);
|
|
295
|
+
}
|
|
296
|
+
// backtick-quote an identifier, escaping embedded backticks by doubling them
|
|
297
|
+
const escapeId = (id) => '`' + String(id).replace(/`/g, '``') + '`';
|
|
298
|
+
// ORDER BY ORDINAL_POSITION is not decoration. Without it MySQL is free to return the rows
|
|
299
|
+
// in any order — and does: the same table came back as (Code, TenantId) in one run and
|
|
300
|
+
// (TenantId, Code) in the next. Column order is part of what a table descriptor means, so
|
|
301
|
+
// it has to be the table's own order, not whatever the optimizer produced this time.
|
|
302
|
+
const tblInfo = (await this.executeOnDb(`SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME=? AND TABLE_SCHEMA=? ORDER BY ORDINAL_POSITION`, [name, dbSchema], orm_1.QueryContext.Select));
|
|
303
|
+
const isView = (await this.executeOnDb(`SHOW FULL TABLES FROM ${escapeId(dbSchema)} WHERE ${escapeId(`Tables_in_${dbSchema}`)}=?`, [name], orm_1.QueryContext.Select));
|
|
214
304
|
let indexInfo = [];
|
|
215
305
|
if (!isView || isView.length === 0) {
|
|
216
|
-
throw new orm_1.OrmException(`Table ${
|
|
306
|
+
throw new orm_1.OrmException(`Table ${dbSchema}.${name} does not exist`);
|
|
217
307
|
}
|
|
218
308
|
if (!tblInfo || !Array.isArray(tblInfo) || tblInfo.length === 0) {
|
|
219
|
-
this.Log.trace(`Table ${
|
|
309
|
+
this.Log.trace(`Table ${dbSchema}.${name} does not have any columns.`);
|
|
220
310
|
return null;
|
|
221
311
|
}
|
|
222
312
|
if (isView && isView[0].Table_type === 'VIEW') {
|
|
223
|
-
this.Log.trace(`Table ${
|
|
313
|
+
this.Log.trace(`Table ${dbSchema}.${name} is a VIEW and dont have indexes set.`);
|
|
224
314
|
}
|
|
225
315
|
else {
|
|
226
|
-
indexInfo = (await this.executeOnDb(`SHOW INDEXES FROM ${name}`, [], orm_1.QueryContext.Select));
|
|
316
|
+
indexInfo = (await this.executeOnDb(`SHOW INDEXES FROM ${escapeId(name)}`, [], orm_1.QueryContext.Select));
|
|
227
317
|
}
|
|
228
318
|
return tblInfo.map((r) => {
|
|
229
319
|
const isPrimary = indexInfo.find((c) => c.Key_name === 'PRIMARY' && c.Column_name === r.COLUMN_NAME) !== undefined;
|
|
@@ -251,75 +341,76 @@ let MySqlOrmDriver = class MySqlOrmDriver extends orm_sql_1.SqlDriver {
|
|
|
251
341
|
};
|
|
252
342
|
});
|
|
253
343
|
}
|
|
254
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Pulls the pooled connection out of a transaction context. The base class only ever hands
|
|
346
|
+
* us contexts this driver's own `_begin` produced, so the cast is safe.
|
|
347
|
+
*/
|
|
348
|
+
txConnection(ctx) {
|
|
349
|
+
return ctx.connection;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Runs a statement on the transaction's own connection, bypassing the ambient-context lookup
|
|
353
|
+
* in `executeOnDb`. Transaction control statements must never land on a pooled connection
|
|
354
|
+
* other than their own.
|
|
355
|
+
*/
|
|
356
|
+
runOnConnection(connection, stmt) {
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
connection.query(stmt, (err) => (err ? reject(err) : resolve()));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
_begin(options) {
|
|
255
362
|
return new Promise((resolve, reject) => {
|
|
256
|
-
const trx = {
|
|
257
|
-
commit: () => Promise.resolve(),
|
|
258
|
-
rollback: () => Promise.resolve(),
|
|
259
|
-
};
|
|
260
|
-
if (!queryOrCallback) {
|
|
261
|
-
resolve(trx);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
363
|
this.Pool.getConnection((err, connection) => {
|
|
265
364
|
if (err) {
|
|
266
365
|
reject(err);
|
|
267
366
|
return;
|
|
268
367
|
}
|
|
269
|
-
|
|
270
|
-
|
|
368
|
+
const begin = () => {
|
|
369
|
+
connection.beginTransaction((err) => {
|
|
370
|
+
if (err) {
|
|
371
|
+
connection.release();
|
|
372
|
+
reject(err);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
resolve({ connection, depth: 0 });
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
if (options?.isolation) {
|
|
379
|
+
// isolation levels are a fixed, validated enum — never caller-supplied free text
|
|
380
|
+
this.runOnConnection(connection, `SET TRANSACTION ISOLATION LEVEL ${options.isolation}`).then(begin, (err) => {
|
|
271
381
|
connection.release();
|
|
272
382
|
reject(err);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
await this.TransactionStorage.run({ connection }, async () => {
|
|
278
|
-
if (Array.isArray(queryOrCallback)) {
|
|
279
|
-
for (const q of queryOrCallback) {
|
|
280
|
-
await q;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
await queryOrCallback(this);
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
resolve({
|
|
288
|
-
commit: async () => {
|
|
289
|
-
return new Promise((res, rej) => {
|
|
290
|
-
connection.commit((err) => {
|
|
291
|
-
if (err) {
|
|
292
|
-
connection.rollback(() => {
|
|
293
|
-
connection.release();
|
|
294
|
-
rej(err);
|
|
295
|
-
});
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
connection.release();
|
|
299
|
-
res();
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
},
|
|
303
|
-
rollback: async () => {
|
|
304
|
-
return new Promise((res) => {
|
|
305
|
-
connection.rollback(() => {
|
|
306
|
-
connection.release();
|
|
307
|
-
res();
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
catch (ex) {
|
|
314
|
-
connection.rollback(() => {
|
|
315
|
-
connection.release();
|
|
316
|
-
reject(ex);
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
});
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
begin();
|
|
320
387
|
});
|
|
321
388
|
});
|
|
322
389
|
}
|
|
390
|
+
_commit(ctx) {
|
|
391
|
+
return new Promise((resolve, reject) => {
|
|
392
|
+
this.txConnection(ctx).commit((err) => (err ? reject(err) : resolve()));
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
_rollback(ctx) {
|
|
396
|
+
return new Promise((resolve, reject) => {
|
|
397
|
+
this.txConnection(ctx).rollback((err) => (err ? reject(err) : resolve()));
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
// savepoint names cannot be bound parameters, so they are inlined through the identifier
|
|
401
|
+
// escaper rather than passed as `?`
|
|
402
|
+
_savepoint(ctx, name) {
|
|
403
|
+
return this.runOnConnection(this.txConnection(ctx), `SAVEPOINT ${(0, orm_sql_1.escapeIdentifier)(name)}`);
|
|
404
|
+
}
|
|
405
|
+
_releaseSavepoint(ctx, name) {
|
|
406
|
+
return this.runOnConnection(this.txConnection(ctx), `RELEASE SAVEPOINT ${(0, orm_sql_1.escapeIdentifier)(name)}`);
|
|
407
|
+
}
|
|
408
|
+
_rollbackToSavepoint(ctx, name) {
|
|
409
|
+
return this.runOnConnection(this.txConnection(ctx), `ROLLBACK TO SAVEPOINT ${(0, orm_sql_1.escapeIdentifier)(name)}`);
|
|
410
|
+
}
|
|
411
|
+
async _dispose(ctx) {
|
|
412
|
+
this.txConnection(ctx).release();
|
|
413
|
+
}
|
|
323
414
|
};
|
|
324
415
|
exports.MySqlOrmDriver = MySqlOrmDriver;
|
|
325
416
|
exports.MySqlOrmDriver = MySqlOrmDriver = __decorate([
|
|
@@ -352,6 +443,7 @@ let MySqlSSHOrmDriver = class MySqlSSHOrmDriver extends MySqlOrmDriver {
|
|
|
352
443
|
reject(err);
|
|
353
444
|
return;
|
|
354
445
|
}
|
|
446
|
+
const pool = this.resolvedPoolOptions();
|
|
355
447
|
this.Pool = mysql.createPool({
|
|
356
448
|
host: 'localhost', // we tunnel via ssh so we use localhost
|
|
357
449
|
user: this.Options.User,
|
|
@@ -359,7 +451,10 @@ let MySqlSSHOrmDriver = class MySqlSSHOrmDriver extends MySqlOrmDriver {
|
|
|
359
451
|
port: this.Options.Port,
|
|
360
452
|
database: this.Options.Database,
|
|
361
453
|
waitForConnections: true,
|
|
362
|
-
connectionLimit:
|
|
454
|
+
connectionLimit: pool.Max,
|
|
455
|
+
// see MySqlOrmDriver.connect — `maxIdle` is a ceiling, so Min = 0 must not reach it
|
|
456
|
+
maxIdle: pool.Min > 0 ? pool.Min : pool.Max,
|
|
457
|
+
idleTimeout: pool.IdleTimeout,
|
|
363
458
|
queueLimit: 0,
|
|
364
459
|
stream: stream,
|
|
365
460
|
});
|