• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

typeorm / typeorm / 14796576772

02 May 2025 01:52PM UTC coverage: 45.367% (-30.9%) from 76.309%
14796576772

Pull #11434

github

web-flow
Merge ec4ce2d00 into fadad1a74
Pull Request #11434: feat: release PR releases using pkg.pr.new

5216 of 12761 branches covered (40.87%)

Branch coverage included in aggregate %.

11439 of 23951 relevant lines covered (47.76%)

15712.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

9.87
/src/driver/mysql/MysqlDriver.ts
1
import { Driver, ReturningType } from "../Driver"
2
import { ConnectionIsNotSetError } from "../../error/ConnectionIsNotSetError"
4✔
3
import { DriverPackageNotInstalledError } from "../../error/DriverPackageNotInstalledError"
4✔
4
import { DriverUtils } from "../DriverUtils"
4✔
5
import { CteCapabilities } from "../types/CteCapabilities"
6
import { MysqlQueryRunner } from "./MysqlQueryRunner"
4✔
7
import { ObjectLiteral } from "../../common/ObjectLiteral"
8
import { ColumnMetadata } from "../../metadata/ColumnMetadata"
9
import { DateUtils } from "../../util/DateUtils"
4✔
10
import { PlatformTools } from "../../platform/PlatformTools"
4✔
11
import { DataSource } from "../../data-source/DataSource"
12
import { RdbmsSchemaBuilder } from "../../schema-builder/RdbmsSchemaBuilder"
4✔
13
import { MysqlConnectionOptions } from "./MysqlConnectionOptions"
14
import { MappedColumnTypes } from "../types/MappedColumnTypes"
15
import { ColumnType } from "../types/ColumnTypes"
16
import { DataTypeDefaults } from "../types/DataTypeDefaults"
17
import { TableColumn } from "../../schema-builder/table/TableColumn"
18
import { MysqlConnectionCredentialsOptions } from "./MysqlConnectionCredentialsOptions"
19
import { EntityMetadata } from "../../metadata/EntityMetadata"
20
import { OrmUtils } from "../../util/OrmUtils"
4✔
21
import { ApplyValueTransformers } from "../../util/ApplyValueTransformers"
4✔
22
import { ReplicationMode } from "../types/ReplicationMode"
23
import { TypeORMError } from "../../error"
4✔
24
import { Table } from "../../schema-builder/table/Table"
25
import { View } from "../../schema-builder/view/View"
26
import { TableForeignKey } from "../../schema-builder/table/TableForeignKey"
27
import { VersionUtils } from "../../util/VersionUtils"
4✔
28
import { InstanceChecker } from "../../util/InstanceChecker"
4✔
29
import { UpsertType } from "../types/UpsertType"
30

31
/**
32
 * Organizes communication with MySQL DBMS.
33
 */
