@oino-ts/db-mariadb 1.5.0 → 1.6.0
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/dist/cjs/OINODbMariadb.js +22 -16
- package/dist/esm/OINODbMariadb.js +22 -16
- package/package.json +3 -3
- package/src/OINODbMariadb.ts +559 -553
|
@@ -403,25 +403,27 @@ class OINODbMariadb extends db_1.OINODb {
|
|
|
403
403
|
return result;
|
|
404
404
|
}
|
|
405
405
|
_getSchemaSql() {
|
|
406
|
-
const sql = `SELECT
|
|
407
|
-
C.COLUMN_NAME,
|
|
408
|
-
C.COLUMN_TYPE,
|
|
409
|
-
C.IS_NULLABLE,
|
|
410
|
-
C.COLUMN_KEY,
|
|
411
|
-
C.COLUMN_DEFAULT,
|
|
412
|
-
C.EXTRA,
|
|
413
|
-
KCU.CONSTRAINT_NAME AS ForeignKeyName
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
406
|
+
const sql = `SELECT
|
|
407
|
+
C.COLUMN_NAME,
|
|
408
|
+
C.COLUMN_TYPE,
|
|
409
|
+
C.IS_NULLABLE,
|
|
410
|
+
C.COLUMN_KEY,
|
|
411
|
+
C.COLUMN_DEFAULT,
|
|
412
|
+
C.EXTRA,
|
|
413
|
+
KCU.CONSTRAINT_NAME AS ForeignKeyName,
|
|
414
|
+
KCU.REFERENCED_TABLE_NAME,
|
|
415
|
+
KCU.REFERENCED_COLUMN_NAME
|
|
416
|
+
FROM information_schema.COLUMNS C
|
|
417
|
+
LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
|
|
418
|
+
WHERE C.TABLE_SCHEMA = ? AND C.TABLE_NAME = ?
|
|
417
419
|
ORDER BY C.ORDINAL_POSITION;`;
|
|
418
420
|
return sql;
|
|
419
421
|
}
|
|
420
422
|
_getValidateSql() {
|
|
421
|
-
const sql = `SELECT
|
|
422
|
-
Count(C.COLUMN_NAME) AS COLUMN_COUNT
|
|
423
|
-
FROM information_schema.COLUMNS C
|
|
424
|
-
LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
|
|
423
|
+
const sql = `SELECT
|
|
424
|
+
Count(C.COLUMN_NAME) AS COLUMN_COUNT
|
|
425
|
+
FROM information_schema.COLUMNS C
|
|
426
|
+
LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
|
|
425
427
|
WHERE C.TABLE_SCHEMA = ?;`;
|
|
426
428
|
return sql;
|
|
427
429
|
}
|
|
@@ -442,9 +444,13 @@ WHERE C.TABLE_SCHEMA = ?;`;
|
|
|
442
444
|
const field_length1 = this._parseFieldLength(field_matches[3] || "0");
|
|
443
445
|
const field_length2 = this._parseFieldLength(field_matches[4] || "0");
|
|
444
446
|
const extra = row[5]?.toString() || "";
|
|
447
|
+
const fk_table = row[7]?.toString() || "";
|
|
448
|
+
const fk_column = row[8]?.toString() || "";
|
|
449
|
+
const foreign_key = fk_table ? { table: fk_table, column: fk_column } : null;
|
|
445
450
|
const field_params = {
|
|
446
451
|
isPrimaryKey: row[3] == "PRI",
|
|
447
|
-
isForeignKey: row[6] != null,
|
|
452
|
+
isForeignKey: (row[6] != null) || (foreign_key != null),
|
|
453
|
+
foreignKey: foreign_key,
|
|
448
454
|
isAutoInc: extra.indexOf('auto_increment') >= 0,
|
|
449
455
|
isNotNull: row[2] == "NO"
|
|
450
456
|
};
|
|
@@ -400,25 +400,27 @@ export class OINODbMariadb extends OINODb {
|
|
|
400
400
|
return result;
|
|
401
401
|
}
|
|
402
402
|
_getSchemaSql() {
|
|
403
|
-
const sql = `SELECT
|
|
404
|
-
C.COLUMN_NAME,
|
|
405
|
-
C.COLUMN_TYPE,
|
|
406
|
-
C.IS_NULLABLE,
|
|
407
|
-
C.COLUMN_KEY,
|
|
408
|
-
C.COLUMN_DEFAULT,
|
|
409
|
-
C.EXTRA,
|
|
410
|
-
KCU.CONSTRAINT_NAME AS ForeignKeyName
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
403
|
+
const sql = `SELECT
|
|
404
|
+
C.COLUMN_NAME,
|
|
405
|
+
C.COLUMN_TYPE,
|
|
406
|
+
C.IS_NULLABLE,
|
|
407
|
+
C.COLUMN_KEY,
|
|
408
|
+
C.COLUMN_DEFAULT,
|
|
409
|
+
C.EXTRA,
|
|
410
|
+
KCU.CONSTRAINT_NAME AS ForeignKeyName,
|
|
411
|
+
KCU.REFERENCED_TABLE_NAME,
|
|
412
|
+
KCU.REFERENCED_COLUMN_NAME
|
|
413
|
+
FROM information_schema.COLUMNS C
|
|
414
|
+
LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
|
|
415
|
+
WHERE C.TABLE_SCHEMA = ? AND C.TABLE_NAME = ?
|
|
414
416
|
ORDER BY C.ORDINAL_POSITION;`;
|
|
415
417
|
return sql;
|
|
416
418
|
}
|
|
417
419
|
_getValidateSql() {
|
|
418
|
-
const sql = `SELECT
|
|
419
|
-
Count(C.COLUMN_NAME) AS COLUMN_COUNT
|
|
420
|
-
FROM information_schema.COLUMNS C
|
|
421
|
-
LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
|
|
420
|
+
const sql = `SELECT
|
|
421
|
+
Count(C.COLUMN_NAME) AS COLUMN_COUNT
|
|
422
|
+
FROM information_schema.COLUMNS C
|
|
423
|
+
LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
|
|
422
424
|
WHERE C.TABLE_SCHEMA = ?;`;
|
|
423
425
|
return sql;
|
|
424
426
|
}
|
|
@@ -439,9 +441,13 @@ WHERE C.TABLE_SCHEMA = ?;`;
|
|
|
439
441
|
const field_length1 = this._parseFieldLength(field_matches[3] || "0");
|
|
440
442
|
const field_length2 = this._parseFieldLength(field_matches[4] || "0");
|
|
441
443
|
const extra = row[5]?.toString() || "";
|
|
444
|
+
const fk_table = row[7]?.toString() || "";
|
|
445
|
+
const fk_column = row[8]?.toString() || "";
|
|
446
|
+
const foreign_key = fk_table ? { table: fk_table, column: fk_column } : null;
|
|
442
447
|
const field_params = {
|
|
443
448
|
isPrimaryKey: row[3] == "PRI",
|
|
444
|
-
isForeignKey: row[6] != null,
|
|
449
|
+
isForeignKey: (row[6] != null) || (foreign_key != null),
|
|
450
|
+
foreignKey: foreign_key,
|
|
445
451
|
isAutoInc: extra.indexOf('auto_increment') >= 0,
|
|
446
452
|
isNotNull: row[2] == "NO"
|
|
447
453
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oino-ts/db-mariadb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "OINO TS package for using Mariadb databases.",
|
|
5
5
|
"author": "Matias Kiviniemi (pragmatta)",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"module": "./dist/esm/index.js",
|
|
22
22
|
"types": "./dist/types/index.d.ts",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@oino-ts/common": "^1.
|
|
25
|
-
"@oino-ts/db": "^1.
|
|
24
|
+
"@oino-ts/common": "^1.6.0",
|
|
25
|
+
"@oino-ts/db": "^1.6.0",
|
|
26
26
|
"mariadb": "^3.2.3"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
package/src/OINODbMariadb.ts
CHANGED
|
@@ -1,553 +1,559 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { OINO_ERROR_PREFIX, OINOBenchmark, OINO_INFO_PREFIX, OINOLog, OINOResult } from "@oino-ts/common";
|
|
8
|
-
|
|
9
|
-
import { OINODataSet, OINOBooleanDataField, OINONumberDataField, OINOStringDataField, OINODataField, OINODataFieldSchema, OINODataFieldParams, OINODataRow, OINODataCell, OINODatetimeDataField, OINOBlobDataField, OINO_EMPTY_ROW, OINO_EMPTY_ROWS } from "@oino-ts/common";
|
|
10
|
-
|
|
11
|
-
import { OINODb, OINODbParams, OINODbSqlStatement } from "@oino-ts/db";
|
|
12
|
-
|
|
13
|
-
import mariadb from "mariadb";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Implmentation of OINODataSet for MariaDb.
|
|
17
|
-
*
|
|
18
|
-
*/
|
|
19
|
-
class OINOMariadbData extends OINODataSet {
|
|
20
|
-
private _rows:OINODataRow[] = OINO_EMPTY_ROWS
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* OINOMariadbData constructor
|
|
24
|
-
* @param params database parameters
|
|
25
|
-
*/
|
|
26
|
-
constructor(data: any, messages:string[]=[]) {
|
|
27
|
-
super(data, messages)
|
|
28
|
-
|
|
29
|
-
if (data == null) {
|
|
30
|
-
this.messages.push(OINO_INFO_PREFIX + "SQL result is empty")
|
|
31
|
-
|
|
32
|
-
} else if (Array.isArray(data)) {
|
|
33
|
-
this._rows = data as OINODataRow[]
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
if (this.isEmpty()) {
|
|
37
|
-
this._currentRow = -1
|
|
38
|
-
this._eof = true
|
|
39
|
-
} else {
|
|
40
|
-
this._currentRow = 0
|
|
41
|
-
this._eof = false
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
private _currentRow: number
|
|
45
|
-
private _eof: boolean
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Is data set empty.
|
|
49
|
-
*
|
|
50
|
-
*/
|
|
51
|
-
isEmpty():boolean {
|
|
52
|
-
return (this._rows.length == 0)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Is there no more content, i.e. either dataset is empty or we have moved beyond last line
|
|
57
|
-
*
|
|
58
|
-
*/
|
|
59
|
-
isEof():boolean {
|
|
60
|
-
return (this._eof)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Attempts to moves dataset to the next row, possibly waiting for more data to become available. Returns !isEof().
|
|
65
|
-
*
|
|
66
|
-
*/
|
|
67
|
-
async next():Promise<boolean> {
|
|
68
|
-
if (this._currentRow < this._rows.length-1) {
|
|
69
|
-
this._currentRow = this._currentRow + 1
|
|
70
|
-
} else {
|
|
71
|
-
this._eof = true
|
|
72
|
-
}
|
|
73
|
-
return Promise.resolve(!this._eof)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Gets current row of data.
|
|
78
|
-
*
|
|
79
|
-
*/
|
|
80
|
-
getRow(): OINODataRow {
|
|
81
|
-
if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
|
|
82
|
-
return this._rows[this._currentRow]
|
|
83
|
-
} else {
|
|
84
|
-
return OINO_EMPTY_ROW
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Gets all rows of data.
|
|
90
|
-
*
|
|
91
|
-
*/
|
|
92
|
-
async getAllRows(): Promise<OINODataRow[]> {
|
|
93
|
-
return this._rows // at the moment theres no result streaming, so we can just return the rows
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Implementation of MariaDb/MySql-database.
|
|
99
|
-
*
|
|
100
|
-
*/
|
|
101
|
-
export class OINODbMariadb extends OINODb {
|
|
102
|
-
|
|
103
|
-
private static _fieldLengthRegex = /([^\(\)]+)(\s?\((\d+)\s?\,?\s?(\d*)?\))?/i
|
|
104
|
-
private static _connectionExceptionMessageRegex = /\(([^\)]*)\) (.*)/i
|
|
105
|
-
private static _sqlExceptionMessageRegex = /\(([^\)]*)\) (.*)\nsql\:(.*)?/i
|
|
106
|
-
|
|
107
|
-
private _pool:mariadb.Pool
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Constructor of `OINODbMariadb`
|
|
111
|
-
* @param params database parameters
|
|
112
|
-
*/
|
|
113
|
-
constructor(params:OINODbParams) {
|
|
114
|
-
super(params)
|
|
115
|
-
|
|
116
|
-
if (this.dbParams.type !== "OINODbMariadb") {
|
|
117
|
-
throw new Error(OINO_ERROR_PREFIX + ": Not OINODbMariadb-type: " + this.dbParams.type)
|
|
118
|
-
}
|
|
119
|
-
this._pool = mariadb.createPool({ host: this.dbParams.url, database: this.dbParams.database, port: this.dbParams.port, user: this.dbParams.user, password: this.dbParams.password, acquireTimeout: 2000, debug:false, rowsAsArray: true, multipleStatements: false }) // statements are now executed individually with bind parameters, so stacked/multi-statement execution is disabled
|
|
120
|
-
delete this.dbParams.password // do not store password in db object
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private _parseFieldLength(fieldLengthStr:string):number {
|
|
124
|
-
let result:number = parseInt(fieldLengthStr)
|
|
125
|
-
if (Number.isNaN(result)) {
|
|
126
|
-
result = 0
|
|
127
|
-
}
|
|
128
|
-
return result
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private async _query(sql:string, params?:OINODataCell[]):Promise<OINODataSet> {
|
|
132
|
-
let connection:mariadb.PoolConnection|null = null
|
|
133
|
-
let rows:OINODataRow[] = OINO_EMPTY_ROWS
|
|
134
|
-
try {
|
|
135
|
-
connection = await this._pool.getConnection()
|
|
136
|
-
const sql_res = (params && params.length > 0) ? await connection.query(sql, params) : await connection.query(sql)
|
|
137
|
-
// console.log("_query: sql=", sql, " result=", result)
|
|
138
|
-
if (Array.isArray(sql_res)) {
|
|
139
|
-
rows = sql_res.filter((r) => Array.isArray(r)) as OINODataRow[] // filter out OkPacket results from multiple statements
|
|
140
|
-
}
|
|
141
|
-
} catch (e:any) {
|
|
142
|
-
OINOLog.exception("@oino-ts/db-mariadb", "OINODbMariadb", "_query", "exception in SQL select", {message:e.message, stack:e.stack})
|
|
143
|
-
return new OINOMariadbData(OINO_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db query: " + e.message, "OINODbMariadb._query") as OINOMariadbData
|
|
144
|
-
|
|
145
|
-
} finally {
|
|
146
|
-
if (connection) {
|
|
147
|
-
connection.release()
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return new OINOMariadbData(rows, [])
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
private async _exec(sql:string, params?:OINODataCell[]):Promise<OINODataSet> {
|
|
154
|
-
let connection:mariadb.PoolConnection|null = null
|
|
155
|
-
let rows:OINODataRow[] = OINO_EMPTY_ROWS
|
|
156
|
-
try {
|
|
157
|
-
connection = await this._pool.getConnection()
|
|
158
|
-
const sql_res = (params && params.length > 0) ? await connection.query(sql, params) : await connection.query(sql)
|
|
159
|
-
// console.log("OINODbMariadb._exec: result=", result)
|
|
160
|
-
if (Array.isArray(sql_res)) {
|
|
161
|
-
rows = sql_res.filter((r) => Array.isArray(r)) // filter out OkPacket results from multiple statements
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
} catch (e:any) {
|
|
165
|
-
const msg_parts = e.message.match(OINODbMariadb._sqlExceptionMessageRegex) || []
|
|
166
|
-
OINOLog.exception("@oino-ts/db-mariadb", "OINODbMariadb", "_exec", "exception in SQL exec", {message:msg_parts[2], stack:e.stack})
|
|
167
|
-
return new OINOMariadbData(OINO_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db exec [" + msg_parts[2] + "]", "OINODbMariadb._exec") as OINOMariadbData
|
|
168
|
-
|
|
169
|
-
} finally {
|
|
170
|
-
if (connection) {
|
|
171
|
-
connection.release()
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return new OINOMariadbData(rows, [])
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Print a table name using database specific SQL escaping.
|
|
179
|
-
*
|
|
180
|
-
* @param sqlTable name of the table
|
|
181
|
-
*
|
|
182
|
-
*/
|
|
183
|
-
printTableName(sqlTable:string): string {
|
|
184
|
-
return "`"+sqlTable.replaceAll("`", "``")+"`"
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Print a column name with correct SQL escaping.
|
|
189
|
-
*
|
|
190
|
-
* @param sqlColumn name of the column
|
|
191
|
-
*
|
|
192
|
-
*/
|
|
193
|
-
printColumnName(sqlColumn:string): string {
|
|
194
|
-
return "`"+sqlColumn.replaceAll("`", "``")+"`"
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Print a bind-parameter placeholder for the given zero-based parameter index (MySQL/MariaDB `?`).
|
|
199
|
-
*
|
|
200
|
-
* @param index zero-based parameter index
|
|
201
|
-
*
|
|
202
|
-
*/
|
|
203
|
-
printParameterName(index:number): string {
|
|
204
|
-
return "?"
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Coerce a data value into a MariaDB bind-parameter value. The connector binds numbers,
|
|
209
|
-
* strings, `Date` and `Buffer` natively; booleans are mapped to 1/0 for `bit`/numeric columns.
|
|
210
|
-
*
|
|
211
|
-
* @param cellValue data value to bind
|
|
212
|
-
* @param nativeType native type name for the table column
|
|
213
|
-
*
|
|
214
|
-
*/
|
|
215
|
-
bindCellValue(cellValue:OINODataCell, nativeType: string): OINODataCell {
|
|
216
|
-
if (cellValue === undefined) {
|
|
217
|
-
return null
|
|
218
|
-
}
|
|
219
|
-
if (typeof cellValue === "boolean") {
|
|
220
|
-
return cellValue ? 1 : 0
|
|
221
|
-
}
|
|
222
|
-
return cellValue
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Print a single data value from serialization using the context of the native data
|
|
228
|
-
* type with the correct SQL escaping.
|
|
229
|
-
*
|
|
230
|
-
* @param cellValue data from sql results
|
|
231
|
-
* @param nativeType native type name for table column
|
|
232
|
-
*
|
|
233
|
-
*/
|
|
234
|
-
printCellAsValue(cellValue:OINODataCell, nativeType: string): string {
|
|
235
|
-
if (cellValue === null) {
|
|
236
|
-
return "NULL"
|
|
237
|
-
|
|
238
|
-
} else if (cellValue === undefined) {
|
|
239
|
-
return "UNDEFINED"
|
|
240
|
-
|
|
241
|
-
} else if ((nativeType == "int") || (nativeType == "smallint") || (nativeType == "float") || (nativeType == "double")) {
|
|
242
|
-
return cellValue.toString()
|
|
243
|
-
|
|
244
|
-
} else if ((nativeType == "longblob") || (nativeType == "binary") || (nativeType == "varbinary")) {
|
|
245
|
-
if (cellValue instanceof Buffer) {
|
|
246
|
-
return "x'" + (cellValue as Buffer).toString("hex") + "'"
|
|
247
|
-
} else if (cellValue instanceof Uint8Array) {
|
|
248
|
-
return "x'" + Buffer.from(cellValue as Uint8Array).toString("hex") + "'"
|
|
249
|
-
} else {
|
|
250
|
-
return "\"" + cellValue?.toString() + "\""
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
} else if (((nativeType == "date") || (nativeType == "datetime") || (nativeType == "timestamp")) && (cellValue instanceof Date)) {
|
|
254
|
-
return "\"" + cellValue.toISOString().replace('T', ' ').substring(0, 23) + "\""
|
|
255
|
-
|
|
256
|
-
} else if ((nativeType == "bit")) {
|
|
257
|
-
if ((cellValue === false) || (cellValue == null) || (cellValue == "") || (cellValue.toString().toLowerCase() == "false") || (cellValue == "0")) {
|
|
258
|
-
return "b'0'"
|
|
259
|
-
} else if ((cellValue === true) || (cellValue.toString().toLowerCase() == "true")) {
|
|
260
|
-
return "b'1'"
|
|
261
|
-
} else {
|
|
262
|
-
return "b'" + cellValue.toString() + "'" // rest is assumed to be a valid bitstring
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
} else {
|
|
266
|
-
return this.printStringValue(cellValue?.toString())
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Print a single string value as valid sql literal
|
|
272
|
-
*
|
|
273
|
-
* @param sqlString string value
|
|
274
|
-
*
|
|
275
|
-
*/
|
|
276
|
-
printStringValue(sqlString:string): string {
|
|
277
|
-
return "\"" + sqlString.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\t", "\\t") + "\""
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Parse a single SQL result value for serialization using the context of the native data
|
|
283
|
-
* type.
|
|
284
|
-
*
|
|
285
|
-
* @param sqlValue data from serialization
|
|
286
|
-
* @param nativeType native type name for table column
|
|
287
|
-
*
|
|
288
|
-
*/
|
|
289
|
-
parseValueAsCell(sqlValue:OINODataCell, nativeType: string): OINODataCell {
|
|
290
|
-
if ((sqlValue === null) || (sqlValue == "NULL")) {
|
|
291
|
-
return null
|
|
292
|
-
|
|
293
|
-
} else if (sqlValue === undefined) {
|
|
294
|
-
return undefined
|
|
295
|
-
|
|
296
|
-
} else if (((nativeType == "date")) && (typeof(sqlValue) == "string") && (sqlValue != "")) {
|
|
297
|
-
return new Date(sqlValue)
|
|
298
|
-
|
|
299
|
-
} else if ((nativeType == "bit") && (sqlValue instanceof Buffer)) { // mariadb returns a buffer for bit-fields
|
|
300
|
-
const buf:Buffer = sqlValue as Buffer
|
|
301
|
-
if (buf.length == 1) {
|
|
302
|
-
return buf.readUInt8(0) === 1
|
|
303
|
-
} else {
|
|
304
|
-
let result:string = ""
|
|
305
|
-
for (let i=0; i<buf.length; i++) {
|
|
306
|
-
result += buf[i].toString(2).padStart(8, '0')
|
|
307
|
-
}
|
|
308
|
-
return result
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
} else {
|
|
312
|
-
return sqlValue
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Connect to database.
|
|
319
|
-
*
|
|
320
|
-
*/
|
|
321
|
-
async connect(): Promise<OINOResult> {
|
|
322
|
-
let result:OINOResult = new OINOResult()
|
|
323
|
-
if (this.isConnected) {
|
|
324
|
-
return result
|
|
325
|
-
}
|
|
326
|
-
let connection:mariadb.PoolConnection|null = null
|
|
327
|
-
try {
|
|
328
|
-
// make sure that any items are correctly URL encoded in the connection string
|
|
329
|
-
connection = await this._pool.getConnection()
|
|
330
|
-
this.isConnected = true
|
|
331
|
-
|
|
332
|
-
} catch (e:any) {
|
|
333
|
-
const msg_parts = (e as Error).message.match(OINODbMariadb._connectionExceptionMessageRegex) || []
|
|
334
|
-
result.setError(500, "Error connecting to server: " + msg_parts[2], "OINODbMariadb.connect")
|
|
335
|
-
OINOLog.exception("@oino-ts/db-mariadb", "OINODbMariadb", "connect", "exception in connect", {message:e.message, stack:e.stack})
|
|
336
|
-
} finally {
|
|
337
|
-
if (connection) {
|
|
338
|
-
await connection.release()
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
OINOBenchmark.endMetric("OINODb", "connect", result.status != 500)
|
|
343
|
-
return result
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Validate connection to database is working.
|
|
348
|
-
*
|
|
349
|
-
*/
|
|
350
|
-
async validate(): Promise<OINOResult> {
|
|
351
|
-
OINOBenchmark.startMetric("OINODb", "validate")
|
|
352
|
-
let result:OINOResult = new OINOResult()
|
|
353
|
-
try {
|
|
354
|
-
const sql = this._getValidateSql()
|
|
355
|
-
const sql_res:OINODataSet = await this._query(sql, [this.dbParams.database])
|
|
356
|
-
if (sql_res.isEmpty()) {
|
|
357
|
-
result.setError(400, "DB returned no rows for schema!", "OINODbMariadb.validate")
|
|
358
|
-
|
|
359
|
-
} else {
|
|
360
|
-
this.isValidated = true
|
|
361
|
-
}
|
|
362
|
-
} catch (e:any) {
|
|
363
|
-
result.setError(500, "Exception validating connection: " + e.message, "OINODbMariadb.validate")
|
|
364
|
-
OINOLog.exception("@oino-ts/db-mariadb", "OINODbMariadb", "validate", "exception in validate", {message:e.message, stack:e.stack})
|
|
365
|
-
}
|
|
366
|
-
OINOBenchmark.endMetric("OINODb", "validate", result.status != 500)
|
|
367
|
-
return result
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Disconnect from database.
|
|
372
|
-
*
|
|
373
|
-
*/
|
|
374
|
-
async disconnect(): Promise<void> {
|
|
375
|
-
if (this.isConnected) {
|
|
376
|
-
await this._pool.end()
|
|
377
|
-
}
|
|
378
|
-
this.isConnected = false
|
|
379
|
-
this.isValidated = false
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Execute a select operation.
|
|
384
|
-
*
|
|
385
|
-
* @param sql SQL statement.
|
|
386
|
-
*
|
|
387
|
-
*/
|
|
388
|
-
async sqlSelect(sql:string): Promise<OINODataSet> {
|
|
389
|
-
if (!this.isValidated) {
|
|
390
|
-
throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
|
|
391
|
-
}
|
|
392
|
-
OINOBenchmark.startMetric("OINODb", "sqlSelect")
|
|
393
|
-
let result:OINODataSet = await this._query(sql)
|
|
394
|
-
OINOBenchmark.endMetric("OINODb", "sqlSelect", result.status != 500)
|
|
395
|
-
return result
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Execute other sql operations.
|
|
400
|
-
*
|
|
401
|
-
* @param sql SQL statement.
|
|
402
|
-
*
|
|
403
|
-
*/
|
|
404
|
-
async sqlExec(sql:string): Promise<OINODataSet> {
|
|
405
|
-
if (!this.isValidated) {
|
|
406
|
-
throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
|
|
407
|
-
}
|
|
408
|
-
OINOBenchmark.startMetric("OINODb", "sqlExec")
|
|
409
|
-
let result:OINODataSet = await this._exec(sql)
|
|
410
|
-
OINOBenchmark.endMetric("OINODb", "sqlExec", result.status != 500)
|
|
411
|
-
return result
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Execute a parameterized statement, binding its values as positional `?` parameters.
|
|
416
|
-
*
|
|
417
|
-
* @param statement statement (SQL text + ordered bind values) to execute
|
|
418
|
-
*
|
|
419
|
-
*/
|
|
420
|
-
async runStatement(statement:OINODbSqlStatement): Promise<OINODataSet> {
|
|
421
|
-
if (!this.isValidated) {
|
|
422
|
-
throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
|
|
423
|
-
}
|
|
424
|
-
OINOBenchmark.startMetric("OINODb", "runStatement")
|
|
425
|
-
let result:OINODataSet = await this._exec(statement.sql, statement.values)
|
|
426
|
-
OINOBenchmark.endMetric("OINODb", "runStatement", result.status != 500)
|
|
427
|
-
return result
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private _getSchemaSql():string {
|
|
431
|
-
const sql =
|
|
432
|
-
`SELECT
|
|
433
|
-
C.COLUMN_NAME,
|
|
434
|
-
C.COLUMN_TYPE,
|
|
435
|
-
C.IS_NULLABLE,
|
|
436
|
-
C.COLUMN_KEY,
|
|
437
|
-
C.COLUMN_DEFAULT,
|
|
438
|
-
C.EXTRA,
|
|
439
|
-
KCU.CONSTRAINT_NAME AS ForeignKeyName
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
const
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
fields.push(new
|
|
489
|
-
} else if (sql_type == "
|
|
490
|
-
fields.push(new
|
|
491
|
-
} else if (sql_type == "
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
} else {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
case "
|
|
539
|
-
return "
|
|
540
|
-
case "
|
|
541
|
-
return "
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { OINO_ERROR_PREFIX, OINOBenchmark, OINO_INFO_PREFIX, OINOLog, OINOResult } from "@oino-ts/common";
|
|
8
|
+
|
|
9
|
+
import { OINODataSet, OINOBooleanDataField, OINONumberDataField, OINOStringDataField, OINODataField, OINODataFieldSchema, OINODataFieldParams, OINODataRow, OINODataCell, OINODatetimeDataField, OINOBlobDataField, OINO_EMPTY_ROW, OINO_EMPTY_ROWS } from "@oino-ts/common";
|
|
10
|
+
|
|
11
|
+
import { OINODb, OINODbParams, OINODbSqlStatement } from "@oino-ts/db";
|
|
12
|
+
|
|
13
|
+
import mariadb from "mariadb";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Implmentation of OINODataSet for MariaDb.
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
class OINOMariadbData extends OINODataSet {
|
|
20
|
+
private _rows:OINODataRow[] = OINO_EMPTY_ROWS
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* OINOMariadbData constructor
|
|
24
|
+
* @param params database parameters
|
|
25
|
+
*/
|
|
26
|
+
constructor(data: any, messages:string[]=[]) {
|
|
27
|
+
super(data, messages)
|
|
28
|
+
|
|
29
|
+
if (data == null) {
|
|
30
|
+
this.messages.push(OINO_INFO_PREFIX + "SQL result is empty")
|
|
31
|
+
|
|
32
|
+
} else if (Array.isArray(data)) {
|
|
33
|
+
this._rows = data as OINODataRow[]
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
if (this.isEmpty()) {
|
|
37
|
+
this._currentRow = -1
|
|
38
|
+
this._eof = true
|
|
39
|
+
} else {
|
|
40
|
+
this._currentRow = 0
|
|
41
|
+
this._eof = false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
private _currentRow: number
|
|
45
|
+
private _eof: boolean
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Is data set empty.
|
|
49
|
+
*
|
|
50
|
+
*/
|
|
51
|
+
isEmpty():boolean {
|
|
52
|
+
return (this._rows.length == 0)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Is there no more content, i.e. either dataset is empty or we have moved beyond last line
|
|
57
|
+
*
|
|
58
|
+
*/
|
|
59
|
+
isEof():boolean {
|
|
60
|
+
return (this._eof)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Attempts to moves dataset to the next row, possibly waiting for more data to become available. Returns !isEof().
|
|
65
|
+
*
|
|
66
|
+
*/
|
|
67
|
+
async next():Promise<boolean> {
|
|
68
|
+
if (this._currentRow < this._rows.length-1) {
|
|
69
|
+
this._currentRow = this._currentRow + 1
|
|
70
|
+
} else {
|
|
71
|
+
this._eof = true
|
|
72
|
+
}
|
|
73
|
+
return Promise.resolve(!this._eof)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets current row of data.
|
|
78
|
+
*
|
|
79
|
+
*/
|
|
80
|
+
getRow(): OINODataRow {
|
|
81
|
+
if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
|
|
82
|
+
return this._rows[this._currentRow]
|
|
83
|
+
} else {
|
|
84
|
+
return OINO_EMPTY_ROW
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gets all rows of data.
|
|
90
|
+
*
|
|
91
|
+
*/
|
|
92
|
+
async getAllRows(): Promise<OINODataRow[]> {
|
|
93
|
+
return this._rows // at the moment theres no result streaming, so we can just return the rows
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Implementation of MariaDb/MySql-database.
|
|
99
|
+
*
|
|
100
|
+
*/
|
|
101
|
+
export class OINODbMariadb extends OINODb {
|
|
102
|
+
|
|
103
|
+
private static _fieldLengthRegex = /([^\(\)]+)(\s?\((\d+)\s?\,?\s?(\d*)?\))?/i
|
|
104
|
+
private static _connectionExceptionMessageRegex = /\(([^\)]*)\) (.*)/i
|
|
105
|
+
private static _sqlExceptionMessageRegex = /\(([^\)]*)\) (.*)\nsql\:(.*)?/i
|
|
106
|
+
|
|
107
|
+
private _pool:mariadb.Pool
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Constructor of `OINODbMariadb`
|
|
111
|
+
* @param params database parameters
|
|
112
|
+
*/
|
|
113
|
+
constructor(params:OINODbParams) {
|
|
114
|
+
super(params)
|
|
115
|
+
|
|
116
|
+
if (this.dbParams.type !== "OINODbMariadb") {
|
|
117
|
+
throw new Error(OINO_ERROR_PREFIX + ": Not OINODbMariadb-type: " + this.dbParams.type)
|
|
118
|
+
}
|
|
119
|
+
this._pool = mariadb.createPool({ host: this.dbParams.url, database: this.dbParams.database, port: this.dbParams.port, user: this.dbParams.user, password: this.dbParams.password, acquireTimeout: 2000, debug:false, rowsAsArray: true, multipleStatements: false }) // statements are now executed individually with bind parameters, so stacked/multi-statement execution is disabled
|
|
120
|
+
delete this.dbParams.password // do not store password in db object
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private _parseFieldLength(fieldLengthStr:string):number {
|
|
124
|
+
let result:number = parseInt(fieldLengthStr)
|
|
125
|
+
if (Number.isNaN(result)) {
|
|
126
|
+
result = 0
|
|
127
|
+
}
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async _query(sql:string, params?:OINODataCell[]):Promise<OINODataSet> {
|
|
132
|
+
let connection:mariadb.PoolConnection|null = null
|
|
133
|
+
let rows:OINODataRow[] = OINO_EMPTY_ROWS
|
|
134
|
+
try {
|
|
135
|
+
connection = await this._pool.getConnection()
|
|
136
|
+
const sql_res = (params && params.length > 0) ? await connection.query(sql, params) : await connection.query(sql)
|
|
137
|
+
// console.log("_query: sql=", sql, " result=", result)
|
|
138
|
+
if (Array.isArray(sql_res)) {
|
|
139
|
+
rows = sql_res.filter((r) => Array.isArray(r)) as OINODataRow[] // filter out OkPacket results from multiple statements
|
|
140
|
+
}
|
|
141
|
+
} catch (e:any) {
|
|
142
|
+
OINOLog.exception("@oino-ts/db-mariadb", "OINODbMariadb", "_query", "exception in SQL select", {message:e.message, stack:e.stack})
|
|
143
|
+
return new OINOMariadbData(OINO_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db query: " + e.message, "OINODbMariadb._query") as OINOMariadbData
|
|
144
|
+
|
|
145
|
+
} finally {
|
|
146
|
+
if (connection) {
|
|
147
|
+
connection.release()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return new OINOMariadbData(rows, [])
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async _exec(sql:string, params?:OINODataCell[]):Promise<OINODataSet> {
|
|
154
|
+
let connection:mariadb.PoolConnection|null = null
|
|
155
|
+
let rows:OINODataRow[] = OINO_EMPTY_ROWS
|
|
156
|
+
try {
|
|
157
|
+
connection = await this._pool.getConnection()
|
|
158
|
+
const sql_res = (params && params.length > 0) ? await connection.query(sql, params) : await connection.query(sql)
|
|
159
|
+
// console.log("OINODbMariadb._exec: result=", result)
|
|
160
|
+
if (Array.isArray(sql_res)) {
|
|
161
|
+
rows = sql_res.filter((r) => Array.isArray(r)) // filter out OkPacket results from multiple statements
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
} catch (e:any) {
|
|
165
|
+
const msg_parts = e.message.match(OINODbMariadb._sqlExceptionMessageRegex) || []
|
|
166
|
+
OINOLog.exception("@oino-ts/db-mariadb", "OINODbMariadb", "_exec", "exception in SQL exec", {message:msg_parts[2], stack:e.stack})
|
|
167
|
+
return new OINOMariadbData(OINO_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db exec [" + msg_parts[2] + "]", "OINODbMariadb._exec") as OINOMariadbData
|
|
168
|
+
|
|
169
|
+
} finally {
|
|
170
|
+
if (connection) {
|
|
171
|
+
connection.release()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return new OINOMariadbData(rows, [])
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Print a table name using database specific SQL escaping.
|
|
179
|
+
*
|
|
180
|
+
* @param sqlTable name of the table
|
|
181
|
+
*
|
|
182
|
+
*/
|
|
183
|
+
printTableName(sqlTable:string): string {
|
|
184
|
+
return "`"+sqlTable.replaceAll("`", "``")+"`"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Print a column name with correct SQL escaping.
|
|
189
|
+
*
|
|
190
|
+
* @param sqlColumn name of the column
|
|
191
|
+
*
|
|
192
|
+
*/
|
|
193
|
+
printColumnName(sqlColumn:string): string {
|
|
194
|
+
return "`"+sqlColumn.replaceAll("`", "``")+"`"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Print a bind-parameter placeholder for the given zero-based parameter index (MySQL/MariaDB `?`).
|
|
199
|
+
*
|
|
200
|
+
* @param index zero-based parameter index
|
|
201
|
+
*
|
|
202
|
+
*/
|
|
203
|
+
printParameterName(index:number): string {
|
|
204
|
+
return "?"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Coerce a data value into a MariaDB bind-parameter value. The connector binds numbers,
|
|
209
|
+
* strings, `Date` and `Buffer` natively; booleans are mapped to 1/0 for `bit`/numeric columns.
|
|
210
|
+
*
|
|
211
|
+
* @param cellValue data value to bind
|
|
212
|
+
* @param nativeType native type name for the table column
|
|
213
|
+
*
|
|
214
|
+
*/
|
|
215
|
+
bindCellValue(cellValue:OINODataCell, nativeType: string): OINODataCell {
|
|
216
|
+
if (cellValue === undefined) {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
if (typeof cellValue === "boolean") {
|
|
220
|
+
return cellValue ? 1 : 0
|
|
221
|
+
}
|
|
222
|
+
return cellValue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Print a single data value from serialization using the context of the native data
|
|
228
|
+
* type with the correct SQL escaping.
|
|
229
|
+
*
|
|
230
|
+
* @param cellValue data from sql results
|
|
231
|
+
* @param nativeType native type name for table column
|
|
232
|
+
*
|
|
233
|
+
*/
|
|
234
|
+
printCellAsValue(cellValue:OINODataCell, nativeType: string): string {
|
|
235
|
+
if (cellValue === null) {
|
|
236
|
+
return "NULL"
|
|
237
|
+
|
|
238
|
+
} else if (cellValue === undefined) {
|
|
239
|
+
return "UNDEFINED"
|
|
240
|
+
|
|
241
|
+
} else if ((nativeType == "int") || (nativeType == "smallint") || (nativeType == "float") || (nativeType == "double")) {
|
|
242
|
+
return cellValue.toString()
|
|
243
|
+
|
|
244
|
+
} else if ((nativeType == "longblob") || (nativeType == "binary") || (nativeType == "varbinary")) {
|
|
245
|
+
if (cellValue instanceof Buffer) {
|
|
246
|
+
return "x'" + (cellValue as Buffer).toString("hex") + "'"
|
|
247
|
+
} else if (cellValue instanceof Uint8Array) {
|
|
248
|
+
return "x'" + Buffer.from(cellValue as Uint8Array).toString("hex") + "'"
|
|
249
|
+
} else {
|
|
250
|
+
return "\"" + cellValue?.toString() + "\""
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
} else if (((nativeType == "date") || (nativeType == "datetime") || (nativeType == "timestamp")) && (cellValue instanceof Date)) {
|
|
254
|
+
return "\"" + cellValue.toISOString().replace('T', ' ').substring(0, 23) + "\""
|
|
255
|
+
|
|
256
|
+
} else if ((nativeType == "bit")) {
|
|
257
|
+
if ((cellValue === false) || (cellValue == null) || (cellValue == "") || (cellValue.toString().toLowerCase() == "false") || (cellValue == "0")) {
|
|
258
|
+
return "b'0'"
|
|
259
|
+
} else if ((cellValue === true) || (cellValue.toString().toLowerCase() == "true")) {
|
|
260
|
+
return "b'1'"
|
|
261
|
+
} else {
|
|
262
|
+
return "b'" + cellValue.toString() + "'" // rest is assumed to be a valid bitstring
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
} else {
|
|
266
|
+
return this.printStringValue(cellValue?.toString())
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Print a single string value as valid sql literal
|
|
272
|
+
*
|
|
273
|
+
* @param sqlString string value
|
|
274
|
+
*
|
|
275
|
+
*/
|
|
276
|
+
printStringValue(sqlString:string): string {
|
|
277
|
+
return "\"" + sqlString.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\t", "\\t") + "\""
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Parse a single SQL result value for serialization using the context of the native data
|
|
283
|
+
* type.
|
|
284
|
+
*
|
|
285
|
+
* @param sqlValue data from serialization
|
|
286
|
+
* @param nativeType native type name for table column
|
|
287
|
+
*
|
|
288
|
+
*/
|
|
289
|
+
parseValueAsCell(sqlValue:OINODataCell, nativeType: string): OINODataCell {
|
|
290
|
+
if ((sqlValue === null) || (sqlValue == "NULL")) {
|
|
291
|
+
return null
|
|
292
|
+
|
|
293
|
+
} else if (sqlValue === undefined) {
|
|
294
|
+
return undefined
|
|
295
|
+
|
|
296
|
+
} else if (((nativeType == "date")) && (typeof(sqlValue) == "string") && (sqlValue != "")) {
|
|
297
|
+
return new Date(sqlValue)
|
|
298
|
+
|
|
299
|
+
} else if ((nativeType == "bit") && (sqlValue instanceof Buffer)) { // mariadb returns a buffer for bit-fields
|
|
300
|
+
const buf:Buffer = sqlValue as Buffer
|
|
301
|
+
if (buf.length == 1) {
|
|
302
|
+
return buf.readUInt8(0) === 1
|
|
303
|
+
} else {
|
|
304
|
+
let result:string = ""
|
|
305
|
+
for (let i=0; i<buf.length; i++) {
|
|
306
|
+
result += buf[i].toString(2).padStart(8, '0')
|
|
307
|
+
}
|
|
308
|
+
return result
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
} else {
|
|
312
|
+
return sqlValue
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Connect to database.
|
|
319
|
+
*
|
|
320
|
+
*/
|
|
321
|
+
async connect(): Promise<OINOResult> {
|
|
322
|
+
let result:OINOResult = new OINOResult()
|
|
323
|
+
if (this.isConnected) {
|
|
324
|
+
return result
|
|
325
|
+
}
|
|
326
|
+
let connection:mariadb.PoolConnection|null = null
|
|
327
|
+
try {
|
|
328
|
+
// make sure that any items are correctly URL encoded in the connection string
|
|
329
|
+
connection = await this._pool.getConnection()
|
|
330
|
+
this.isConnected = true
|
|
331
|
+
|
|
332
|
+
} catch (e:any) {
|
|
333
|
+
const msg_parts = (e as Error).message.match(OINODbMariadb._connectionExceptionMessageRegex) || []
|
|
334
|
+
result.setError(500, "Error connecting to server: " + msg_parts[2], "OINODbMariadb.connect")
|
|
335
|
+
OINOLog.exception("@oino-ts/db-mariadb", "OINODbMariadb", "connect", "exception in connect", {message:e.message, stack:e.stack})
|
|
336
|
+
} finally {
|
|
337
|
+
if (connection) {
|
|
338
|
+
await connection.release()
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
OINOBenchmark.endMetric("OINODb", "connect", result.status != 500)
|
|
343
|
+
return result
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Validate connection to database is working.
|
|
348
|
+
*
|
|
349
|
+
*/
|
|
350
|
+
async validate(): Promise<OINOResult> {
|
|
351
|
+
OINOBenchmark.startMetric("OINODb", "validate")
|
|
352
|
+
let result:OINOResult = new OINOResult()
|
|
353
|
+
try {
|
|
354
|
+
const sql = this._getValidateSql()
|
|
355
|
+
const sql_res:OINODataSet = await this._query(sql, [this.dbParams.database])
|
|
356
|
+
if (sql_res.isEmpty()) {
|
|
357
|
+
result.setError(400, "DB returned no rows for schema!", "OINODbMariadb.validate")
|
|
358
|
+
|
|
359
|
+
} else {
|
|
360
|
+
this.isValidated = true
|
|
361
|
+
}
|
|
362
|
+
} catch (e:any) {
|
|
363
|
+
result.setError(500, "Exception validating connection: " + e.message, "OINODbMariadb.validate")
|
|
364
|
+
OINOLog.exception("@oino-ts/db-mariadb", "OINODbMariadb", "validate", "exception in validate", {message:e.message, stack:e.stack})
|
|
365
|
+
}
|
|
366
|
+
OINOBenchmark.endMetric("OINODb", "validate", result.status != 500)
|
|
367
|
+
return result
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Disconnect from database.
|
|
372
|
+
*
|
|
373
|
+
*/
|
|
374
|
+
async disconnect(): Promise<void> {
|
|
375
|
+
if (this.isConnected) {
|
|
376
|
+
await this._pool.end()
|
|
377
|
+
}
|
|
378
|
+
this.isConnected = false
|
|
379
|
+
this.isValidated = false
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Execute a select operation.
|
|
384
|
+
*
|
|
385
|
+
* @param sql SQL statement.
|
|
386
|
+
*
|
|
387
|
+
*/
|
|
388
|
+
async sqlSelect(sql:string): Promise<OINODataSet> {
|
|
389
|
+
if (!this.isValidated) {
|
|
390
|
+
throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
|
|
391
|
+
}
|
|
392
|
+
OINOBenchmark.startMetric("OINODb", "sqlSelect")
|
|
393
|
+
let result:OINODataSet = await this._query(sql)
|
|
394
|
+
OINOBenchmark.endMetric("OINODb", "sqlSelect", result.status != 500)
|
|
395
|
+
return result
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Execute other sql operations.
|
|
400
|
+
*
|
|
401
|
+
* @param sql SQL statement.
|
|
402
|
+
*
|
|
403
|
+
*/
|
|
404
|
+
async sqlExec(sql:string): Promise<OINODataSet> {
|
|
405
|
+
if (!this.isValidated) {
|
|
406
|
+
throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
|
|
407
|
+
}
|
|
408
|
+
OINOBenchmark.startMetric("OINODb", "sqlExec")
|
|
409
|
+
let result:OINODataSet = await this._exec(sql)
|
|
410
|
+
OINOBenchmark.endMetric("OINODb", "sqlExec", result.status != 500)
|
|
411
|
+
return result
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Execute a parameterized statement, binding its values as positional `?` parameters.
|
|
416
|
+
*
|
|
417
|
+
* @param statement statement (SQL text + ordered bind values) to execute
|
|
418
|
+
*
|
|
419
|
+
*/
|
|
420
|
+
async runStatement(statement:OINODbSqlStatement): Promise<OINODataSet> {
|
|
421
|
+
if (!this.isValidated) {
|
|
422
|
+
throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
|
|
423
|
+
}
|
|
424
|
+
OINOBenchmark.startMetric("OINODb", "runStatement")
|
|
425
|
+
let result:OINODataSet = await this._exec(statement.sql, statement.values)
|
|
426
|
+
OINOBenchmark.endMetric("OINODb", "runStatement", result.status != 500)
|
|
427
|
+
return result
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private _getSchemaSql():string {
|
|
431
|
+
const sql =
|
|
432
|
+
`SELECT
|
|
433
|
+
C.COLUMN_NAME,
|
|
434
|
+
C.COLUMN_TYPE,
|
|
435
|
+
C.IS_NULLABLE,
|
|
436
|
+
C.COLUMN_KEY,
|
|
437
|
+
C.COLUMN_DEFAULT,
|
|
438
|
+
C.EXTRA,
|
|
439
|
+
KCU.CONSTRAINT_NAME AS ForeignKeyName,
|
|
440
|
+
KCU.REFERENCED_TABLE_NAME,
|
|
441
|
+
KCU.REFERENCED_COLUMN_NAME
|
|
442
|
+
FROM information_schema.COLUMNS C
|
|
443
|
+
LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
|
|
444
|
+
WHERE C.TABLE_SCHEMA = ? AND C.TABLE_NAME = ?
|
|
445
|
+
ORDER BY C.ORDINAL_POSITION;`
|
|
446
|
+
return sql
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private _getValidateSql():string {
|
|
450
|
+
const sql =
|
|
451
|
+
`SELECT
|
|
452
|
+
Count(C.COLUMN_NAME) AS COLUMN_COUNT
|
|
453
|
+
FROM information_schema.COLUMNS C
|
|
454
|
+
LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
|
|
455
|
+
WHERE C.TABLE_SCHEMA = ?;`
|
|
456
|
+
return sql
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Get the schema fields of a table as `OINODataField`s (without any API-level field filtering).
|
|
462
|
+
*
|
|
463
|
+
* @param tableName name of the table
|
|
464
|
+
*
|
|
465
|
+
*/
|
|
466
|
+
async getSchemaFields(tableName:string): Promise<OINODataField[]> {
|
|
467
|
+
const fields:OINODataField[] = []
|
|
468
|
+
const schema_res:OINODataSet = await this._query(this._getSchemaSql(), [this.dbParams.database, tableName])
|
|
469
|
+
while (!schema_res.isEof()) {
|
|
470
|
+
const row:OINODataRow = schema_res.getRow()
|
|
471
|
+
const field_name:string = row[0]?.toString() || ""
|
|
472
|
+
const field_matches = OINODbMariadb._fieldLengthRegex.exec(row[1]?.toString() || "") || []
|
|
473
|
+
const sql_type:string = field_matches[1] || ""
|
|
474
|
+
const field_length1:number = this._parseFieldLength(field_matches[3] || "0")
|
|
475
|
+
const field_length2:number = this._parseFieldLength(field_matches[4] || "0")
|
|
476
|
+
const extra:string = row[5]?.toString() || ""
|
|
477
|
+
const fk_table:string = row[7]?.toString() || ""
|
|
478
|
+
const fk_column:string = row[8]?.toString() || ""
|
|
479
|
+
const foreign_key = fk_table ? { table: fk_table, column: fk_column } : null
|
|
480
|
+
const field_params:OINODataFieldParams = {
|
|
481
|
+
isPrimaryKey: row[3] == "PRI",
|
|
482
|
+
isForeignKey: (row[6] != null) || (foreign_key != null),
|
|
483
|
+
foreignKey: foreign_key,
|
|
484
|
+
isAutoInc: extra.indexOf('auto_increment') >= 0,
|
|
485
|
+
isNotNull: row[2] == "NO"
|
|
486
|
+
}
|
|
487
|
+
if ((sql_type == "int") || (sql_type == "smallint") || (sql_type == "mediumint") || (sql_type == "bigint") || (sql_type == "float") || (sql_type == "double")) {
|
|
488
|
+
fields.push(new OINONumberDataField(this, field_name, sql_type, field_params))
|
|
489
|
+
} else if ((sql_type == "date") || (sql_type == "datetime") || (sql_type == "timestamp")) {
|
|
490
|
+
fields.push(new OINODatetimeDataField(this, field_name, sql_type, field_params))
|
|
491
|
+
} else if ((sql_type == "char") || (sql_type == "varchar") || (sql_type == "tinytext") || (sql_type == "mediumtext") || (sql_type == "longtext")) {
|
|
492
|
+
fields.push(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1))
|
|
493
|
+
} else if ((sql_type == "longblob") || (sql_type == "binary") || (sql_type == "varbinary")) {
|
|
494
|
+
fields.push(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length1))
|
|
495
|
+
} else if (sql_type == "decimal") {
|
|
496
|
+
fields.push(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1 + field_length2 + 1))
|
|
497
|
+
} else if (sql_type == "bit") {
|
|
498
|
+
if (field_length1 == 1) {
|
|
499
|
+
fields.push(new OINOBooleanDataField(this, field_name, sql_type, field_params))
|
|
500
|
+
} else {
|
|
501
|
+
fields.push(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1 * 8))
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
fields.push(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
|
|
505
|
+
}
|
|
506
|
+
await schema_res.next()
|
|
507
|
+
}
|
|
508
|
+
return fields
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Get the names of all user (base) tables in the database schema, excluding system tables and views.
|
|
513
|
+
*
|
|
514
|
+
*/
|
|
515
|
+
async getSchemaTables(): Promise<string[]> {
|
|
516
|
+
const tables:string[] = []
|
|
517
|
+
const sql:string = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME;"
|
|
518
|
+
const tables_res:OINODataSet = await this._query(sql, [this.dbParams.database])
|
|
519
|
+
while (!tables_res.isEof()) {
|
|
520
|
+
const row:OINODataRow = tables_res.getRow()
|
|
521
|
+
const table_name:string = row[0]?.toString() || ""
|
|
522
|
+
if (table_name) {
|
|
523
|
+
tables.push(table_name)
|
|
524
|
+
}
|
|
525
|
+
await tables_res.next()
|
|
526
|
+
}
|
|
527
|
+
return tables
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Resolve the optimal native (SQL) type for a serialized field schema.
|
|
532
|
+
*
|
|
533
|
+
* @param schema serialized field schema
|
|
534
|
+
*
|
|
535
|
+
*/
|
|
536
|
+
getNativeDataType(schema:OINODataFieldSchema): string {
|
|
537
|
+
switch (schema.type) {
|
|
538
|
+
case "string":
|
|
539
|
+
return (schema.maxLength || 0) > 0 ? "varchar(" + schema.maxLength + ")" : "longtext"
|
|
540
|
+
case "number":
|
|
541
|
+
return schema.fieldParams?.isAutoInc ? "bigint" : "double"
|
|
542
|
+
case "boolean":
|
|
543
|
+
return "bit(1)"
|
|
544
|
+
case "datetime":
|
|
545
|
+
return "datetime"
|
|
546
|
+
case "blob":
|
|
547
|
+
return "longblob"
|
|
548
|
+
default:
|
|
549
|
+
throw new Error(OINO_ERROR_PREFIX + ": OINODbMariadb.getNativeDataType - unsupported field type '" + schema.type + "'")
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
protected _printColumnAutoInc(): string {
|
|
554
|
+
return "AUTO_INCREMENT"
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
|