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

typeorm / typeorm / 15089093306

17 May 2025 09:03PM UTC coverage: 50.109% (-26.2%) from 76.346%
15089093306

Pull #11437

github

naorpeled
add comment about vector <#>
Pull Request #11437: feat(postgres): support vector data type

5836 of 12767 branches covered (45.71%)

Branch coverage included in aggregate %.

16 of 17 new or added lines in 4 files covered. (94.12%)

6283 existing lines in 64 files now uncovered.

12600 of 24025 relevant lines covered (52.45%)

28708.0 hits per line

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

75.29
/src/persistence/SubjectExecutor.ts
1
import { QueryRunner } from "../query-runner/QueryRunner"
2
import { Subject } from "./Subject"
3
import { SubjectTopologicalSorter } from "./SubjectTopologicalSorter"
6✔
4
import { SubjectChangedColumnsComputer } from "./SubjectChangedColumnsComputer"
6✔
5
import { SubjectWithoutIdentifierError } from "../error/SubjectWithoutIdentifierError"
6✔
6
import { SubjectRemovedAndUpdatedError } from "../error/SubjectRemovedAndUpdatedError"
6✔
7
import { MongoEntityManager } from "../entity-manager/MongoEntityManager"
8
import { ObjectLiteral } from "../common/ObjectLiteral"
9
import { SaveOptions } from "../repository/SaveOptions"
10
import { RemoveOptions } from "../repository/RemoveOptions"
11
import { BroadcasterResult } from "../subscriber/BroadcasterResult"
6✔
12
import { NestedSetSubjectExecutor } from "./tree/NestedSetSubjectExecutor"
6✔
13
import { ClosureSubjectExecutor } from "./tree/ClosureSubjectExecutor"
6✔
14
import { MaterializedPathSubjectExecutor } from "./tree/MaterializedPathSubjectExecutor"
6✔
15
import { OrmUtils } from "../util/OrmUtils"
6✔
16
import { UpdateResult } from "../query-builder/result/UpdateResult"
17
import { ObjectUtils } from "../util/ObjectUtils"
6✔
18
import { InstanceChecker } from "../util/InstanceChecker"
6✔
19

20
/**
21
 * Executes all database operations (inserts, updated, deletes) that must be executed
22
 * with given persistence subjects.
23
 */