34
export class MysqlDriver implements Driver {
4✔
35
    // -------------------------------------------------------------------------
36
    // Public Properties
37
    // -------------------------------------------------------------------------
38

39
    /**
40
     * Connection used by driver.
41
     */
42
    connection: DataSource
43

44
    /**
45
     * Mysql underlying library.
46
     */
47
    mysql: any
48

49
    /**
50
     * Connection pool.
51
     * Used in non-replication mode.
52
     */
53
    pool: any
54

55
    /**
56
     * Pool cluster used in replication mode.
57
     */
58
    poolCluster: any
59

60
    // -------------------------------------------------------------------------
61
    // Public Implemented Properties
62
    // -------------------------------------------------------------------------
63

64
    /**
65
     * Connection options.
66
     */
67
    options: MysqlConnectionOptions
68

69
    /**
70
     * Version of MySQL. Requires a SQL query to the DB, so it is not always set
71
     */
72
    version?: string
73

74
    /**
75
     * Master database used to perform all write queries.
76
     */
77
    database?: string
78

79
    /**
80
     * Indicates if replication is enabled.
81
     */
82
    isReplicated: boolean = false
52✔
83

84
    /**
85
     * Indicates if tree tables are supported by this driver.
86
     */
87
    treeSupport = true
52✔
88

89
    /**
90
     * Represent transaction support by this driver
91
     */
92
    transactionSupport = "nested" as const
52✔
93

94
    /**
95
     * Gets list of supported column data types by a driver.
96
     *
97
     * @see https://www.tutorialspoint.com/mysql/mysql-data-types.htm
98
     * @see https://dev.mysql.com/doc/refman/8.0/en/data-types.html
99
     */
100
    supportedDataTypes: ColumnType[] = [
52✔
101
        // numeric types
102
        "bit",
103
        "int",
104
        "integer", // synonym for int
105
        "tinyint",
106
        "smallint",
107
        "mediumint",
108
        "bigint",
109
        "float",
110
        "double",
111
        "double precision", // synonym for double
112
        "real", // synonym for double
113
        "decimal",
114
        "dec", // synonym for decimal
115
        "numeric", // synonym for decimal
116
        "fixed", // synonym for decimal
117
        "bool", // synonym for tinyint
118
        "boolean", // synonym for tinyint
119
        // date and time types
120
        "date",
121
        "datetime",
122
        "timestamp",
123
        "time",
124
        "year",
125
        // string types
126
        "char",
127
        "nchar", // synonym for national char
128
        "national char",
129
        "varchar",
130
        "nvarchar", // synonym for national varchar
131
        "national varchar",
132
        "blob",
133
        "text",
134
        "tinyblob",
135
        "tinytext",
136
        "mediumblob",
137
        "mediumtext",
138
        "longblob",
139
        "longtext",
140
        "enum",
141
        "set",
142
        "binary",
143
        "varbinary",
144
        // json data type
145
        "json",
146
        // spatial data types
147
        "geometry",
148
        "point",
149
        "linestring",
150
        "polygon",
151
        "multipoint",
152
        "multilinestring",
153
        "multipolygon",
154
        "geometrycollection",
155
        // additional data types for mariadb
156
        "uuid",
157
        "inet4",
158
        "inet6",
159
    ]
160

161
    /**
162
     * Returns type of upsert supported by driver if any
163
     */
164
    supportedUpsertTypes: UpsertType[] = ["on-duplicate-key-update"]
52✔
165

166
    /**
167
     * Gets list of spatial column data types.
168
     */
169
    spatialTypes: ColumnType[] = [
52✔
170
        "geometry",
171
        "point",
172
        "linestring",
173
        "polygon",
174
        "multipoint",
175
        "multilinestring",
176
        "multipolygon",
177
        "geometrycollection",
178
    ]
179

180
    /**
181
     * Gets list of column data types that support length by a driver.
182
     */
183
    withLengthColumnTypes: ColumnType[] = [
52✔
184
        "char",
185
        "varchar",
186
        "nvarchar",
187
        "binary",
188
        "varbinary",
189
    ]
190

191
    /**
192
     * Gets list of column data types that support length by a driver.
193
     */
194
    withWidthColumnTypes: ColumnType[] = [
52✔
195
        "bit",
196
        "tinyint",
197
        "smallint",
198
        "mediumint",
199
        "int",
200
        "integer",
201
        "bigint",
202
    ]
203

204
    /**
205
     * Gets list of column data types that support precision by a driver.
206
     */
207
    withPrecisionColumnTypes: ColumnType[] = [
52✔
208
        "decimal",
209
        "dec",
210
        "numeric",
211
        "fixed",
212
        "float",
213
        "double",
214
        "double precision",
215
        "real",
216
        "time",
217
        "datetime",
218
        "timestamp",
219
    ]
220

221
    /**
222
     * Gets list of column data types that supports scale by a driver.
223
     */
224
    withScaleColumnTypes: ColumnType[] = [
52✔
225
        "decimal",
226
        "dec",
227
        "numeric",
228
        "fixed",
229
        "float",
230
        "double",
231
        "double precision",
232
        "real",
233
    ]
234

235
    /**
236
     * Gets list of column data types that supports UNSIGNED and ZEROFILL attributes.
237
     */
238
    unsignedAndZerofillTypes: ColumnType[] = [
52✔
239
        "int",
240
        "integer",
241
        "smallint",
242
        "tinyint",
243
        "mediumint",
244
        "bigint",
245
        "decimal",
246
        "dec",
247
        "numeric",
248
        "fixed",
249
        "float",
250
        "double",
251
        "double precision",
252
        "real",
253
    ]
254

255
    /**
256
     * ORM has special columns and we need to know what database column types should be for those columns.
257
     * Column types are driver dependant.
258
     */
259
    mappedDataTypes: MappedColumnTypes = {
52✔
260
        createDate: "datetime",
261
        createDatePrecision: 6,
262
        createDateDefault: "CURRENT_TIMESTAMP(6)",
263
        updateDate: "datetime",
264
        updateDatePrecision: 6,
265
        updateDateDefault: "CURRENT_TIMESTAMP(6)",
266
        deleteDate: "datetime",
267
        deleteDatePrecision: 6,
268
        deleteDateNullable: true,
269
        version: "int",
270
        treeLevel: "int",
271
        migrationId: "int",
272
        migrationName: "varchar",
273
        migrationTimestamp: "bigint",
274
        cacheId: "int",
275
        cacheIdentifier: "varchar",
276
        cacheTime: "bigint",
277
        cacheDuration: "int",
278
        cacheQuery: "text",
279
        cacheResult: "text",
280
        metadataType: "varchar",
281
        metadataDatabase: "varchar",
282
        metadataSchema: "varchar",
283
        metadataTable: "varchar",
284
        metadataName: "varchar",
285
        metadataValue: "text",
286
    }
287

288
    /**
289
     * Default values of length, precision and scale depends on column data type.
290
     * Used in the cases when length/precision/scale is not specified by user.
291
     */
292
    dataTypeDefaults: DataTypeDefaults = {
52✔
293
        varchar: { length: 255 },
294
        nvarchar: { length: 255 },
295
        "national varchar": { length: 255 },
296
        char: { length: 1 },
297
        binary: { length: 1 },
298
        varbinary: { length: 255 },
299
        decimal: { precision: 10, scale: 0 },
300
        dec: { precision: 10, scale: 0 },
301
        numeric: { precision: 10, scale: 0 },
302
        fixed: { precision: 10, scale: 0 },
303
        float: { precision: 12 },
304
        double: { precision: 22 },
305
        time: { precision: 0 },
306
        datetime: { precision: 0 },
307
        timestamp: { precision: 0 },
308
        bit: { width: 1 },
309
        int: { width: 11 },
310
        integer: { width: 11 },
311
        tinyint: { width: 4 },
312
        smallint: { width: 6 },
313
        mediumint: { width: 9 },
314
        bigint: { width: 20 },
315
    }
316

317
    /**
318
     * Max length allowed by MySQL for aliases.
319
     * @see https://dev.mysql.com/doc/refman/5.5/en/identifiers.html
320
     */
321
    maxAliasLength = 63
52✔
322

323
    cteCapabilities: CteCapabilities = {
52✔
324
        enabled: false,
325
        requiresRecursiveHint: true,
326
    }
327

328
    /**
329
     * Supported returning types
330
     */
331
    private readonly _isReturningSqlSupported: Record<ReturningType, boolean> =
52✔
332
        {
333
            delete: false,
334
            insert: false,
335
            update: false,
336
        }
337

338
    /** MariaDB supports uuid type for version 10.7.0 and up */
339
    private uuidColumnTypeSuported = false
52✔
340

341
    // -------------------------------------------------------------------------
342
    // Constructor
343
    // -------------------------------------------------------------------------
344

345
    constructor(connection: DataSource) {
346
        this.connection = connection
52✔
347
        this.options = {
52✔
348
            legacySpatialSupport: true,
349
            ...connection.options,
350
        } as MysqlConnectionOptions
351
        this.isReplicated = this.options.replication ? true : false
52!
352

353
        // load mysql package
354
        this.loadDependencies()
52✔
355

356
        this.database = DriverUtils.buildDriverOptions(
52✔
357
            this.options.replication
52!
358
                ? this.options.replication.master
359
                : this.options,
360
        ).database
361

362
        // validate options to make sure everything is set
363
        // todo: revisit validation with replication in mind
364
        // if (!(this.options.host || (this.options.extra && this.options.extra.socketPath)) && !this.options.socketPath)
365
        //     throw new DriverOptionNotSetError("socketPath and host");
366
        // if (!this.options.username)
367
        //     throw new DriverOptionNotSetError("username");
368
        // if (!this.options.database)
369
        //     throw new DriverOptionNotSetError("database");
370
        // todo: check what is going on when connection is setup without database and how to connect to a database then?
371
        // todo: provide options to auto-create a database if it does not exist yet
372
    }
373

374
    // -------------------------------------------------------------------------
375
    // Public Methods
376
    // -------------------------------------------------------------------------
377

378
    /**
379
     * Performs connection to the database.
380
     */
381
    async connect(): Promise<void> {
382
        if (this.options.replication) {
×
383
            this.poolCluster = this.mysql.createPoolCluster(
×
384
                this.options.replication,
385
            )
386
            this.options.replication.slaves.forEach((slave, index) => {
×
387
                this.poolCluster.add(
×
388
                    "SLAVE" + index,
389
                    this.createConnectionOptions(this.options, slave),
390
                )
391
            })
392
            this.poolCluster.add(
×
393
                "MASTER",
394
                this.createConnectionOptions(
395
                    this.options,
396
                    this.options.replication.master,
397
                ),
398
            )
399
        } else {
400
            this.pool = await this.createPool(
×
401
                this.createConnectionOptions(this.options, this.options),
402
            )
403
        }
404

405
        if (!this.database) {
×
406
            const queryRunner = this.createQueryRunner("master")
×
407

408
            this.database = await queryRunner.getCurrentDatabase()
×
409

410
            await queryRunner.release()
×
411
        }
412

413
        const queryRunner = this.createQueryRunner("master")
×
414
        this.version = await queryRunner.getVersion()
×
415
        await queryRunner.release()
×
416

417
        if (this.options.type === "mariadb") {
×
418
            if (VersionUtils.isGreaterOrEqual(this.version, "10.0.5")) {
×
419
                this._isReturningSqlSupported.delete = true
×
420
            }
421
            if (VersionUtils.isGreaterOrEqual(this.version, "10.5.0")) {
×
422
                this._isReturningSqlSupported.insert = true
×
423
            }
424
            if (VersionUtils.isGreaterOrEqual(this.version, "10.2.0")) {
×
425
                this.cteCapabilities.enabled = true
×
426
            }
427
            if (VersionUtils.isGreaterOrEqual(this.version, "10.7.0")) {
×
428
                this.uuidColumnTypeSuported = true
×
429
            }
430
        } else if (this.options.type === "mysql") {
×
431
            if (VersionUtils.isGreaterOrEqual(this.version, "8.0.0")) {
×
432
                this.cteCapabilities.enabled = true
×
433
            }
434
        }
435
    }
436

437
    /**
438
     * Makes any action after connection (e.g. create extensions in Postgres driver).
439
     */
440
    afterConnect(): Promise<void> {
441
        return Promise.resolve()
×
442
    }
443

444
    /**
445
     * Closes connection with the database.
446
     */
447
    async disconnect(): Promise<void> {
448
        if (!this.poolCluster && !this.pool)
×
449
            return Promise.reject(new ConnectionIsNotSetError("mysql"))
×
450

451
        if (this.poolCluster) {
×
452
            return new Promise<void>((ok, fail) => {
×
453
                this.poolCluster.end((err: any) => (err ? fail(err) : ok()))
×
454
                this.poolCluster = undefined
×
455
            })
456
        }
457
        if (this.pool) {
×
458
            return new Promise<void>((ok, fail) => {
×
459
                this.pool.end((err: any) => {
×
460
                    if (err) return fail(err)
×
461
                    this.pool = undefined
×
462
                    ok()
×
463
                })
464
            })
465
        }
466
    }
467

468
    /**
469
     * Creates a schema builder used to build and sync a schema.
470
     */
471
    createSchemaBuilder() {
472
        return new RdbmsSchemaBuilder(this.connection)
×
473
    }
474

475
    /**
476
     * Creates a query runner used to execute database queries.
477
     */
478
    createQueryRunner(mode: ReplicationMode) {
479
        return new MysqlQueryRunner(this, mode)
×
480
    }
481

482
    /**
483
     * Replaces parameters in the given sql with special escaping character
484
     * and an array of parameter names to be passed to a query.
485
     */
486
    escapeQueryWithParameters(
487
        sql: string,
488
        parameters: ObjectLiteral,
489
        nativeParameters: ObjectLiteral,
490
    ): [string, any[]] {
491
        const escapedParameters: any[] = Object.keys(nativeParameters).map(
×
492
            (key) => nativeParameters[key],
×
493
        )
494
        if (!parameters || !Object.keys(parameters).length)
×
495
            return [sql, escapedParameters]
×
496

497
        sql = sql.replace(
×
498
            /:(\.\.\.)?([A-Za-z0-9_.]+)/g,
499
            (full, isArray: string, key: string): string => {
500
                if (!parameters.hasOwnProperty(key)) {
×
501
                    return full
×
502
                }
503

504
                const value: any = parameters[key]
×
505

506
                if (isArray) {
×
507
                    return value
×
508
                        .map((v: any) => {
509
                            escapedParameters.push(v)
×
510
                            return this.createParameter(
×
511
                                key,
512
                                escapedParameters.length - 1,
513
                            )
514
                        })
515
                        .join(", ")
516
                }
517

518
                if (typeof value === "function") {
×
519
                    return value()
×
520
                }
521

522
                escapedParameters.push(value)
×
523
                return this.createParameter(key, escapedParameters.length - 1)
×
524
            },
525
        ) // todo: make replace only in value statements, otherwise problems
526
        return [sql, escapedParameters]
×
527
    }
528

529
    /**
530
     * Escapes a column name.
531
     */
532
    escape(columnName: string): string {
533
        return "`" + columnName + "`"
×
534
    }
535

536
    /**
537
     * Build full table name with database name, schema name and table name.
538
     * E.g. myDB.mySchema.myTable
539
     */
540
    buildTableName(
541
        tableName: string,
542
        schema?: string,
543
        database?: string,
544
    ): string {
545
        const tablePath = [tableName]
72✔
546

547
        if (database) {
72!
548
            tablePath.unshift(database)
×
549
        }
550

551
        return tablePath.join(".")
72✔
552
    }
553

554
    /**
555
     * Parse a target table name or other types and return a normalized table definition.
556
     */
557
    parseTableName(
558
        target: EntityMetadata | Table | View | TableForeignKey | string,
559
    ): { database?: string; schema?: string; tableName: string } {
560
        const driverDatabase = this.database
×
561
        const driverSchema = undefined
×
562

563
        if (InstanceChecker.isTable(target) || InstanceChecker.isView(target)) {
×
564
            const parsed = this.parseTableName(target.name)
×
565

566
            return {
×
567
                database: target.database || parsed.database || driverDatabase,
×
568
                schema: target.schema || parsed.schema || driverSchema,
×
569
                tableName: parsed.tableName,
570
            }
571
        }
572

573
        if (InstanceChecker.isTableForeignKey(target)) {
×
574
            const parsed = this.parseTableName(target.referencedTableName)
×
575

576
            return {
×
577
                database:
578
                    target.referencedDatabase ||
×
579
                    parsed.database ||
580
                    driverDatabase,
581
                schema:
582
                    target.referencedSchema || parsed.schema || driverSchema,
×
583
                tableName: parsed.tableName,
584
            }
585
        }
586

587
        if (InstanceChecker.isEntityMetadata(target)) {
×
588
            // EntityMetadata tableName is never a path
589

590
            return {
×
591
                database: target.database || driverDatabase,
×
592
                schema: target.schema || driverSchema,
×
593
                tableName: target.tableName,
594
            }
595
        }
596

597
        const parts = target.split(".")
×
598

599
        return {
×
600
            database:
601
                (parts.length > 1 ? parts[0] : undefined) || driverDatabase,
×
602
            schema: driverSchema,
603
            tableName: parts.length > 1 ? parts[1] : parts[0],
×
604
        }
605
    }
606

607
    /**
608
     * Prepares given value to a value to be persisted, based on its column type and metadata.
609
     */
610
    preparePersistentValue(value: any, columnMetadata: ColumnMetadata): any {
611
        if (columnMetadata.transformer)
×
612
            value = ApplyValueTransformers.transformTo(
×
613
                columnMetadata.transformer,
614
                value,
615
            )
616

617
        if (value === null || value === undefined) return value
×
618

619
        if (columnMetadata.type === Boolean) {
×
620
            return value === true ? 1 : 0
×
621
        } else if (columnMetadata.type === "date") {
×
622
            return DateUtils.mixedDateToDateString(value)
×
623
        } else if (columnMetadata.type === "time") {
×
624
            return DateUtils.mixedDateToTimeString(value)
×
625
        } else if (columnMetadata.type === "json") {
×
626
            return JSON.stringify(value)
×
627
        } else if (
×
628
            columnMetadata.type === "timestamp" ||
×
629
            columnMetadata.type === "datetime" ||
630
            columnMetadata.type === Date
631
        ) {
632
            return DateUtils.mixedDateToDate(value)
×
633
        } else if (columnMetadata.type === "simple-array") {
×
634
            return DateUtils.simpleArrayToString(value)
×
635
        } else if (columnMetadata.type === "simple-json") {
×
636
            return DateUtils.simpleJsonToString(value)
×
637
        } else if (
×
638
            columnMetadata.type === "enum" ||
×
639
            columnMetadata.type === "simple-enum"
640
        ) {
641
            return "" + value
×
642
        } else if (columnMetadata.type === "set") {
×
643
            return DateUtils.simpleArrayToString(value)
×
644
        } else if (columnMetadata.type === Number) {
×
645
            // convert to number if number
646
            value = !isNaN(+value) ? parseInt(value) : value
×
647
        }
648

649
        return value
×
650
    }
651

652
    /**
653
     * Prepares given value to a value to be persisted, based on its column type or metadata.
654
     */
655
    prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any {
656
        if (value === null || value === undefined)
×
657
            return columnMetadata.transformer
×
658
                ? ApplyValueTransformers.transformFrom(
659
                      columnMetadata.transformer,
660
                      value,
661
                  )
662
                : value
663

664
        if (
×
665
            columnMetadata.type === Boolean ||
×
666
            columnMetadata.type === "bool" ||
667
            columnMetadata.type === "boolean"
668
        ) {
669
            value = value ? true : false
×
670
        } else if (
×
671
            columnMetadata.type === "datetime" ||
×
672
            columnMetadata.type === Date
673
        ) {
674
            value = DateUtils.normalizeHydratedDate(value)
×
675
        } else if (columnMetadata.type === "date") {
×
676
            value = DateUtils.mixedDateToDateString(value)
×
677
        } else if (columnMetadata.type === "json") {
×
678
            value = typeof value === "string" ? JSON.parse(value) : value
×
679
        } else if (columnMetadata.type === "time") {
×
680
            value = DateUtils.mixedTimeToString(value)
×
681
        } else if (columnMetadata.type === "simple-array") {
×
682
            value = DateUtils.stringToSimpleArray(value)
×
683
        } else if (columnMetadata.type === "simple-json") {
×
684
            value = DateUtils.stringToSimpleJson(value)
×
685
        } else if (
×
686
            (columnMetadata.type === "enum" ||
×
687
                columnMetadata.type === "simple-enum") &&
688
            columnMetadata.enum &&
689
            !isNaN(value) &&
690
            columnMetadata.enum.indexOf(parseInt(value)) >= 0
691
        ) {
692
            // convert to number if that exists in possible enum options
693
            value = parseInt(value)
×
694
        } else if (columnMetadata.type === "set") {
×
695
            value = DateUtils.stringToSimpleArray(value)
×
696
        } else if (columnMetadata.type === Number) {
×
697
            // convert to number if number
698
            value = !isNaN(+value) ? parseInt(value) : value
×
699
        }
700

701
        if (columnMetadata.transformer)
×
702
            value = ApplyValueTransformers.transformFrom(
×
703
                columnMetadata.transformer,
704
                value,
705
            )
706

707
        return value
×
708
    }
709

710
    /**
711
     * Creates a database type from a given column metadata.
712
     */
713
    normalizeType(column: {
714
        type: ColumnType
715
        length?: number | string
716
        precision?: number | null
717
        scale?: number
718
    }): string {
719
        if (column.type === Number || column.type === "integer") {
220✔
720
            return "int"
156✔
721
        } else if (column.type === String) {
64!
722
            return "varchar"
64✔
723
        } else if (column.type === Date) {
×
724
            return "datetime"
×
725
        } else if ((column.type as any) === Buffer) {
×
726
            return "blob"
×
727
        } else if (column.type === Boolean) {
×
728
            return "tinyint"
×
729
        } else if (column.type === "uuid" && !this.uuidColumnTypeSuported) {
×
730
            return "varchar"
×
731
        } else if (
×
732
            column.type === "json" &&
×
733
            this.options.type === "mariadb" &&
734
            !VersionUtils.isGreaterOrEqual(this.version, "10.4.3")
735
        ) {
736
            /*
737
             * MariaDB implements this as a LONGTEXT rather, as the JSON data type contradicts the SQL standard,
738
             * and MariaDB's benchmarks indicate that performance is at least equivalent.
739
             *
740
             * @see https://mariadb.com/kb/en/json-data-type/
741
             * if Version is 10.4.3 or greater, JSON is an alias for longtext and an automatic check_json(column) constraint is added
742
             */
743
            return "longtext"
×
744
        } else if (
×
745
            column.type === "simple-array" ||
×
746
            column.type === "simple-json"
747
        ) {
748
            return "text"
×
749
        } else if (column.type === "simple-enum") {
×
750
            return "enum"
×
751
        } else if (
×
752
            column.type === "double precision" ||
×
753
            column.type === "real"
754
        ) {
755
            return "double"
×
756
        } else if (
×
757
            column.type === "dec" ||
×
758
            column.type === "numeric" ||
759
            column.type === "fixed"
760
        ) {
761
            return "decimal"
×
762
        } else if (column.type === "bool" || column.type === "boolean") {
×
763
            return "tinyint"
×
764
        } else if (
×
765
            column.type === "nvarchar" ||
×
766
            column.type === "national varchar"
767
        ) {
768
            return "varchar"
×
769
        } else if (column.type === "nchar" || column.type === "national char") {
×
770
            return "char"
×
771
        } else {
772
            return (column.type as string) || ""
×
773
        }
774
    }
775

776
    /**
777
     * Normalizes "default" value of the column.
778
     */
779
    normalizeDefault(columnMetadata: ColumnMetadata): string | undefined {
780
        const defaultValue = columnMetadata.default
×
781

782
        if (defaultValue === null) {
×
783
            return undefined
×
784
        }
785

786
        if (
×
787
            (columnMetadata.type === "enum" ||
×
788
                columnMetadata.type === "simple-enum" ||
789
                typeof defaultValue === "string") &&
790
            defaultValue !== undefined
791
        ) {
792
            return `'${defaultValue}'`
×
793
        }
794

795
        if (columnMetadata.type === "set" && defaultValue !== undefined) {
×
796
            return `'${DateUtils.simpleArrayToString(defaultValue)}'`
×
797
        }
798

799
        if (typeof defaultValue === "number") {
×
800
            return `'${defaultValue.toFixed(columnMetadata.scale)}'`
×
801
        }
802

803
        if (typeof defaultValue === "boolean") {
×
804
            return defaultValue ? "1" : "0"
×
805
        }
806

807
        if (typeof defaultValue === "function") {
×
808
            const value = defaultValue()
×
809
            return this.normalizeDatetimeFunction(value)
×
810
        }
811

812
        if (defaultValue === undefined) {
×
813
            return undefined
×
814
        }
815

816
        return `${defaultValue}`
×
817
    }
818

819
    /**
820
     * Normalizes "isUnique" value of the column.
821
     */
822
    normalizeIsUnique(column: ColumnMetadata): boolean {
823
        return column.entityMetadata.indices.some(
×
824
            (idx) =>
825
                idx.isUnique &&
×
826
                idx.columns.length === 1 &&
827
                idx.columns[0] === column,
828
        )
829
    }
830

831
    /**
832
     * Returns default column lengths, which is required on column creation.
833
     */
834
    getColumnLength(column: ColumnMetadata | TableColumn): string {
835
        if (column.length) return column.length.toString()
×
836

837
        /**
838
         * fix https://github.com/typeorm/typeorm/issues/1139
839
         * note that if the db did support uuid column type it wouldn't have been defaulted to varchar
840
         */
841
        if (
×
842
            column.generationStrategy === "uuid" &&
×
843
            !this.uuidColumnTypeSuported
844
        )
845
            return "36"
×
846

847
        switch (column.type) {
×
848
            case String:
849
            case "varchar":
850
            case "nvarchar":
851
            case "national varchar":
852
                return "255"
×
853
            case "varbinary":
854
                return "255"
×
855
            default:
856
                return ""
×
857
        }
858
    }
859

860
    /**
861
     * Creates column type definition including length, precision and scale
862
     */
863
    createFullType(column: TableColumn): string {
864
        let type = column.type
×
865

866
        // used 'getColumnLength()' method, because MySQL requires column length for `varchar`, `nvarchar` and `varbinary` data types
867
        if (this.getColumnLength(column)) {
×
868
            type += `(${this.getColumnLength(column)})`
×
869
        } else if (column.width) {
×
870
            type += `(${column.width})`
×
871
        } else if (
×
872
            column.precision !== null &&
×
873
            column.precision !== undefined &&
874
            column.scale !== null &&
875
            column.scale !== undefined
876
        ) {
877
            type += `(${column.precision},${column.scale})`
×
878
        } else if (
×
879
            column.precision !== null &&
×
880
            column.precision !== undefined
881
        ) {
882
            type += `(${column.precision})`
×
883
        }
884

885
        if (column.isArray) type += " array"
×
886

887
        return type
×
888
    }
889

890
    /**
891
     * Obtains a new database connection to a master server.
892
     * Used for replication.
893
     * If replication is not setup then returns default connection's database connection.
894
     */
895
    obtainMasterConnection(): Promise<any> {
896
        return new Promise<any>((ok, fail) => {
×
897
            if (this.poolCluster) {
×
898
                this.poolCluster.getConnection(
×
899
                    "MASTER",
900
                    (err: any, dbConnection: any) => {
901
                        err
×
902
                            ? fail(err)
903
                            : ok(this.prepareDbConnection(dbConnection))
904
                    },
905
                )
906
            } else if (this.pool) {
×
907
                this.pool.getConnection((err: any, dbConnection: any) => {
×
908
                    err ? fail(err) : ok(this.prepareDbConnection(dbConnection))
×
909
                })
910
            } else {
911
                fail(
×
912
                    new TypeORMError(
913
                        `Connection is not established with mysql database`,
914
                    ),
915
                )
916
            }
917
        })
918
    }
919

920
    /**
921
     * Obtains a new database connection to a slave server.
922
     * Used for replication.
923
     * If replication is not setup then returns master (default) connection's database connection.
924
     */
925
    obtainSlaveConnection(): Promise<any> {
926
        if (!this.poolCluster) return this.obtainMasterConnection()
×
927

928
        return new Promise<any>((ok, fail) => {
×
929
            this.poolCluster.getConnection(
×
930
                "SLAVE*",
931
                (err: any, dbConnection: any) => {
932
                    err ? fail(err) : ok(this.prepareDbConnection(dbConnection))
×
933
                },
934
            )
935
        })
936
    }
937

938
    /**
939
     * Creates generated map of values generated or returned by database after INSERT query.
940
     */
941
    createGeneratedMap(
942
        metadata: EntityMetadata,
943
        insertResult: any,
944
        entityIndex: number,
945
    ) {
946
        if (!insertResult) {
×
947
            return undefined
×
948
        }
949

950
        if (insertResult.insertId === undefined) {
×
951
            return Object.keys(insertResult).reduce((map, key) => {
×
952
                const column = metadata.findColumnWithDatabaseName(key)
×
953
                if (column) {
×
954
                    OrmUtils.mergeDeep(
×
955
                        map,
956
                        column.createValueMap(insertResult[key]),
957
                    )
958
                    // OrmUtils.mergeDeep(map, column.createValueMap(this.prepareHydratedValue(insertResult[key], column))); // TODO: probably should be like there, but fails on enums, fix later
959
                }
960
                return map
×
961
            }, {} as ObjectLiteral)
962
        }
963

964
        const generatedMap = metadata.generatedColumns.reduce(
×
965
            (map, generatedColumn) => {
966
                let value: any
967
                if (
×
968
                    generatedColumn.generationStrategy === "increment" &&
×
969
                    insertResult.insertId
970
                ) {
971
                    // NOTE: When multiple rows is inserted by a single INSERT statement,
972
                    // `insertId` is the value generated for the first inserted row only.
973
                    value = insertResult.insertId + entityIndex
×
974
                    // } else if (generatedColumn.generationStrategy === "uuid") {
975
                    //     console.log("getting db value:", generatedColumn.databaseName);
976
                    //     value = generatedColumn.getEntityValue(uuidMap);
977
                }
978

979
                return OrmUtils.mergeDeep(
×
980
                    map,
981
                    generatedColumn.createValueMap(value),
982
                )
983
            },
984
            {} as ObjectLiteral,
985
        )
986

987
        return Object.keys(generatedMap).length > 0 ? generatedMap : undefined
×
988
    }
989

990
    /**
991
     * Differentiate columns of this table and columns from the given column metadatas columns
992
     * and returns only changed.
993
     */
994
    findChangedColumns(
995
        tableColumns: TableColumn[],
996
        columnMetadatas: ColumnMetadata[],
997
    ): ColumnMetadata[] {
998
        return columnMetadatas.filter((columnMetadata) => {
×
999
            const tableColumn = tableColumns.find(
×
1000
                (c) => c.name === columnMetadata.databaseName,
×
1001
            )
1002
            if (!tableColumn) return false // we don't need new columns, we only need exist and changed
×
1003

1004
            const isColumnChanged =
1005
                tableColumn.name !== columnMetadata.databaseName ||
×
1006
                this.isColumnDataTypeChanged(tableColumn, columnMetadata) ||
1007
                tableColumn.length !== this.getColumnLength(columnMetadata) ||
1008
                tableColumn.width !== columnMetadata.width ||
1009
                (columnMetadata.precision !== undefined &&
1010
                    tableColumn.precision !== columnMetadata.precision) ||
1011
                (columnMetadata.scale !== undefined &&
1012
                    tableColumn.scale !== columnMetadata.scale) ||
1013
                tableColumn.zerofill !== columnMetadata.zerofill ||
1014
                tableColumn.unsigned !== columnMetadata.unsigned ||
1015
                tableColumn.asExpression !== columnMetadata.asExpression ||
1016
                tableColumn.generatedType !== columnMetadata.generatedType ||
1017
                tableColumn.comment !==
1018
                    this.escapeComment(columnMetadata.comment) ||
1019
                !this.compareDefaultValues(
1020
                    this.normalizeDefault(columnMetadata),
1021
                    tableColumn.default,
1022
                ) ||
1023
                (tableColumn.enum &&
1024
                    columnMetadata.enum &&
1025
                    !OrmUtils.isArraysEqual(
1026
                        tableColumn.enum,
1027
                        columnMetadata.enum.map((val) => val + ""),
×
1028
                    )) ||
1029
                tableColumn.onUpdate !==
1030
                    this.normalizeDatetimeFunction(columnMetadata.onUpdate) ||
1031
                tableColumn.isPrimary !== columnMetadata.isPrimary ||
1032
                !this.compareNullableValues(columnMetadata, tableColumn) ||
1033
                tableColumn.isUnique !==
1034
                    this.normalizeIsUnique(columnMetadata) ||
1035
                (columnMetadata.generationStrategy !== "uuid" &&
1036
                    tableColumn.isGenerated !== columnMetadata.isGenerated)
1037

1038
            // DEBUG SECTION
1039
            // if (isColumnChanged) {
1040
            //     console.log("table:", columnMetadata.entityMetadata.tableName)
1041
            //     console.log(
1042
            //         "name:",
1043
            //         tableColumn.name,
1044
            //         columnMetadata.databaseName,
1045
            //     )
1046
            //     console.log(
1047
            //         "type:",
1048
            //         tableColumn.type,
1049
            //         this.normalizeType(columnMetadata),
1050
            //     )
1051
            //     console.log(
1052
            //         "length:",
1053
            //         tableColumn.length,
1054
            //         columnMetadata.length,
1055
            //     )
1056
            //     console.log("width:", tableColumn.width, columnMetadata.width)
1057
            //     console.log(
1058
            //         "precision:",
1059
            //         tableColumn.precision,
1060
            //         columnMetadata.precision,
1061
            //     )
1062
            //     console.log("scale:", tableColumn.scale, columnMetadata.scale)
1063
            //     console.log(
1064
            //         "zerofill:",
1065
            //         tableColumn.zerofill,
1066
            //         columnMetadata.zerofill,
1067
            //     )
1068
            //     console.log(
1069
            //         "unsigned:",
1070
            //         tableColumn.unsigned,
1071
            //         columnMetadata.unsigned,
1072
            //     )
1073
            //     console.log(
1074
            //         "asExpression:",
1075
            //         tableColumn.asExpression,
1076
            //         columnMetadata.asExpression,
1077
            //     )
1078
            //     console.log(
1079
            //         "generatedType:",
1080
            //         tableColumn.generatedType,
1081
            //         columnMetadata.generatedType,
1082
            //     )
1083
            //     console.log(
1084
            //         "comment:",
1085
            //         tableColumn.comment,
1086
            //         this.escapeComment(columnMetadata.comment),
1087
            //     )
1088
            //     console.log(
1089
            //         "default:",
1090
            //         tableColumn.default,
1091
            //         this.normalizeDefault(columnMetadata),
1092
            //     )
1093
            //     console.log("enum:", tableColumn.enum, columnMetadata.enum)
1094
            //     console.log(
1095
            //         "default changed:",
1096
            //         !this.compareDefaultValues(
1097
            //             this.normalizeDefault(columnMetadata),
1098
            //             tableColumn.default,
1099
            //         ),
1100
            //     )
1101
            //     console.log(
1102
            //         "isPrimary:",
1103
            //         tableColumn.isPrimary,
1104
            //         columnMetadata.isPrimary,
1105
            //     )
1106
            //     console.log(
1107
            //         "isNullable changed:",
1108
            //         !this.compareNullableValues(columnMetadata, tableColumn),
1109
            //     )
1110
            //     console.log(
1111
            //         "isUnique:",
1112
            //         tableColumn.isUnique,
1113
            //         this.normalizeIsUnique(columnMetadata),
1114
            //     )
1115
            //     console.log(
1116
            //         "isGenerated:",
1117
            //         tableColumn.isGenerated,
1118
            //         columnMetadata.isGenerated,
1119
            //     )
1120
            //     console.log(
1121
            //         columnMetadata.generationStrategy !== "uuid" &&
1122
            //             tableColumn.isGenerated !== columnMetadata.isGenerated,
1123
            //     )
1124
            //     console.log("==========================================")
1125
            // }
1126

1127
            return isColumnChanged
×
1128
        })
1129
    }
1130

1131
    /**
1132
     * Returns true if driver supports RETURNING / OUTPUT statement.
1133
     */
1134
    isReturningSqlSupported(returningType: ReturningType): boolean {
1135
        return this._isReturningSqlSupported[returningType]
×
1136
    }
1137

1138
    /**
1139
     * Returns true if driver supports uuid values generation on its own.
1140
     */
1141
    isUUIDGenerationSupported(): boolean {
1142
        return false
×
1143
    }
1144

1145
    /**
1146
     * Returns true if driver supports fulltext indices.
1147
     */
1148
    isFullTextColumnTypeSupported(): boolean {
1149
        return true
×
1150
    }
1151

1152
    /**
1153
     * Creates an escaped parameter.
1154
     */
1155
    createParameter(parameterName: string, index: number): string {
1156
        return "?"
×
1157
    }
1158

1159
    // -------------------------------------------------------------------------
1160
    // Protected Methods
1161
    // -------------------------------------------------------------------------
1162

1163
    /**
1164
     * Loads all driver dependencies.
1165
     */
1166
    protected loadDependencies(): void {
1167
        const connectorPackage = this.options.connectorPackage ?? "mysql"
52✔
1168
        const fallbackConnectorPackage =
1169
            connectorPackage === "mysql"
52!
1170
                ? ("mysql2" as const)
1171
                : ("mysql" as const)
1172
        try {
52✔
1173
            // try to load first supported package
1174
            const mysql =
1175
                this.options.driver || PlatformTools.load(connectorPackage)
52✔
1176
            this.mysql = mysql
52✔
1177
            /*
1178
             * Some frameworks (such as Jest) may mess up Node's require cache and provide garbage for the 'mysql' module
1179
             * if it was not installed. We check that the object we got actually contains something otherwise we treat
1180
             * it as if the `require` call failed.
1181
             *
1182
             * @see https://github.com/typeorm/typeorm/issues/1373
1183
             */
1184
            if (Object.keys(this.mysql).length === 0) {
52!
1185
                throw new TypeORMError(
×
1186
                    `'${connectorPackage}' was found but it is empty. Falling back to '${fallbackConnectorPackage}'.`,
1187
                )
1188
            }
1189
        } catch (e) {
1190
            try {
×
1191
                this.mysql = PlatformTools.load(fallbackConnectorPackage) // try to load second supported package
×
1192
            } catch (e) {
1193
                throw new DriverPackageNotInstalledError(
×
1194
                    "Mysql",
1195
                    connectorPackage,
1196
                )
1197
            }
1198
        }
1199
    }
1200

1201
    /**
1202
     * Creates a new connection pool for a given database credentials.
1203
     */
1204
    protected createConnectionOptions(
1205
        options: MysqlConnectionOptions,
1206
        credentials: MysqlConnectionCredentialsOptions,
1207
    ): Promise<any> {
1208
        credentials = Object.assign(
×
1209
            {},
1210
            credentials,
1211
            DriverUtils.buildDriverOptions(credentials),
1212
        ) // todo: do it better way
1213

1214
        // build connection options for the driver
1215
        return Object.assign(
×
1216
            {},
1217
            {
1218
                charset: options.charset,
1219
                timezone: options.timezone,
1220
                connectTimeout: options.connectTimeout,
1221
                insecureAuth: options.insecureAuth,
1222
                supportBigNumbers:
1223
                    options.supportBigNumbers !== undefined
×
1224
                        ? options.supportBigNumbers
1225
                        : true,
1226
                bigNumberStrings:
1227
                    options.bigNumberStrings !== undefined
×
1228
                        ? options.bigNumberStrings
1229
                        : true,
1230
                dateStrings: options.dateStrings,
1231
                debug: options.debug,
1232
                trace: options.trace,
1233
                multipleStatements: options.multipleStatements,
1234
                flags: options.flags,
1235
            },
1236
            {
1237
                host: credentials.host,
1238
                user: credentials.username,
1239
                password: credentials.password,
1240
                database: credentials.database,
1241
                port: credentials.port,
1242
                ssl: options.ssl,
1243
                socketPath: credentials.socketPath,
1244
            },
1245
            options.acquireTimeout === undefined
×
1246
                ? {}
1247
                : { acquireTimeout: options.acquireTimeout },
1248
            { connectionLimit: options.poolSize },
1249
            options.extra || {},
×
1250
        )
1251
    }
1252

1253
    /**
1254
     * Creates a new connection pool for a given database credentials.
1255
     */
1256
    protected createPool(connectionOptions: any): Promise<any> {
1257
        // create a connection pool
1258
        const pool = this.mysql.createPool(connectionOptions)
×
1259

1260
        // make sure connection is working fine
1261
        return new Promise<void>((ok, fail) => {
×
1262
            // (issue #610) we make first connection to database to make sure if connection credentials are wrong
1263
            // we give error before calling any other method that creates actual query runner
1264
            pool.getConnection((err: any, connection: any) => {
×
1265
                if (err) return pool.end(() => fail(err))
×
1266

1267
                connection.release()
×
1268
                ok(pool)
×
1269
            })
1270
        })
1271
    }
1272

1273
    /**
1274
     * Attaches all required base handlers to a database connection, such as the unhandled error handler.
1275
     */
1276
    private prepareDbConnection(connection: any): any {
1277
        const { logger } = this.connection
×
1278
        /*
1279
         * Attaching an error handler to connection errors is essential, as, otherwise, errors raised will go unhandled and
1280
         * cause the hosting app to crash.
1281
         */
1282
        if (connection.listeners("error").length === 0) {
×
1283
            connection.on("error", (error: any) =>
×
1284
                logger.log(
×
1285
                    "warn",
1286
                    `MySQL connection raised an error. ${error}`,
1287
                ),
1288
            )
1289
        }
1290
        return connection
×
1291
    }
1292

1293
    /**
1294
     * Checks if "DEFAULT" values in the column metadata and in the database are equal.
1295
     */
1296
    protected compareDefaultValues(
1297
        columnMetadataValue: string | undefined,
1298
        databaseValue: string | undefined,
1299
    ): boolean {
1300
        if (
×
1301
            typeof columnMetadataValue === "string" &&
×
1302
            typeof databaseValue === "string"
1303
        ) {
1304
            // we need to cut out "'" because in mysql we can understand returned value is a string or a function
1305
            // as result compare cannot understand if default is really changed or not
1306
            columnMetadataValue = columnMetadataValue.replace(/^'+|'+$/g, "")
×
1307
            databaseValue = databaseValue.replace(/^'+|'+$/g, "")
×
1308
        }
1309

1310
        return columnMetadataValue === databaseValue
×
1311
    }
1312

1313
    compareNullableValues(
1314
        columnMetadata: ColumnMetadata,
1315
        tableColumn: TableColumn,
1316
    ): boolean {
1317
        // MariaDB does not support NULL/NOT NULL expressions for generated columns
1318
        const isMariaDb = this.options.type === "mariadb"
×
1319
        if (isMariaDb && columnMetadata.generatedType) {
×
1320
            return true
×
1321
        }
1322

1323
        return columnMetadata.isNullable === tableColumn.isNullable
×
1324
    }
1325

1326
    /**
1327
     * If parameter is a datetime function, e.g. "CURRENT_TIMESTAMP", normalizes it.
1328
     * Otherwise returns original input.
1329
     */
1330
    protected normalizeDatetimeFunction(value?: string) {
1331
        if (!value) return value
×
1332

1333
        // check if input is datetime function
1334
        const isDatetimeFunction =
1335
            value.toUpperCase().indexOf("CURRENT_TIMESTAMP") !== -1 ||
×
1336
            value.toUpperCase().indexOf("NOW") !== -1
1337

1338
        if (isDatetimeFunction) {
×
1339
            // extract precision, e.g. "(3)"
1340
            const precision = value.match(/\(\d+\)/)
×
1341
            if (this.options.type === "mariadb") {
×
1342
                return precision
×
1343
                    ? `CURRENT_TIMESTAMP${precision[0]}`
1344
                    : "CURRENT_TIMESTAMP()"
1345
            } else {
1346
                return precision
×
1347
                    ? `CURRENT_TIMESTAMP${precision[0]}`
1348
                    : "CURRENT_TIMESTAMP"
1349
            }
1350
        } else {
1351
            return value
×
1352
        }
1353
    }
1354

1355
    /**
1356
     * Escapes a given comment.
1357
     */
1358
    protected escapeComment(comment?: string) {
1359
        if (!comment) return comment
×
1360

1361
        comment = comment.replace(/\u0000/g, "") // Null bytes aren't allowed in comments
×
1362

1363
        return comment
×
1364
    }
1365

1366
    /**
1367
     * A helper to check if column data types have changed
1368
     * This can be used to manage checking any types the
1369
     * database may alias
1370
     */
1371
    private isColumnDataTypeChanged(
1372
        tableColumn: TableColumn,
1373
        columnMetadata: ColumnMetadata,
1374
    ) {
1375
        // this is an exception for mariadb versions where json is an alias for longtext
1376
        if (
×
1377
            this.normalizeType(columnMetadata) === "json" &&
×
1378
            tableColumn.type.toLowerCase() === "longtext"
1379
        )
1380
            return false
×
1381
        return tableColumn.type !== this.normalizeType(columnMetadata)
×
1382
    }
1383
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc