• 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

87.79
/src/query-runner/BaseQueryRunner.ts
1
import { PostgresConnectionOptions } from "../driver/postgres/PostgresConnectionOptions"
2
import { Query } from "../driver/Query"
4✔
3
import { SqlInMemory } from "../driver/SqlInMemory"
4✔
4
import { SqlServerConnectionOptions } from "../driver/sqlserver/SqlServerConnectionOptions"
5
import { TableIndex } from "../schema-builder/table/TableIndex"
6
import { View } from "../schema-builder/view/View"
7
import { DataSource } from "../data-source/DataSource"
8
import { Table } from "../schema-builder/table/Table"
9
import { EntityManager } from "../entity-manager/EntityManager"
10
import { TableColumn } from "../schema-builder/table/TableColumn"
11
import { Broadcaster } from "../subscriber/Broadcaster"
12
import { ReplicationMode } from "../driver/types/ReplicationMode"
13
import { TypeORMError } from "../error/TypeORMError"
4✔
14
import { EntityMetadata } from "../metadata/EntityMetadata"
15
import { TableForeignKey } from "../schema-builder/table/TableForeignKey"
16
import { OrmUtils } from "../util/OrmUtils"
4✔
17
import { MetadataTableType } from "../driver/types/MetadataTableType"
18
import { InstanceChecker } from "../util/InstanceChecker"
4✔
19