24
export class SubjectExecutor {
6✔
25
    // -------------------------------------------------------------------------
26
    // Public Properties
27
    // -------------------------------------------------------------------------
28

29
    /**
30
     * Indicates if executor has any operations to execute (e.g. has insert / update / delete operations to be executed).
31
     */
32
    hasExecutableOperations: boolean = false
39,613✔
33

34
    // -------------------------------------------------------------------------
35
    // Protected Properties
36
    // -------------------------------------------------------------------------
37

38
    /**
39
     * QueryRunner used to execute all queries with a given subjects.
40
     */
41
    protected queryRunner: QueryRunner
42

43
    /**
44
     * Persistence options.
45
     */
46
    protected options?: SaveOptions & RemoveOptions
47

48
    /**
49
     * All subjects that needs to be operated.
50
     */
51
    protected allSubjects: Subject[]
52

53
    /**
54
     * Subjects that must be inserted.
55
     */
56
    protected insertSubjects: Subject[] = []
39,613✔
57

58
    /**
59
     * Subjects that must be updated.
60
     */
61
    protected updateSubjects: Subject[] = []
39,613✔
62

63
    /**
64
     * Subjects that must be removed.
65
     */
66
    protected removeSubjects: Subject[] = []
39,613✔
67

68
    /**
69
     * Subjects that must be soft-removed.
70
     */
71
    protected softRemoveSubjects: Subject[] = []
39,613✔
72

73
    /**
74
     * Subjects that must be recovered.
75
     */
76
    protected recoverSubjects: Subject[] = []
39,613✔
77

78
    // -------------------------------------------------------------------------
79
    // Constructor
80
    // -------------------------------------------------------------------------
81

82
    constructor(
83
        queryRunner: QueryRunner,
84
        subjects: Subject[],
85
        options?: SaveOptions & RemoveOptions,
86
    ) {
87
        this.queryRunner = queryRunner
39,613✔
88
        this.allSubjects = subjects
39,613✔
89
        this.options = options
39,613✔
90
        this.validate()
39,613✔
91
        this.recompute()
39,613✔
92
    }
93

94
    // -------------------------------------------------------------------------
95
    // Public Methods
96
    // -------------------------------------------------------------------------
97

98
    /**
99
     * Executes all operations over given array of subjects.
100
     * Executes queries using given query runner.
101
     */
102
    async execute(): Promise<void> {
103
        // console.time("SubjectExecutor.execute");
104

105
        // broadcast "before" events before we start insert / update / remove operations
106
        let broadcasterResult: BroadcasterResult | undefined = undefined
39,493✔
107
        if (!this.options || this.options.listeners !== false) {
39,493✔
108
            // console.time(".broadcastBeforeEventsForAll");
109
            broadcasterResult = this.broadcastBeforeEventsForAll()
39,475✔
110
            if (broadcasterResult.promises.length > 0)
39,475✔
111
                await Promise.all(broadcasterResult.promises)
18✔
112
            // console.timeEnd(".broadcastBeforeEventsForAll");
113
        }
114

115
        // since event listeners and subscribers can call save methods and/or trigger entity changes we need to recompute operational subjects
116
        // recompute only in the case if any listener or subscriber was really executed
117
        if (broadcasterResult && broadcasterResult.count > 0) {
39,493✔
118
            // console.time(".recompute");
119
            this.insertSubjects.forEach((subject) => subject.recompute())
117✔
120
            this.updateSubjects.forEach((subject) => subject.recompute())
117✔
121
            this.removeSubjects.forEach((subject) => subject.recompute())
117✔
122
            this.softRemoveSubjects.forEach((subject) => subject.recompute())
117✔
123
            this.recoverSubjects.forEach((subject) => subject.recompute())
117✔
124
            this.recompute()
117✔
125
            // console.timeEnd(".recompute");
126
        }
127

128
        // make sure our insert subjects are sorted (using topological sorting) to make cascade inserts work properly
129

130
        // console.timeEnd("prepare");
131

132
        // execute all insert operations
133
        // console.time(".insertion");
134
        this.insertSubjects = new SubjectTopologicalSorter(
39,493✔
135
            this.insertSubjects,
136
        ).sort("insert")
137
        await this.executeInsertOperations()
39,493✔
138
        // console.timeEnd(".insertion");
139

140
        // recompute update operations since insertion can create updation operations for the
141
        // properties it wasn't able to handle on its own (referenced columns)
142
        this.updateSubjects = this.allSubjects.filter(
39,461✔
143
            (subject) => subject.mustBeUpdated,
117,335✔
144
        )
145

146
        // execute update operations
147
        // console.time(".updation");
148
        await this.executeUpdateOperations()
39,461✔
149
        // console.timeEnd(".updation");
150

151
        // make sure our remove subjects are sorted (using topological sorting) when multiple entities are passed for the removal
152
        // console.time(".removal");
153
        this.removeSubjects = new SubjectTopologicalSorter(
39,458✔
154
            this.removeSubjects,
155
        ).sort("delete")
156
        await this.executeRemoveOperations()
39,458✔
157
        // console.timeEnd(".removal");
158

159
        // recompute soft-remove operations
160
        this.softRemoveSubjects = this.allSubjects.filter(
39,458✔
161
            (subject) => subject.mustBeSoftRemoved,
117,328✔
162
        )
163

164
        // execute soft-remove operations
165
        await this.executeSoftRemoveOperations()
39,458✔
166

167
        // recompute recover operations
168
        this.recoverSubjects = this.allSubjects.filter(
39,446✔
169
            (subject) => subject.mustBeRecovered,
117,316✔
170
        )
171

172
        // execute recover operations
173
        await this.executeRecoverOperations()
39,446✔
174

175
        // update all special columns in persisted entities, like inserted id or remove ids from the removed entities
176
        // console.time(".updateSpecialColumnsInPersistedEntities");
177
        this.updateSpecialColumnsInPersistedEntities()
39,434✔
178
        // console.timeEnd(".updateSpecialColumnsInPersistedEntities");
179

180
        // finally broadcast "after" events after we finish insert / update / remove operations
181
        if (!this.options || this.options.listeners !== false) {
39,434✔
182
            // console.time(".broadcastAfterEventsForAll");
183
            broadcasterResult = this.broadcastAfterEventsForAll()
39,416✔
184
            if (broadcasterResult.promises.length > 0)
39,416!
185
                await Promise.all(broadcasterResult.promises)
×
186
            // console.timeEnd(".broadcastAfterEventsForAll");
187
        }
188
        // console.timeEnd("SubjectExecutor.execute");
189
    }
190

191
    // -------------------------------------------------------------------------
192
    // Protected Methods
193
    // -------------------------------------------------------------------------
194

195
    /**
196
     * Validates all given subjects.
197
     */
198
    protected validate() {
199
        this.allSubjects.forEach((subject) => {
39,613✔
200
            if (subject.mustBeUpdated && subject.mustBeRemoved)
117,525!
201
                throw new SubjectRemovedAndUpdatedError(subject)
×
202
        })
203
    }
204

205
    /**
206
     * Performs entity re-computations - finds changed columns, re-builds insert/update/remove subjects.
207
     */
208
    protected recompute(): void {
209
        new SubjectChangedColumnsComputer().compute(this.allSubjects)
39,730✔
210
        this.insertSubjects = this.allSubjects.filter(
39,730✔
211
            (subject) => subject.mustBeInserted,
117,666✔
212
        )
213
        this.updateSubjects = this.allSubjects.filter(
39,730✔
214
            (subject) => subject.mustBeUpdated,
117,666✔
215
        )
216
        this.removeSubjects = this.allSubjects.filter(
39,730✔
217
            (subject) => subject.mustBeRemoved,
117,666✔
218
        )
219
        this.softRemoveSubjects = this.allSubjects.filter(
39,730✔
220
            (subject) => subject.mustBeSoftRemoved,
117,666✔
221
        )
222
        this.recoverSubjects = this.allSubjects.filter(
39,730✔
223
            (subject) => subject.mustBeRecovered,
117,666✔
224
        )
225
        this.hasExecutableOperations =
39,730✔
226
            this.insertSubjects.length > 0 ||
41,970✔
227
            this.updateSubjects.length > 0 ||
228
            this.removeSubjects.length > 0 ||
229
            this.softRemoveSubjects.length > 0 ||
230
            this.recoverSubjects.length > 0
231
    }
232

233
    /**
234
     * Broadcasts "BEFORE_INSERT", "BEFORE_UPDATE", "BEFORE_REMOVE", "BEFORE_SOFT_REMOVE", "BEFORE_RECOVER" events for all given subjects.
235
     */
236
    protected broadcastBeforeEventsForAll(): BroadcasterResult {
237
        const result = new BroadcasterResult()
39,475✔
238
        if (this.insertSubjects.length)
39,475✔
239
            this.insertSubjects.forEach((subject) =>
38,492✔
240
                this.queryRunner.broadcaster.broadcastBeforeInsertEvent(
114,030✔
241
                    result,
242
                    subject.metadata,
243
                    subject.entity!,
244
                ),
245
            )
246
        if (this.updateSubjects.length)
39,475✔
247
            this.updateSubjects.forEach((subject) =>
1,384✔
248
                this.queryRunner.broadcaster.broadcastBeforeUpdateEvent(
2,151✔
249
                    result,
250
                    subject.metadata,
251
                    subject.entity!,
252
                    subject.databaseEntity,
253
                    subject.diffColumns,
254
                    subject.diffRelations,
255
                ),
256
            )
257
        if (this.removeSubjects.length)
39,475✔
258
            this.removeSubjects.forEach((subject) =>
357✔
259
                this.queryRunner.broadcaster.broadcastBeforeRemoveEvent(
518✔
260
                    result,
261
                    subject.metadata,
262
                    subject.entity!,
263
                    subject.databaseEntity,
264
                    subject.identifier,
265
                ),
266
            )
267
        if (this.softRemoveSubjects.length)
39,475✔
268
            this.softRemoveSubjects.forEach((subject) =>
108✔
269
                this.queryRunner.broadcaster.broadcastBeforeSoftRemoveEvent(
108✔
270
                    result,
271
                    subject.metadata,
272
                    subject.entity!,
273
                    subject.databaseEntity,
274
                    subject.identifier,
275
                ),
276
            )
277
        if (this.recoverSubjects.length)
39,475✔
278
            this.recoverSubjects.forEach((subject) =>
36✔
279
                this.queryRunner.broadcaster.broadcastBeforeRecoverEvent(
36✔
280
                    result,
281
                    subject.metadata,
282
                    subject.entity!,
283
                    subject.databaseEntity,
284
                    subject.identifier,
285
                ),
286
            )
287
        return result
39,475✔
288
    }
289

290
    /**
291
     * Broadcasts "AFTER_INSERT", "AFTER_UPDATE", "AFTER_REMOVE", "AFTER_SOFT_REMOVE", "AFTER_RECOVER" events for all given subjects.
292
     * Returns void if there wasn't any listener or subscriber executed.
293
     * Note: this method has a performance-optimized code organization.
294
     */
295
    protected broadcastAfterEventsForAll(): BroadcasterResult {
296
        const result = new BroadcasterResult()
39,416✔
297
        if (this.insertSubjects.length)
39,416✔
298
            this.insertSubjects.forEach((subject) =>
38,459✔
299
                this.queryRunner.broadcaster.broadcastAfterInsertEvent(
113,996✔
300
                    result,
301
                    subject.metadata,
302
                    subject.entity!,
303
                    subject.identifier,
304
                ),
305
            )
306
        if (this.updateSubjects.length)
39,416✔
307
            this.updateSubjects.forEach((subject) =>
1,397✔
308
                this.queryRunner.broadcaster.broadcastAfterUpdateEvent(
2,163✔
309
                    result,
310
                    subject.metadata,
311
                    subject.entity!,
312
                    subject.databaseEntity,
313
                    subject.diffColumns,
314
                    subject.diffRelations,
315
                ),
316
            )
317
        if (this.removeSubjects.length)
39,416✔
318
            this.removeSubjects.forEach((subject) =>
357✔
319
                this.queryRunner.broadcaster.broadcastAfterRemoveEvent(
518✔
320
                    result,
321
                    subject.metadata,
322
                    subject.entity!,
323
                    subject.databaseEntity,
324
                    subject.identifier,
325
                ),
326
            )
327
        if (this.softRemoveSubjects.length)
39,416✔
328
            this.softRemoveSubjects.forEach((subject) =>
96✔
329
                this.queryRunner.broadcaster.broadcastAfterSoftRemoveEvent(
96✔
330
                    result,
331
                    subject.metadata,
332
                    subject.entity!,
333
                    subject.databaseEntity,
334
                    subject.identifier,
335
                ),
336
            )
337
        if (this.recoverSubjects.length)
39,416✔
338
            this.recoverSubjects.forEach((subject) =>
24✔
339
                this.queryRunner.broadcaster.broadcastAfterRecoverEvent(
24✔
340
                    result,
341
                    subject.metadata,
342
                    subject.entity!,
343
                    subject.databaseEntity,
344
                    subject.identifier,
345
                ),
346
            )
347
        return result
39,416✔
348
    }
349

350
    /**
351
     * Executes insert operations.
352
     */
353
    protected async executeInsertOperations(): Promise<void> {
354
        // group insertion subjects to make bulk insertions
355
        const [groupedInsertSubjects, groupedInsertSubjectKeys] =
356
            this.groupBulkSubjects(this.insertSubjects, "insert")
39,493✔
357

358
        // then we run insertion in the sequential order which is important since we have an ordered subjects
359
        for (const groupName of groupedInsertSubjectKeys) {
39,493✔
360
            const subjects = groupedInsertSubjects[groupName]
50,014✔
361

362
            // we must separately insert entities which does not have any values to insert
363
            // because its not possible to insert multiple entities with only default values in bulk
364
            const bulkInsertMaps: ObjectLiteral[] = []
50,014✔
365
            const bulkInsertSubjects: Subject[] = []
50,014✔
366
            const singleInsertSubjects: Subject[] = []
50,014✔
367
            if (this.queryRunner.connection.driver.options.type === "mongodb") {
50,014!
UNCOV
368
                subjects.forEach((subject) => {
×
UNCOV
369
                    if (subject.metadata.createDateColumn && subject.entity) {
×
UNCOV
370
                        subject.entity[
×
371
                            subject.metadata.createDateColumn.databaseName
372
                        ] = new Date()
373
                    }
374

UNCOV
375
                    if (subject.metadata.updateDateColumn && subject.entity) {
×
UNCOV
376
                        subject.entity[
×
377
                            subject.metadata.updateDateColumn.databaseName
378
                        ] = new Date()
379
                    }
380

UNCOV
381
                    subject.createValueSetAndPopChangeMap()
×
382

UNCOV
383
                    bulkInsertSubjects.push(subject)
×
UNCOV
384
                    bulkInsertMaps.push(subject.entity!)
×
385
                })
386
            } else if (
50,014!
387
                this.queryRunner.connection.driver.options.type === "oracle"
388
            ) {
UNCOV
389
                subjects.forEach((subject) => {
×
UNCOV
390
                    singleInsertSubjects.push(subject)
×
391
                })
392
            } else {
393
                subjects.forEach((subject) => {
50,014✔
394
                    // we do not insert in bulk in following cases:
395
                    // - when there is no values in insert (only defaults are inserted), since we cannot use DEFAULT VALUES expression for multiple inserted rows
396
                    // - when entity is a tree table, since tree tables require extra operation per each inserted row
397
                    // - when oracle is used, since oracle's bulk insertion is very bad
398
                    if (
114,036✔
399
                        subject.changeMaps.length === 0 ||
450,750✔
400
                        subject.metadata.treeType ||
401
                        this.queryRunner.connection.driver.options.type ===
402
                            "oracle" ||
403
                        this.queryRunner.connection.driver.options.type ===
404
                            "sap"
405
                    ) {
406
                        singleInsertSubjects.push(subject)
2,572✔
407
                    } else {
408
                        bulkInsertSubjects.push(subject)
111,464✔
409
                        bulkInsertMaps.push(
111,464✔
410
                            subject.createValueSetAndPopChangeMap(),
411
                        )
412
                    }
413
                })
414
            }
415

416
            // for mongodb we have a bit different insertion logic
417
            if (
50,014!
418
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
419
            ) {
UNCOV
420
                const insertResult = await this.queryRunner.manager.insert(
×
421
                    subjects[0].metadata.target,
422
                    bulkInsertMaps,
423
                )
UNCOV
424
                subjects.forEach((subject, index) => {
×
UNCOV
425
                    subject.identifier = insertResult.identifiers[index]
×
UNCOV
426
                    subject.generatedMap = insertResult.generatedMaps[index]
×
UNCOV
427
                    subject.insertedValueSet = bulkInsertMaps[index]
×
428
                })
429
            } else {
430
                // here we execute our insertion query
431
                // we need to enable entity updation because we DO need to have updated insertedMap
432
                // which is not same object as our entity that's why we don't need to worry about our entity to get dirty
433
                // also, we disable listeners because we call them on our own in persistence layer
434
                if (bulkInsertMaps.length > 0) {
50,014✔
435
                    const insertResult = await this.queryRunner.manager
47,610✔
436
                        .createQueryBuilder()
437
                        .insert()
438
                        .into(subjects[0].metadata.target)
439
                        .values(bulkInsertMaps)
440
                        .updateEntity(
441
                            this.options && this.options.reload === false
95,254!
442
                                ? false
443
                                : true,
444
                        )
445
                        .callListeners(false)
446
                        .execute()
447

448
                    bulkInsertSubjects.forEach((subject, index) => {
47,597✔
449
                        subject.identifier = insertResult.identifiers[index]
111,451✔
450
                        subject.generatedMap = insertResult.generatedMaps[index]
111,451✔
451
                        subject.insertedValueSet = bulkInsertMaps[index]
111,451✔
452
                    })
453
                }
454

455
                // insert subjects which must be inserted in separate requests (all default values)
456
                if (singleInsertSubjects.length > 0) {
50,001✔
457
                    for (const subject of singleInsertSubjects) {
2,404✔
458
                        subject.insertedValueSet =
2,572✔
459
                            subject.createValueSetAndPopChangeMap() // important to have because query builder sets inserted values into it
460

461
                        // for nested set we execute additional queries
462
                        if (subject.metadata.treeType === "nested-set")
2,572✔
463
                            await new NestedSetSubjectExecutor(
523✔
464
                                this.queryRunner,
465
                            ).insert(subject)
466

467
                        await this.queryRunner.manager
2,558✔
468
                            .createQueryBuilder()
469
                            .insert()
470
                            .into(subject.metadata.target)
471
                            .values(subject.insertedValueSet)
472
                            .updateEntity(
473
                                this.options && this.options.reload === false
5,116!
474
                                    ? false
475
                                    : true,
476
                            )
477
                            .callListeners(false)
478
                            .execute()
479
                            .then((insertResult) => {
480
                                subject.identifier = insertResult.identifiers[0]
2,553✔
481
                                subject.generatedMap =
2,553✔
482
                                    insertResult.generatedMaps[0]
483
                            })
484

485
                        // for tree tables we execute additional queries
486
                        if (subject.metadata.treeType === "closure-table") {
2,553✔
487
                            await new ClosureSubjectExecutor(
781✔
488
                                this.queryRunner,
489
                            ).insert(subject)
490
                        } else if (
1,772✔
491
                            subject.metadata.treeType === "materialized-path"
492
                        ) {
493
                            await new MaterializedPathSubjectExecutor(
1,057✔
494
                                this.queryRunner,
495
                            ).insert(subject)
496
                        }
497
                    }
498
                }
499
            }
500

501
            subjects.forEach((subject) => {
49,982✔
502
                if (subject.generatedMap) {
114,004✔
503
                    subject.metadata.columns.forEach((column) => {
114,004✔
504
                        const value = column.getEntityValue(
395,781✔
505
                            subject.generatedMap!,
506
                        )
507
                        if (value !== undefined && value !== null) {
395,781✔
508
                            const preparedValue =
509
                                this.queryRunner.connection.driver.prepareHydratedValue(
61,377✔
510
                                    value,
511
                                    column,
512
                                )
513
                            column.setEntityValue(
61,377✔
514
                                subject.generatedMap!,
515
                                preparedValue,
516
                            )
517
                        }
518
                    })
519
                }
520
            })
521
        }
522
    }
523

524
    /**
525
     * Updates all given subjects in the database.
526
     */
527
    protected async executeUpdateOperations(): Promise<void> {
528
        const updateSubject = async (subject: Subject) => {
39,461✔
529
            if (!subject.identifier)
2,167!
530
                throw new SubjectWithoutIdentifierError(subject)
×
531

532
            // for mongodb we have a bit different updation logic
533
            if (
2,167!
534
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
535
            ) {
UNCOV
536
                const partialEntity = this.cloneMongoSubjectEntity(subject)
×
UNCOV
537
                if (
×
538
                    subject.metadata.objectIdColumn &&
×
539
                    subject.metadata.objectIdColumn.propertyName
540
                ) {
UNCOV
541
                    delete partialEntity[
×
542
                        subject.metadata.objectIdColumn.propertyName
543
                    ]
544
                }
545

UNCOV
546
                if (
×
547
                    subject.metadata.createDateColumn &&
×
548
                    subject.metadata.createDateColumn.propertyName
549
                ) {
UNCOV
550
                    delete partialEntity[
×
551
                        subject.metadata.createDateColumn.propertyName
552
                    ]
553
                }
554

UNCOV
555
                if (
×
556
                    subject.metadata.updateDateColumn &&
×
557
                    subject.metadata.updateDateColumn.propertyName
558
                ) {
UNCOV
559
                    partialEntity[
×
560
                        subject.metadata.updateDateColumn.propertyName
561
                    ] = new Date()
562
                }
563

UNCOV
564
                const manager = this.queryRunner.manager as MongoEntityManager
×
565

UNCOV
566
                await manager.update(
×
567
                    subject.metadata.target,
568
                    subject.identifier,
569
                    partialEntity,
570
                )
571
            } else {
572
                const updateMap: ObjectLiteral =
573
                    subject.createValueSetAndPopChangeMap()
2,167✔
574

575
                // for tree tables we execute additional queries
576
                switch (subject.metadata.treeType) {
2,167✔
577
                    case "nested-set":
578
                        await new NestedSetSubjectExecutor(
19✔
579
                            this.queryRunner,
580
                        ).update(subject)
581
                        break
17✔
582

583
                    case "closure-table":
584
                        await new ClosureSubjectExecutor(
13✔
585
                            this.queryRunner,
586
                        ).update(subject)
587
                        break
13✔
588

589
                    case "materialized-path":
590
                        await new MaterializedPathSubjectExecutor(
27✔
591
                            this.queryRunner,
592
                        ).update(subject)
593
                        break
27✔
594
                }
595

596
                // here we execute our updation query
597
                // we need to enable entity updation because we update a subject identifier
598
                // which is not same object as our entity that's why we don't need to worry about our entity to get dirty
599
                // also, we disable listeners because we call them on our own in persistence layer
600
                const updateQueryBuilder = this.queryRunner.manager
2,165✔
601
                    .createQueryBuilder()
602
                    .update(subject.metadata.target)
603
                    .set(updateMap)
604
                    .updateEntity(
605
                        this.options && this.options.reload === false
4,342!
606
                            ? false
607
                            : true,
608
                    )
609
                    .callListeners(false)
610

611
                if (subject.entity) {
2,165✔
612
                    updateQueryBuilder.whereEntity(subject.identifier)
595✔
613
                } else {
614
                    // in this case identifier is just conditions object to update by
615
                    updateQueryBuilder.where(subject.identifier)
1,570✔
616
                }
617

618
                const updateResult = await updateQueryBuilder.execute()
2,165✔
619
                const updateGeneratedMap = updateResult.generatedMaps[0]
2,163✔
620
                if (updateGeneratedMap) {
2,163✔
621
                    subject.metadata.columns.forEach((column) => {
64✔
622
                        const value = column.getEntityValue(updateGeneratedMap!)
348✔
623
                        if (value !== undefined && value !== null) {
348✔
624
                            const preparedValue =
625
                                this.queryRunner.connection.driver.prepareHydratedValue(
129✔
626
                                    value,
627
                                    column,
628
                                )
629
                            column.setEntityValue(
129✔
630
                                updateGeneratedMap!,
631
                                preparedValue,
632
                            )
633
                        }
634
                    })
635
                    if (!subject.generatedMap) {
64✔
636
                        subject.generatedMap = {}
57✔
637
                    }
638
                    Object.assign(subject.generatedMap, updateGeneratedMap)
64✔
639
                }
640
            }
641
        }
642

643
        // Nested sets need to be updated one by one
644
        // Split array in two, one with nested set subjects and the other with the remaining subjects
645
        const nestedSetSubjects: Subject[] = []
39,461✔
646
        const remainingSubjects: Subject[] = []
39,461✔
647

648
        for (const subject of this.updateSubjects) {
39,461✔
649
            if (subject.metadata.treeType === "nested-set") {
2,167✔
650
                nestedSetSubjects.push(subject)
19✔
651
            } else {
652
                remainingSubjects.push(subject)
2,148✔
653
            }
654
        }
655

656
        // Run nested set updates one by one
657
        const nestedSetPromise = new Promise<void>(async (ok, fail) => {
39,461✔
658
            for (const subject of nestedSetSubjects) {
39,461✔
659
                try {
19✔
660
                    await updateSubject(subject)
19✔
661
                } catch (error) {
662
                    fail(error)
2✔
663
                }
664
            }
665
            ok()
39,461✔
666
        })
667

668
        // Run all remaining subjects in parallel
669
        await Promise.all([
39,461✔
670
            ...remainingSubjects.map(updateSubject),
671
            nestedSetPromise,
672
        ])
673
    }
674

675
    /**
676
     * Removes all given subjects from the database.
677
     *
678
     * todo: we need to apply topological sort here as well
679
     */
680
    protected async executeRemoveOperations(): Promise<void> {
681
        // group insertion subjects to make bulk insertions
682
        const [groupedRemoveSubjects, groupedRemoveSubjectKeys] =
683
            this.groupBulkSubjects(this.removeSubjects, "delete")
39,458✔
684

685
        for (const groupName of groupedRemoveSubjectKeys) {
39,458✔
686
            const subjects = groupedRemoveSubjects[groupName]
465✔
687
            const deleteMaps = subjects.map((subject) => {
465✔
688
                if (!subject.identifier)
524!
689
                    throw new SubjectWithoutIdentifierError(subject)
×
690

691
                return subject.identifier
524✔
692
            })
693

694
            // for mongodb we have a bit different updation logic
695
            if (
465!
696
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
697
            ) {
UNCOV
698
                const manager = this.queryRunner.manager as MongoEntityManager
×
UNCOV
699
                await manager.delete(subjects[0].metadata.target, deleteMaps)
×
700
            } else {
701
                // for tree tables we execute additional queries
702
                switch (subjects[0].metadata.treeType) {
465✔
703
                    case "nested-set":
704
                        await new NestedSetSubjectExecutor(
6✔
705
                            this.queryRunner,
706
                        ).remove(subjects)
707
                        break
6✔
708

709
                    case "closure-table":
710
                        await new ClosureSubjectExecutor(
3✔
711
                            this.queryRunner,
712
                        ).remove(subjects)
713
                        break
3✔
714
                }
715

716
                // here we execute our deletion query
717
                // we don't need to specify entities and set update entity to true since the only thing query builder
718
                // will do for use is a primary keys deletion which is handled by us later once persistence is finished
719
                // also, we disable listeners because we call them on our own in persistence layer
720
                await this.queryRunner.manager
465✔
721
                    .createQueryBuilder()
722
                    .delete()
723
                    .from(subjects[0].metadata.target)
724
                    .where(deleteMaps)
725
                    .callListeners(false)
726
                    .execute()
727
            }
728
        }
729
    }
730

731
    private cloneMongoSubjectEntity(subject: Subject): ObjectLiteral {
UNCOV
732
        const target: ObjectLiteral = {}
×
733

UNCOV
734
        if (subject.entity) {
×
UNCOV
735
            for (const column of subject.metadata.columns) {
×
UNCOV
736
                OrmUtils.mergeDeep(
×
737
                    target,
738
                    column.getEntityValueMap(subject.entity),
739
                )
740
            }
741
        }
742

UNCOV
743
        return target
×
744
    }
745

746
    /**
747
     * Soft-removes all given subjects in the database.
748
     */
749
    protected async executeSoftRemoveOperations(): Promise<void> {
750
        await Promise.all(
39,458✔
751
            this.softRemoveSubjects.map(async (subject) => {
752
                if (!subject.identifier)
114!
753
                    throw new SubjectWithoutIdentifierError(subject)
×
754

755
                let updateResult: UpdateResult
756

757
                // for mongodb we have a bit different updation logic
758
                if (
114!
759
                    InstanceChecker.isMongoEntityManager(
760
                        this.queryRunner.manager,
761
                    )
762
                ) {
UNCOV
763
                    const partialEntity = this.cloneMongoSubjectEntity(subject)
×
UNCOV
764
                    if (
×
765
                        subject.metadata.objectIdColumn &&
×
766
                        subject.metadata.objectIdColumn.propertyName
767
                    ) {
UNCOV
768
                        delete partialEntity[
×
769
                            subject.metadata.objectIdColumn.propertyName
770
                        ]
771
                    }
772

UNCOV
773
                    if (
×
774
                        subject.metadata.createDateColumn &&
×
775
                        subject.metadata.createDateColumn.propertyName
776
                    ) {
777
                        delete partialEntity[
×
778
                            subject.metadata.createDateColumn.propertyName
779
                        ]
780
                    }
781

UNCOV
782
                    if (
×
783
                        subject.metadata.updateDateColumn &&
×
784
                        subject.metadata.updateDateColumn.propertyName
785
                    ) {
786
                        partialEntity[
×
787
                            subject.metadata.updateDateColumn.propertyName
788
                        ] = new Date()
789
                    }
790

UNCOV
791
                    if (
×
792
                        subject.metadata.deleteDateColumn &&
×
793
                        subject.metadata.deleteDateColumn.propertyName
794
                    ) {
UNCOV
795
                        partialEntity[
×
796
                            subject.metadata.deleteDateColumn.propertyName
797
                        ] = new Date()
798
                    }
799

UNCOV
800
                    const manager = this.queryRunner
×
801
                        .manager as MongoEntityManager
802

UNCOV
803
                    updateResult = await manager.update(
×
804
                        subject.metadata.target,
805
                        subject.identifier,
806
                        partialEntity,
807
                    )
808
                } else {
809
                    // here we execute our soft-deletion query
810
                    // we need to enable entity soft-deletion because we update a subject identifier
811
                    // which is not same object as our entity that's why we don't need to worry about our entity to get dirty
812
                    // also, we disable listeners because we call them on our own in persistence layer
813
                    const softDeleteQueryBuilder = this.queryRunner.manager
114✔
814
                        .createQueryBuilder()
815
                        .softDelete()
816
                        .from(subject.metadata.target)
817
                        .updateEntity(
818
                            this.options && this.options.reload === false
240!
819
                                ? false
820
                                : true,
821
                        )
822
                        .callListeners(false)
823

824
                    if (subject.entity) {
114✔
825
                        softDeleteQueryBuilder.whereEntity(subject.identifier)
96✔
826
                    } else {
827
                        // in this case identifier is just conditions object to update by
828
                        softDeleteQueryBuilder.where(subject.identifier)
18✔
829
                    }
830

831
                    updateResult = await softDeleteQueryBuilder.execute()
114✔
832
                }
833

834
                subject.generatedMap = updateResult.generatedMaps[0]
102✔
835
                if (subject.generatedMap) {
102✔
836
                    subject.metadata.columns.forEach((column) => {
84✔
837
                        const value = column.getEntityValue(
360✔
838
                            subject.generatedMap!,
839
                        )
840
                        if (value !== undefined && value !== null) {
360✔
841
                            const preparedValue =
842
                                this.queryRunner.connection.driver.prepareHydratedValue(
160✔
843
                                    value,
844
                                    column,
845
                                )
846
                            column.setEntityValue(
160✔
847
                                subject.generatedMap!,
848
                                preparedValue,
849
                            )
850
                        }
851
                    })
852
                }
853

854
                // experiments, remove probably, need to implement tree tables children removal
855
                // if (subject.updatedRelationMaps.length > 0) {
856
                //     await Promise.all(subject.updatedRelationMaps.map(async updatedRelation => {
857
                //         if (!updatedRelation.relation.isTreeParent) return;
858
                //         if (!updatedRelation.value !== null) return;
859
                //
860
                //         if (subject.metadata.treeType === "closure-table") {
861
                //             await new ClosureSubjectExecutor(this.queryRunner).deleteChildrenOf(subject);
862
                //         }
863
                //     }));
864
                // }
865
            }),
866
        )
867
    }
868

869
    /**
870
     * Recovers all given subjects in the database.
871
     */
872
    protected async executeRecoverOperations(): Promise<void> {
873
        await Promise.all(
39,446✔
874
            this.recoverSubjects.map(async (subject) => {
875
                if (!subject.identifier)
36!
876
                    throw new SubjectWithoutIdentifierError(subject)
×
877

878
                let updateResult: UpdateResult
879

880
                // for mongodb we have a bit different updation logic
881
                if (
36!
882
                    InstanceChecker.isMongoEntityManager(
883
                        this.queryRunner.manager,
884
                    )
885
                ) {
886
                    const partialEntity = this.cloneMongoSubjectEntity(subject)
×
887
                    if (
×
888
                        subject.metadata.objectIdColumn &&
×
889
                        subject.metadata.objectIdColumn.propertyName
890
                    ) {
891
                        delete partialEntity[
×
892
                            subject.metadata.objectIdColumn.propertyName
893
                        ]
894
                    }
895

896
                    if (
×
897
                        subject.metadata.createDateColumn &&
×
898
                        subject.metadata.createDateColumn.propertyName
899
                    ) {
900
                        delete partialEntity[
×
901
                            subject.metadata.createDateColumn.propertyName
902
                        ]
903
                    }
904

905
                    if (
×
906
                        subject.metadata.updateDateColumn &&
×
907
                        subject.metadata.updateDateColumn.propertyName
908
                    ) {
909
                        partialEntity[
×
910
                            subject.metadata.updateDateColumn.propertyName
911
                        ] = new Date()
912
                    }
913

914
                    if (
×
915
                        subject.metadata.deleteDateColumn &&
×
916
                        subject.metadata.deleteDateColumn.propertyName
917
                    ) {
918
                        partialEntity[
×
919
                            subject.metadata.deleteDateColumn.propertyName
920
                        ] = null
921
                    }
922

923
                    const manager = this.queryRunner
×
924
                        .manager as MongoEntityManager
925

926
                    updateResult = await manager.update(
×
927
                        subject.metadata.target,
928
                        subject.identifier,
929
                        partialEntity,
930
                    )
931
                } else {
932
                    // here we execute our restory query
933
                    // we need to enable entity restory because we update a subject identifier
934
                    // which is not same object as our entity that's why we don't need to worry about our entity to get dirty
935
                    // also, we disable listeners because we call them on our own in persistence layer
936
                    const softDeleteQueryBuilder = this.queryRunner.manager
36✔
937
                        .createQueryBuilder()
938
                        .restore()
939
                        .from(subject.metadata.target)
940
                        .updateEntity(
941
                            this.options && this.options.reload === false
78!
942
                                ? false
943
                                : true,
944
                        )
945
                        .callListeners(false)
946

947
                    if (subject.entity) {
36!
948
                        softDeleteQueryBuilder.whereEntity(subject.identifier)
36✔
949
                    } else {
950
                        // in this case identifier is just conditions object to update by
951
                        softDeleteQueryBuilder.where(subject.identifier)
×
952
                    }
953

954
                    updateResult = await softDeleteQueryBuilder.execute()
36✔
955
                }
956

957
                subject.generatedMap = updateResult.generatedMaps[0]
24✔
958
                if (subject.generatedMap) {
24✔
959
                    subject.metadata.columns.forEach((column) => {
24✔
960
                        const value = column.getEntityValue(
144✔
961
                            subject.generatedMap!,
962
                        )
963
                        if (value !== undefined && value !== null) {
144✔
964
                            const preparedValue =
965
                                this.queryRunner.connection.driver.prepareHydratedValue(
26✔
966
                                    value,
967
                                    column,
968
                                )
969
                            column.setEntityValue(
26✔
970
                                subject.generatedMap!,
971
                                preparedValue,
972
                            )
973
                        }
974
                    })
975
                }
976

977
                // experiments, remove probably, need to implement tree tables children removal
978
                // if (subject.updatedRelationMaps.length > 0) {
979
                //     await Promise.all(subject.updatedRelationMaps.map(async updatedRelation => {
980
                //         if (!updatedRelation.relation.isTreeParent) return;
981
                //         if (!updatedRelation.value !== null) return;
982
                //
983
                //         if (subject.metadata.treeType === "closure-table") {
984
                //             await new ClosureSubjectExecutor(this.queryRunner).deleteChildrenOf(subject);
985
                //         }
986
                //     }));
987
                // }
988
            }),
989
        )
990
    }
991

992
    /**
993
     * Updates all special columns of the saving entities (create date, update date, version, etc.).
994
     * Also updates nullable columns and columns with default values.
995
     */
996
    protected updateSpecialColumnsInPersistedEntities(): void {
997
        // update inserted entity properties
998
        if (this.insertSubjects.length)
39,434✔
999
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
38,465✔
1000
                this.insertSubjects,
1001
            )
1002

1003
        // update updated entity properties
1004
        if (this.updateSubjects.length)
39,434✔
1005
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
1,397✔
1006
                this.updateSubjects,
1007
            )
1008

1009
        // update soft-removed entity properties
1010
        if (this.softRemoveSubjects.length)
39,434✔
1011
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
102✔
1012
                this.softRemoveSubjects,
1013
            )
1014

1015
        // update recovered entity properties
1016
        if (this.recoverSubjects.length)
39,434✔
1017
            this.updateSpecialColumnsInInsertedAndUpdatedEntities(
24✔
1018
                this.recoverSubjects,
1019
            )
1020

1021
        // remove ids from the entities that were removed
1022
        if (this.removeSubjects.length) {
39,434✔
1023
            this.removeSubjects.forEach((subject) => {
363✔
1024
                if (!subject.entity) return
524✔
1025

1026
                subject.metadata.primaryColumns.forEach((primaryColumn) => {
302✔
1027
                    primaryColumn.setEntityValue(subject.entity!, undefined)
388✔
1028
                })
1029
            })
1030
        }
1031

1032
        // other post-persist updations
1033
        this.allSubjects.forEach((subject) => {
39,434✔
1034
            if (!subject.entity) return
117,304✔
1035

1036
            subject.metadata.relationIds.forEach((relationId) => {
104,323✔
1037
                relationId.setValue(subject.entity!)
1,922✔
1038
            })
1039

1040
            // mongo _id remove
1041
            if (
104,323!
1042
                InstanceChecker.isMongoEntityManager(this.queryRunner.manager)
1043
            ) {
UNCOV
1044
                if (
×
1045
                    subject.metadata.objectIdColumn &&
×
1046
                    subject.metadata.objectIdColumn.databaseName &&
1047
                    subject.metadata.objectIdColumn.databaseName !==
1048
                        subject.metadata.objectIdColumn.propertyName
1049
                ) {
UNCOV
1050
                    delete subject.entity[
×
1051
                        subject.metadata.objectIdColumn.databaseName
1052
                    ]
1053
                }
1054
            }
1055
        })
