@oino-ts/db-mariadb 1.5.1 → 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.
@@ -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
- FROM information_schema.COLUMNS C
415
- 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
416
- WHERE C.TABLE_SCHEMA = ? AND C.TABLE_NAME = ?
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
- FROM information_schema.COLUMNS C
412
- 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
413
- WHERE C.TABLE_SCHEMA = ? AND C.TABLE_NAME = ?
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.5.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.5.1",
25
- "@oino-ts/db": "^1.5.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": {
@@ -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
- FROM information_schema.COLUMNS C
441
- 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
442
- WHERE C.TABLE_SCHEMA = ? AND C.TABLE_NAME = ?
443
- ORDER BY C.ORDINAL_POSITION;`
444
- return sql
445
- }
446
-
447
- private _getValidateSql():string {
448
- const sql =
449
- `SELECT
450
- Count(C.COLUMN_NAME) AS COLUMN_COUNT
451
- FROM information_schema.COLUMNS C
452
- 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
453
- WHERE C.TABLE_SCHEMA = ?;`
454
- return sql
455
- }
456
-
457
-
458
- /**
459
- * Get the schema fields of a table as `OINODataField`s (without any API-level field filtering).
460
- *
461
- * @param tableName name of the table
462
- *
463
- */
464
- async getSchemaFields(tableName:string): Promise<OINODataField[]> {
465
- const fields:OINODataField[] = []
466
- const schema_res:OINODataSet = await this._query(this._getSchemaSql(), [this.dbParams.database, tableName])
467
- while (!schema_res.isEof()) {
468
- const row:OINODataRow = schema_res.getRow()
469
- const field_name:string = row[0]?.toString() || ""
470
- const field_matches = OINODbMariadb._fieldLengthRegex.exec(row[1]?.toString() || "") || []
471
- const sql_type:string = field_matches[1] || ""
472
- const field_length1:number = this._parseFieldLength(field_matches[3] || "0")
473
- const field_length2:number = this._parseFieldLength(field_matches[4] || "0")
474
- const extra:string = row[5]?.toString() || ""
475
- const field_params:OINODataFieldParams = {
476
- isPrimaryKey: row[3] == "PRI",
477
- isForeignKey: row[6] != null,
478
- isAutoInc: extra.indexOf('auto_increment') >= 0,
479
- isNotNull: row[2] == "NO"
480
- }
481
- if ((sql_type == "int") || (sql_type == "smallint") || (sql_type == "mediumint") || (sql_type == "bigint") || (sql_type == "float") || (sql_type == "double")) {
482
- fields.push(new OINONumberDataField(this, field_name, sql_type, field_params))
483
- } else if ((sql_type == "date") || (sql_type == "datetime") || (sql_type == "timestamp")) {
484
- fields.push(new OINODatetimeDataField(this, field_name, sql_type, field_params))
485
- } else if ((sql_type == "char") || (sql_type == "varchar") || (sql_type == "tinytext") || (sql_type == "mediumtext") || (sql_type == "longtext")) {
486
- fields.push(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1))
487
- } else if ((sql_type == "longblob") || (sql_type == "binary") || (sql_type == "varbinary")) {
488
- fields.push(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length1))
489
- } else if (sql_type == "decimal") {
490
- fields.push(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1 + field_length2 + 1))
491
- } else if (sql_type == "bit") {
492
- if (field_length1 == 1) {
493
- fields.push(new OINOBooleanDataField(this, field_name, sql_type, field_params))
494
- } else {
495
- fields.push(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1 * 8))
496
- }
497
- } else {
498
- fields.push(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
499
- }
500
- await schema_res.next()
501
- }
502
- return fields
503
- }
504
-
505
- /**
506
- * Get the names of all user (base) tables in the database schema, excluding system tables and views.
507
- *
508
- */
509
- async getSchemaTables(): Promise<string[]> {
510
- const tables:string[] = []
511
- const sql:string = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME;"
512
- const tables_res:OINODataSet = await this._query(sql, [this.dbParams.database])
513
- while (!tables_res.isEof()) {
514
- const row:OINODataRow = tables_res.getRow()
515
- const table_name:string = row[0]?.toString() || ""
516
- if (table_name) {
517
- tables.push(table_name)
518
- }
519
- await tables_res.next()
520
- }
521
- return tables
522
- }
523
-
524
- /**
525
- * Resolve the optimal native (SQL) type for a serialized field schema.
526
- *
527
- * @param schema serialized field schema
528
- *
529
- */
530
- getNativeDataType(schema:OINODataFieldSchema): string {
531
- switch (schema.type) {
532
- case "string":
533
- return (schema.maxLength || 0) > 0 ? "varchar(" + schema.maxLength + ")" : "longtext"
534
- case "number":
535
- return schema.fieldParams?.isAutoInc ? "bigint" : "double"
536
- case "boolean":
537
- return "bit(1)"
538
- case "datetime":
539
- return "datetime"
540
- case "blob":
541
- return "longblob"
542
- default:
543
- throw new Error(OINO_ERROR_PREFIX + ": OINODbMariadb.getNativeDataType - unsupported field type '" + schema.type + "'")
544
- }
545
- }
546
-
547
- protected _printColumnAutoInc(): string {
548
- return "AUTO_INCREMENT"
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
+