20
export abstract class BaseQueryRunner {
4✔
21
    // -------------------------------------------------------------------------
22
    // Public Properties
23
    // -------------------------------------------------------------------------
24

25
    /**
26
     * Connection used by this query runner.
27
     */
28
    connection: DataSource
29

30
    /**
31
     * Entity manager working only with current query runner.
32
     */
33
    manager: EntityManager
34

35
    /**
36
     * Indicates if connection for this query runner is released.
37
     * Once its released, query runner cannot run queries anymore.
38
     */
39
    isReleased = false
12,464✔
40

41
    /**
42
     * Indicates if transaction is in progress.
43
     */
44
    isTransactionActive = false
12,464✔
45

46
    /**
47
     * Stores temporarily user data.
48
     * Useful for sharing data with subscribers.
49
     */
50
    data = {}
12,464✔
51

52
    /**
53
     * All synchronized tables in the database.
54
     */
55
    loadedTables: Table[] = []
12,464✔
56

57
    /**
58
     * All synchronized views in the database.
59
     */
60
    loadedViews: View[] = []
12,464✔
61

62
    /**
63
     * Broadcaster used on this query runner to broadcast entity events.
64
     */
65
    broadcaster: Broadcaster
66

67
    // -------------------------------------------------------------------------
68
    // Protected Properties
69
    // -------------------------------------------------------------------------
70

71
    /**
72
     * Real database connection from a connection pool used to perform queries.
73
     */
74
    protected databaseConnection: any
75

76
    /**
77
     * Indicates if special query runner mode in which sql queries won't be executed is enabled.
78
     */
79
    protected sqlMemoryMode: boolean = false
12,464✔
80

81
    /**
82
     * Sql-s stored if "sql in memory" mode is enabled.
83
     */
84
    protected sqlInMemory: SqlInMemory = new SqlInMemory()
12,464✔
85

86
    /**
87
     * Mode in which query runner executes.
88
     * Used for replication.
89
     * If replication is not setup its value is ignored.
90
     */
91
    protected mode: ReplicationMode
92

93
    /**
94
     * current depth of transaction.
95
     * for transactionDepth > 0 will use SAVEPOINT to start and commit/rollback transaction blocks
96
     */
97
    protected transactionDepth = 0
12,464✔
98

99
    private cachedTablePaths: Record<string, string> = {}
12,464✔
100

101
    // -------------------------------------------------------------------------
102
    // Public Abstract Methods
103
    // -------------------------------------------------------------------------
104

105
    /**
106
     * Executes a given SQL query.
107
     */
108
    abstract query(
109
        query: string,
110
        parameters?: any[],
111
        useStructuredResult?: boolean,
112
    ): Promise<any>
113

114
    // -------------------------------------------------------------------------
115
    // Protected Abstract Methods
116
    // -------------------------------------------------------------------------
117

118
    protected abstract loadTables(tablePaths?: string[]): Promise<Table[]>
119

120
    protected abstract loadViews(tablePaths?: string[]): Promise<View[]>
121

122
    // -------------------------------------------------------------------------
123
    // Public Methods
124
    // -------------------------------------------------------------------------
125

126
    /**
127
     * Called before migrations are run.
128
     */
129
    async beforeMigration(): Promise<void> {
130
        // Do nothing
131
    }
132

133
    /**
134
     * Called after migrations are run.
135
     */
136
    async afterMigration(): Promise<void> {
137
        // Do nothing
138
    }
139

140
    /**
141
     * Loads given table's data from the database.
142
     */
143
    async getTable(tablePath: string): Promise<Table | undefined> {
144
        this.loadedTables = await this.loadTables([tablePath])
804✔
145
        return this.loadedTables.length > 0 ? this.loadedTables[0] : undefined
804✔
146
    }
147

148
    /**
149
     * Loads all tables (with given names) from the database.
150
     */
151
    async getTables(tableNames?: string[]): Promise<Table[]> {
152
        if (!tableNames) {
5,241✔
153
            // Don't cache in this case.
154
            // This is the new case & isn't used anywhere else anyway.
155
            return await this.loadTables(tableNames)
2✔
156
        }
157

158
        this.loadedTables = await this.loadTables(tableNames)
5,239✔
159
        return this.loadedTables
5,239✔
160
    }
161

162
    /**
163
     * Loads given view's data from the database.
164
     */
165
    async getView(viewPath: string): Promise<View | undefined> {
166
        this.loadedViews = await this.loadViews([viewPath])
21✔
167
        return this.loadedViews.length > 0 ? this.loadedViews[0] : undefined
21✔
168
    }
169

170
    /**
171
     * Loads given view's data from the database.
172
     */
173
    async getViews(viewPaths?: string[]): Promise<View[]> {
174
        this.loadedViews = await this.loadViews(viewPaths)
5,235✔
175
        return this.loadedViews
5,235✔
176
    }
177

178
    /**
179
     * Enables special query runner mode in which sql queries won't be executed,
180
     * instead they will be memorized into a special variable inside query runner.
181
     * You can get memorized sql using getMemorySql() method.
182
     */
183
    enableSqlMemory(): void {
184
        this.sqlInMemory = new SqlInMemory()
91✔
185
        this.sqlMemoryMode = true
91✔
186
    }
187

188
    /**
189
     * Disables special query runner mode in which sql queries won't be executed
190
     * started by calling enableSqlMemory() method.
191
     *
192
     * Previously memorized sql will be flushed.
193
     */
194
    disableSqlMemory(): void {
195
        this.sqlInMemory = new SqlInMemory()
91✔
196
        this.sqlMemoryMode = false
91✔
197
    }
198

199
    /**
200
     * Flushes all memorized sqls.
201
     */
202
    clearSqlMemory(): void {
203
        this.sqlInMemory = new SqlInMemory()
34,574✔
204
    }
205

206
    /**
207
     * Gets sql stored in the memory. Parameters in the sql are already replaced.
208
     */
209
    getMemorySql(): SqlInMemory {
210
        return this.sqlInMemory
91✔
211
    }
212

213
    /**
214
     * Executes up sql queries.
215
     */
216
    async executeMemoryUpSql(): Promise<void> {
217
        for (const { query, parameters } of this.sqlInMemory.upQueries) {
×
218
            await this.query(query, parameters)
×
219
        }
220
    }
221

222
    /**
223
     * Executes down sql queries.
224
     */
225
    async executeMemoryDownSql(): Promise<void> {
226
        for (const {
122✔
227
            query,
228
            parameters,
229
        } of this.sqlInMemory.downQueries.reverse()) {
230
            await this.query(query, parameters)
652✔
231
        }
232
    }
233

234
    getReplicationMode(): ReplicationMode {
235
        return this.mode
68✔
236
    }
237

238
    // -------------------------------------------------------------------------
239
    // Protected Methods
240
    // -------------------------------------------------------------------------
241

242
    /**
243
     * Gets view from previously loaded views, otherwise loads it from database.
244
     */
245
    protected async getCachedView(viewName: string): Promise<View> {
246
        const view = this.loadedViews.find((view) => view.name === viewName)
2✔
247
        if (view) return view
2✔
248

249
        const foundViews = await this.loadViews([viewName])
×
250
        if (foundViews.length > 0) {
×
251
            this.loadedViews.push(foundViews[0])
×
252
            return foundViews[0]
×
253
        } else {
254
            throw new TypeORMError(`View "${viewName}" does not exist.`)
×
255
        }
256
    }
257

258
    /**
259
     * Gets table from previously loaded tables, otherwise loads it from database.
260
     */
261
    protected async getCachedTable(tableName: string): Promise<Table> {
262
        if (tableName in this.cachedTablePaths) {
138✔
263
            const tablePath = this.cachedTablePaths[tableName]
23✔
264
            const table = this.loadedTables.find(
23✔
265
                (table) => this.getTablePath(table) === tablePath,
17✔
266
            )
267

268
            if (table) {
23✔
269
                return table
17✔
270
            }
271
        }
272

273
        const foundTables = await this.loadTables([tableName])
121✔
274

275
        if (foundTables.length > 0) {
121!
276
            const foundTablePath = this.getTablePath(foundTables[0])
121✔
277

278
            const cachedTable = this.loadedTables.find(
121✔
279
                (table) => this.getTablePath(table) === foundTablePath,
37✔
280
            )
281

282
            if (!cachedTable) {
121✔
283
                this.cachedTablePaths[tableName] = this.getTablePath(
109✔
284
                    foundTables[0],
285
                )
286
                this.loadedTables.push(foundTables[0])
109✔
287
                return foundTables[0]
109✔
288
            } else {
289
                return cachedTable
12✔
290
            }
291
        } else {
292
            throw new TypeORMError(`Table "${tableName}" does not exist.`)
×
293
        }
294
    }
295

296
    /**
297
     * Replaces loaded table with given changed table.
298
     */
299
    protected replaceCachedTable(table: Table, changedTable: Table): void {
300
        const oldTablePath = this.getTablePath(table)
5,169✔
301
        const foundTable = this.loadedTables.find(
5,169✔
302
            (loadedTable) => this.getTablePath(loadedTable) === oldTablePath,
21,218✔
303
        )
304

305
        // Clean up the lookup cache..
306
        for (const [key, cachedPath] of Object.entries(this.cachedTablePaths)) {
5,169✔
307
            if (cachedPath === oldTablePath) {
174✔
308
                this.cachedTablePaths[key] = this.getTablePath(changedTable)
82✔
309
            }
310
        }
311

312
        if (foundTable) {
5,169✔
313
            foundTable.database = changedTable.database
5,169✔
314
            foundTable.schema = changedTable.schema
5,169✔
315
            foundTable.name = changedTable.name
5,169✔
316
            foundTable.columns = changedTable.columns
5,169✔
317
            foundTable.indices = changedTable.indices
5,169✔
318
            foundTable.foreignKeys = changedTable.foreignKeys
5,169✔
319
            foundTable.uniques = changedTable.uniques
5,169✔
320
            foundTable.checks = changedTable.checks
5,169✔
321
            foundTable.justCreated = changedTable.justCreated
5,169✔
322
            foundTable.engine = changedTable.engine
5,169✔
323
            foundTable.comment = changedTable.comment
5,169✔
324
        }
325
    }
326

327
    protected getTablePath(
328
        target: EntityMetadata | Table | View | TableForeignKey | string,
329
    ): string {
330
        const parsed = this.connection.driver.parseTableName(target)
29,613✔
331

332
        return this.connection.driver.buildTableName(
29,613✔
333
            parsed.tableName,
334
            parsed.schema,
335
            parsed.database,
336
        )
337
    }
338

339
    protected getTypeormMetadataTableName(): string {
340
        const options = <
341
            SqlServerConnectionOptions | PostgresConnectionOptions
342
        >this.connection.driver.options
5,606✔
343
        return this.connection.driver.buildTableName(
5,606✔
344
            this.connection.metadataTableName,
345
            options.schema,
346
            options.database,
347
        )
348
    }
349

350
    /**
351
     * Generates SQL query to select record from typeorm metadata table.
352
     */
353
    protected selectTypeormMetadataSql({
354
        database,
355
        schema,
356
        table,
357
        type,
358
        name,
359
    }: {
360
        database?: string
361
        schema?: string
362
        table?: string
363
        type: MetadataTableType
364
        name: string
365
    }): Query {
366
        const qb = this.connection.createQueryBuilder()
70✔
367
        const selectQb = qb
70✔
368
            .select()
369
            .from(this.getTypeormMetadataTableName(), "t")
370
            .where(`${qb.escape("type")} = :type`, { type })
371
            .andWhere(`${qb.escape("name")} = :name`, { name })
372

373
        if (database) {
70!
374
            selectQb.andWhere(`${qb.escape("database")} = :database`, {
×
375
                database,
376
            })
377
        }
378

379
        if (schema) {
70!
380
            selectQb.andWhere(`${qb.escape("schema")} = :schema`, { schema })
×
381
        }
382

383
        if (table) {
70✔
384
            selectQb.andWhere(`${qb.escape("table")} = :table`, { table })
70✔
385
        }
386

387
        const [query, parameters] = selectQb.getQueryAndParameters()
70✔
388
        return new Query(query, parameters)
70✔
389
    }
390

391
    /**
392
     * Generates SQL query to insert a record into typeorm metadata table.
393
     */
394
    protected insertTypeormMetadataSql({
395
        database,
396
        schema,
397
        table,
398
        type,
399
        name,
400
        value,
401
    }: {
402
        database?: string
403
        schema?: string
404
        table?: string
405
        type: MetadataTableType
406
        name: string
407
        value?: string
408
    }): Query {
409
        const [query, parameters] = this.connection
110✔
410
            .createQueryBuilder()
411
            .insert()
412
            .into(this.getTypeormMetadataTableName())
413
            .values({
414
                database: database,
415
                schema: schema,
416
                table: table,
417
                type: type,
418
                name: name,
419
                value: value,
420
            })
421
            .getQueryAndParameters()
422

423
        return new Query(query, parameters)
110✔
424
    }
425

426
    /**
427
     * Generates SQL query to delete a record from typeorm metadata table.
428
     */
429
    protected deleteTypeormMetadataSql({
430
        database,
431
        schema,
432
        table,
433
        type,
434
        name,
435
    }: {
436
        database?: string
437
        schema?: string
438
        table?: string
439
        type: MetadataTableType
440
        name: string
441
    }): Query {
442
        const qb = this.connection.createQueryBuilder()
110✔
443
        const deleteQb = qb
110✔
444
            .delete()
445
            .from(this.getTypeormMetadataTableName())
446
            .where(`${qb.escape("type")} = :type`, { type })
447
            .andWhere(`${qb.escape("name")} = :name`, { name })
448

449
        if (database) {
110!
450
            deleteQb.andWhere(`${qb.escape("database")} = :database`, {
×
451
                database,
452
            })
453
        }
454

455
        if (schema) {
110!
456
            deleteQb.andWhere(`${qb.escape("schema")} = :schema`, { schema })
×
457
        }
458

459
        if (table) {
110✔
460
            deleteQb.andWhere(`${qb.escape("table")} = :table`, { table })
60✔
461
        }
462

463
        const [query, parameters] = deleteQb.getQueryAndParameters()
110✔
464
        return new Query(query, parameters)
110✔
465
    }
466

467
    /**
468
     * Checks if at least one of column properties was changed.
469
     * Does not checks column type, length and autoincrement, because these properties changes separately.
470
     */
471
    protected isColumnChanged(
472
        oldColumn: TableColumn,
473
        newColumn: TableColumn,
474
        checkDefault?: boolean,
475
        checkComment?: boolean,
476
        checkEnum = true,
32✔
477
    ): boolean {
478
        // this logs need to debug issues in column change detection. Do not delete it!
479

480
        // console.log("charset ---------------");
481
        // console.log(oldColumn.charset !== newColumn.charset);
482
        // console.log(oldColumn.charset, newColumn.charset);
483
        // console.log("collation ---------------");
484
        // console.log(oldColumn.collation !== newColumn.collation);
485
        // console.log(oldColumn.collation, newColumn.collation);
486
        // console.log("precision ---------------");
487
        // console.log(oldColumn.precision !== newColumn.precision);
488
        // console.log(oldColumn.precision, newColumn.precision);
489
        // console.log("scale ---------------");
490
        // console.log(oldColumn.scale !== newColumn.scale);
491
        // console.log(oldColumn.scale, newColumn.scale);
492
        // console.log("default ---------------");
493
        // console.log((checkDefault && oldColumn.default !== newColumn.default));
494
        // console.log(oldColumn.default, newColumn.default);
495
        // console.log("isNullable ---------------");
496
        // console.log(oldColumn.isNullable !== newColumn.isNullable);
497
        // console.log(oldColumn.isNullable, newColumn.isNullable);
498
        // console.log("comment ---------------");
499
        // console.log((checkComment && oldColumn.comment !== newColumn.comment));
500
        // console.log(oldColumn.comment, newColumn.comment);
501
        // console.log("enum ---------------");
502
        // console.log(!OrmUtils.isArraysEqual(oldColumn.enum || [], newColumn.enum || []));
503
        // console.log(oldColumn.enum, newColumn.enum);
504

505
        return (
32✔
506
            oldColumn.charset !== newColumn.charset ||
470!
507
            oldColumn.collation !== newColumn.collation ||
508
            oldColumn.precision !== newColumn.precision ||
509
            oldColumn.scale !== newColumn.scale ||
510
            oldColumn.width !== newColumn.width || // MySQL only
511
            oldColumn.zerofill !== newColumn.zerofill || // MySQL only
512
            oldColumn.unsigned !== newColumn.unsigned || // MySQL only
513
            oldColumn.asExpression !== newColumn.asExpression ||
514
            (checkDefault && oldColumn.default !== newColumn.default) ||
515
            oldColumn.onUpdate !== newColumn.onUpdate || // MySQL only
516
            oldColumn.isNullable !== newColumn.isNullable ||
517
            (checkComment && oldColumn.comment !== newColumn.comment) ||
518
            (checkEnum && this.isEnumChanged(oldColumn, newColumn))
519
        )
520
    }
521

522
    protected isEnumChanged(oldColumn: TableColumn, newColumn: TableColumn) {
523
        return !OrmUtils.isArraysEqual(
30✔
524
            oldColumn.enum || [],
60✔
525
            newColumn.enum || [],
60✔
526
        )
527
    }
528

529
    /**
530
     * Checks if column length is by default.
531
     */
532
    protected isDefaultColumnLength(
533
        table: Table,
534
        column: TableColumn,
535
        length: string,
536
    ): boolean {
537
        // if table have metadata, we check if length is specified in column metadata
538
        if (this.connection.hasMetadata(table.name)) {
823✔
539
            const metadata = this.connection.getMetadata(table.name)
762✔
540
            const columnMetadata = metadata.findColumnWithDatabaseName(
762✔
541
                column.name,
542
            )
543

544
            if (columnMetadata) {
762✔
545
                const columnMetadataLength =
546
                    this.connection.driver.getColumnLength(columnMetadata)
744✔
547
                if (columnMetadataLength) return false
744✔
548
            }
549
        }
550

551
        if (
83✔
552
            this.connection.driver.dataTypeDefaults &&
249✔
553
            this.connection.driver.dataTypeDefaults[column.type] &&
554
            this.connection.driver.dataTypeDefaults[column.type].length
555
        ) {
556
            return (
83✔
557
                this.connection.driver.dataTypeDefaults[
558
                    column.type
559
                ].length!.toString() === length.toString()
560
            )
561
        }
562

563
        return false
×
564
    }
565

566
    /**
567
     * Checks if column precision is by default.
568
     */
569
    protected isDefaultColumnPrecision(
570
        table: Table,
571
        column: TableColumn,
572
        precision: number,
573
    ): boolean {
574
        // if table have metadata, we check if length is specified in column metadata
575
        if (this.connection.hasMetadata(table.name)) {
59✔
576
            const metadata = this.connection.getMetadata(table.name)
59✔
577
            const columnMetadata = metadata.findColumnWithDatabaseName(
59✔
578
                column.name,
579
            )
580
            if (
59✔
581
                columnMetadata &&
177✔
582
                columnMetadata.precision !== null &&
583
                columnMetadata.precision !== undefined
584
            )
585
                return false
9✔
586
        }
587

588
        if (
50✔
589
            this.connection.driver.dataTypeDefaults &&
200✔
590
            this.connection.driver.dataTypeDefaults[column.type] &&
591
            this.connection.driver.dataTypeDefaults[column.type].precision !==
592
                null &&
593
            this.connection.driver.dataTypeDefaults[column.type].precision !==
594
                undefined
595
        )
596
            return (
50✔
597
                this.connection.driver.dataTypeDefaults[column.type]
598
                    .precision === precision
599
            )
600

601
        return false
×
602
    }
603

604
    /**
605
     * Checks if column scale is by default.
606
     */
607
    protected isDefaultColumnScale(
608
        table: Table,
609
        column: TableColumn,
610
        scale: number,
611
    ): boolean {
612
        // if table have metadata, we check if length is specified in column metadata
613
        if (this.connection.hasMetadata(table.name)) {
65✔
614
            const metadata = this.connection.getMetadata(table.name)
7✔
615
            const columnMetadata = metadata.findColumnWithDatabaseName(
7✔
616
                column.name,
617
            )
618
            if (
7✔
619
                columnMetadata &&
17✔
620
                columnMetadata.scale !== null &&
621
                columnMetadata.scale !== undefined
622
            )
623
                return false
5✔
624
        }
625

626
        if (
60!
627
            this.connection.driver.dataTypeDefaults &&
120!
628
            this.connection.driver.dataTypeDefaults[column.type] &&
629
            this.connection.driver.dataTypeDefaults[column.type].scale !==
630
                null &&
631
            this.connection.driver.dataTypeDefaults[column.type].scale !==
632
                undefined
633
        )
634
            return (
×
635
                this.connection.driver.dataTypeDefaults[column.type].scale ===
636
                scale
637
            )
638

639
        return false
60✔
640
    }
641

642
    /**
643
     * Executes sql used special for schema build.
644
     */
645
    protected async executeQueries(
646
        upQueries: Query | Query[],
647
        downQueries: Query | Query[],
648
    ): Promise<void> {
649
        if (InstanceChecker.isQuery(upQueries)) upQueries = [upQueries]
25,558✔
650
        if (InstanceChecker.isQuery(downQueries)) downQueries = [downQueries]
25,558✔
651

652
        this.sqlInMemory.upQueries.push(...upQueries)
25,558✔
653
        this.sqlInMemory.downQueries.push(...downQueries)
25,558✔
654

655
        // if sql-in-memory mode is enabled then simply store sql in memory and return
656
        if (this.sqlMemoryMode === true)
25,558✔
657
            return Promise.resolve() as Promise<any>
65✔
658

659
        for (const { query, parameters } of upQueries) {
25,493✔
660
            await this.query(query, parameters)
55,702✔
661
        }
662
    }
663

664
    /**
665
     * Generated an index name for a table and index
666
     */
667
    protected generateIndexName(
668
        table: Table | View,
669
        index: TableIndex,
670
    ): string {
671
        // new index may be passed without name. In this case we generate index name manually.
672
        return this.connection.namingStrategy.indexName(
16✔
673
            table,
674
            index.columnNames,
675
            index.where,
676
        )
677
    }
678
}
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