1056
    }
1057

1058
    /**
1059
     * Updates all special columns of the saving entities (create date, update date, version, etc.).
1060
     * Also updates nullable columns and columns with default values.
1061
     */
1062
    protected updateSpecialColumnsInInsertedAndUpdatedEntities(
1063
        subjects: Subject[],
1064
    ): void {
1065
        subjects.forEach((subject) => {
39,988✔
1066
            if (!subject.entity) return
116,291✔
1067

1068
            // set values to "null" for nullable columns that did not have values
1069
            subject.metadata.columns.forEach((column) => {
103,532✔
1070
                // if table inheritance is used make sure this column is not child's column
1071
                if (
372,066✔
1072
                    subject.metadata.childEntityMetadatas.length > 0 &&
372,180✔
1073
                    subject.metadata.childEntityMetadatas
1074
                        .map((metadata) => metadata.target)
222✔
1075
                        .indexOf(column.target) !== -1
1076
                )
1077
                    return
37✔
1078

1079
                // entities does not have virtual columns
1080
                if (column.isVirtual) return
372,029✔
1081

1082
                // if column is deletedAt
1083
                if (column.isDeleteDate) return
354,920✔
1084

1085
                // update nullable columns
1086
                if (column.isNullable) {
354,314✔
1087
                    const columnValue = column.getEntityValue(subject.entity!)
3,143✔
1088
                    if (columnValue === undefined)
3,143✔
1089
                        column.setEntityValue(subject.entity!, null)
1,617✔
1090
                }
1091

1092
                // update relational columns
1093
                if (subject.updatedRelationMaps.length > 0) {
354,314✔
1094
                    subject.updatedRelationMaps.forEach(
15,056✔
1095
                        (updatedRelationMap) => {
1096
                            updatedRelationMap.relation.joinColumns.forEach(
16,782✔
1097
                                (column) => {
1098
                                    if (column.isVirtual === true) return
18,334✔
1099

1100
                                    column.setEntityValue(
1,638✔
1101
                                        subject.entity!,
1102
                                        ObjectUtils.isObject(
1,638✔
1103
                                            updatedRelationMap.value,
1104
                                        )
1105
                                            ? column.referencedColumn!.getEntityValue(
1106
                                                  updatedRelationMap.value,
1107
                                              )
1108
                                            : updatedRelationMap.value,
1109
                                    )
1110
                                },
1111
                            )
1112
                        },
1113
                    )
1114
                }
1115
            })
1116

1117
            // merge into entity all generated values returned by a database
1118
            if (subject.generatedMap)
103,532✔
1119
                this.queryRunner.manager.merge(
103,016✔
1120
                    subject.metadata.target as any,
1121
                    subject.entity,
1122
                    subject.generatedMap,
1123
                )
1124
        })
1125
    }
1126

1127
    /**
1128
     * Groups subjects by metadata names (by tables) to make bulk insertions and deletions possible.
1129
     * However there are some limitations with bulk insertions of data into tables with generated (increment) columns
1130
     * in some drivers. Some drivers like mysql and sqlite does not support returning multiple generated columns
1131
     * after insertion and can only return a single generated column value, that's why its not possible to do bulk insertion,
1132
     * because it breaks insertion result's generatedMap and leads to problems when this subject is used in other subjects saves.
1133
     * That's why we only support bulking in junction tables for those drivers.
1134
     *
1135
     * Other drivers like postgres and sql server support RETURNING / OUTPUT statement which allows to return generated
1136
     * id for each inserted row, that's why bulk insertion is not limited to junction tables in there.
1137
     */
1138
    protected groupBulkSubjects(
1139
        subjects: Subject[],
1140
        type: "insert" | "delete",
1141
    ): [{ [key: string]: Subject[] }, string[]] {
1142
        const group: { [key: string]: Subject[] } = {}
78,951✔
1143
        const keys: string[] = []
78,951✔
1144
        const hasReturningDependColumns = subjects.some((subject) => {
78,951✔
1145
            return subject.metadata.getInsertionReturningColumns().length > 0
63,203✔
1146
        })
1147
        const groupingAllowed =
1148
            type === "delete" ||
78,951✔
1149
            this.queryRunner.connection.driver.isReturningSqlSupported(
1150
                "insert",
1151
            ) ||
1152
            hasReturningDependColumns === false
1153

1154
        subjects.forEach((subject, index) => {
78,951✔
1155
            const key =
1156
                groupingAllowed || subject.metadata.isJunction
114,560✔
1157
                    ? subject.metadata.name
1158
                    : subject.metadata.name + "_" + index
1159
            if (!group[key]) {
114,560✔
1160
                group[key] = [subject]
50,479✔
1161
                keys.push(key)
50,479✔
1162
            } else {
1163
                group[key].push(subject)
64,081✔
1164
            }
1165
        })
1166

1167
        return [group, keys]
78,951✔
1168
    }
1169
}
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