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

typeorm / typeorm / 14743371524

29 Apr 2025 11:33PM UTC coverage: 76.307% (-0.01%) from 76.317%
14743371524

push

github

web-flow
fix: change how array columns are compared on column changed detection (#11269)

* fix: change how array columns are compared on column changed detection

Closes: #5967

* add tests with date array colum

* Normalize date arrays before comparing

9199 of 12761 branches covered (72.09%)

Branch coverage included in aggregate %.

10 of 16 new or added lines in 1 file covered. (62.5%)

1 existing line in 1 file now uncovered.

18801 of 23933 relevant lines covered (78.56%)

168699.43 hits per line

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

81.41
/src/persistence/SubjectChangedColumnsComputer.ts
1
import { Subject } from "./Subject"
2
import { DateUtils } from "../util/DateUtils"
37✔
3
import { ObjectLiteral } from "../common/ObjectLiteral"
4
import { OrmUtils } from "../util/OrmUtils"
37✔
5
import { ApplyValueTransformers } from "../util/ApplyValueTransformers"
37✔
6
import { ObjectUtils } from "../util/ObjectUtils"
37✔
7

8
/**
9
 * Finds what columns are changed in the subject entities.
10
 */
11
export class SubjectChangedColumnsComputer {
37✔
12
    // -------------------------------------------------------------------------
13
    // Public Methods
14
    // -------------------------------------------------------------------------
15

16
    /**
17
     * Finds what columns are changed in the subject entities.
18
     */
19
    compute(subjects: Subject[]) {
20
        subjects.forEach((subject) => {
234,273✔
21
            this.computeDiffColumns(subject)
548,749✔
22
            this.computeDiffRelationalColumns(subjects, subject)
548,749✔
23
        })
24
    }
25

26
    // -------------------------------------------------------------------------
27
    // Protected Methods
28
    // -------------------------------------------------------------------------
29

30
    /**
31
     * Differentiate columns from the updated entity and entity stored in the database.
32
     */
33
    protected computeDiffColumns(subject: Subject): void {
34
        // if there is no persisted entity then nothing to compute changed in it
35
        if (!subject.entity) return
548,749✔
36

37
        subject.metadata.columns.forEach((column) => {
468,769✔
38
            // ignore special columns
39
            if (
1,632,237✔
40
                column.isVirtual ||
3,156,154✔
41
                column.isDiscriminator // ||
42
                // column.isUpdateDate ||
43
                // column.isVersion ||
44
                // column.isCreateDate
45
            )
46
                return
108,914✔
47

48
            const changeMap = subject.changeMaps.find(
1,523,323✔
49
                (changeMap) => changeMap.column === column,
1,675,739✔
50
            )
51
            if (changeMap) {
1,523,323✔
52
                subject.changeMaps.splice(
1,641✔
53
                    subject.changeMaps.indexOf(changeMap),
54
                    1,
55
                )
56
            }
57

58
            // get user provided value - column value from the user provided persisted entity
59
            const entityValue = column.getEntityValue(subject.entity!)
1,523,323✔
60

61
            // we don't perform operation over undefined properties (but we DO need null properties!)
62
            if (entityValue === undefined) return
1,523,323✔
63

64
            // if there is no database entity then all columns are treated as new, e.g. changed
65
            if (subject.databaseEntity) {
1,260,011✔
66
                // skip transform database value for json / jsonb for comparison later on
67
                const shouldTransformDatabaseEntity =
68
                    column.type !== "json" && column.type !== "jsonb"
37,044✔
69

70
                // get database value of the column
71
                let databaseValue = column.getEntityValue(
37,044✔
72
                    subject.databaseEntity,
73
                    shouldTransformDatabaseEntity,
74
                )
75

76
                // filter out "relational columns" only in the case if there is a relation object in entity
77
                if (column.relationMetadata) {
37,044✔
78
                    const value = column.relationMetadata.getEntityValue(
703✔
79
                        subject.entity!,
80
                    )
81
                    if (value !== null && value !== undefined) return
703✔
82
                }
83
                let normalizedValue = entityValue
36,956✔
84
                // normalize special values to make proper comparision
85
                if (entityValue !== null) {
36,956✔
86
                    switch (column.type) {
35,151!
87
                        case "date":
88
                            normalizedValue = column.isArray
17✔
89
                                ? entityValue.map((date: Date) =>
90
                                      DateUtils.mixedDateToDateString(date),
15✔
91
                                  )
92
                                : DateUtils.mixedDateToDateString(entityValue)
93
                            databaseValue = column.isArray
17✔
94
                                ? databaseValue.map((date: Date) =>
95
                                      DateUtils.mixedDateToDateString(date),
15✔
96
                                  )
97
                                : DateUtils.mixedDateToDateString(databaseValue)
98
                            break
17✔
99

100
                        case "time":
101
                        case "time with time zone":
102
                        case "time without time zone":
103
                        case "timetz":
NEW
104
                            normalizedValue = column.isArray
×
105
                                ? entityValue.map((date: Date) =>
NEW
106
                                      DateUtils.mixedDateToTimeString(date),
×
107
                                  )
108
                                : DateUtils.mixedDateToTimeString(entityValue)
NEW
109
                            databaseValue = column.isArray
×
110
                                ? databaseValue.map((date: Date) =>
NEW
111
                                      DateUtils.mixedDateToTimeString(date),
×
112
                                  )
113
                                : DateUtils.mixedDateToTimeString(databaseValue)
UNCOV
114
                            break
×
115

116
                        case "datetime":
117
                        case "datetime2":
118
                        case Date:
119
                        case "timestamp":
120
                        case "timestamp without time zone":
121
                        case "timestamp with time zone":
122
                        case "timestamp with local time zone":
123
                        case "timestamptz":
124
                            normalizedValue = column.isArray
1,110!
125
                                ? entityValue.map((date: Date) =>
NEW
126
                                      DateUtils.mixedDateToUtcDatetimeString(
×
127
                                          date,
128
                                      ),
129
                                  )
130
                                : DateUtils.mixedDateToUtcDatetimeString(
131
                                      entityValue,
132
                                  )
133

134
                            databaseValue = column.isArray
1,110!
135
                                ? databaseValue.map((date: Date) =>
NEW
136
                                      DateUtils.mixedDateToUtcDatetimeString(
×
137
                                          date,
138
                                      ),
139
                                  )
140
                                : DateUtils.mixedDateToUtcDatetimeString(
141
                                      databaseValue,
142
                                  )
143

144
                            break
1,110✔
145

146
                        case "json":
147
                        case "jsonb":
148
                            // JSON.stringify doesn't work because postgresql sorts jsonb before save.
149
                            // If you try to save json '[{"messages": "", "attribute Key": "", "level":""}] ' as jsonb,
150
                            // then postgresql will save it as '[{"level": "", "message":"", "attributeKey": ""}]'
151
                            if (
12✔
152
                                OrmUtils.deepCompare(entityValue, databaseValue)
153
                            )
154
                                return
9✔
155
                            break
3✔
156

157
                        case "simple-array":
158
                            normalizedValue =
×
159
                                DateUtils.simpleArrayToString(entityValue)
160
                            databaseValue =
×
161
                                DateUtils.simpleArrayToString(databaseValue)
162
                            break
×
163
                        case "simple-enum":
164
                            normalizedValue =
×
165
                                DateUtils.simpleEnumToString(entityValue)
166
                            databaseValue =
×
167
                                DateUtils.simpleEnumToString(databaseValue)
168
                            break
×
169
                        case "simple-json":
170
                            normalizedValue =
×
171
                                DateUtils.simpleJsonToString(entityValue)
172
                            databaseValue =
×
173
                                DateUtils.simpleJsonToString(databaseValue)
174
                            break
×
175
                    }
176

177
                    if (column.transformer) {
35,142✔
178
                        normalizedValue = ApplyValueTransformers.transformTo(
369✔
179
                            column.transformer,
180
                            entityValue,
181
                        )
182
                    }
183
                }
184

185
                // if value is not changed - then do nothing
186
                if (column.isArray) {
36,947✔
187
                    if (OrmUtils.deepCompare(normalizedValue, databaseValue))
21✔
188
                        return
15✔
189
                } else if (
36,926✔
190
                    Buffer.isBuffer(normalizedValue) &&
36,932✔
191
                    Buffer.isBuffer(databaseValue)
192
                ) {
193
                    if (normalizedValue.equals(databaseValue)) {
6✔
194
                        return
6✔
195
                    }
196
                } else {
197
                    if (normalizedValue === databaseValue) return
36,920✔
198
                }
199
            }
200

201
            if (!subject.diffColumns.includes(column))
1,228,740✔
202
                subject.diffColumns.push(column)
1,227,136✔
203

204
            subject.changeMaps.push({
1,228,740✔
205
                column: column,
206
                value: entityValue,
207
            })
208
        })
209
    }
210

211
    /**
212
     * Difference columns of the owning one-to-one and many-to-one columns.
213
     */
214
    protected computeDiffRelationalColumns(
215
        allSubjects: Subject[],
216
        subject: Subject,
217
    ): void {
218
        // if there is no persisted entity then nothing to compute changed in it
219
        if (!subject.entity) return
548,749✔
220

221
        subject.metadata.relationsWithJoinColumns.forEach((relation) => {
468,769✔
222
            // get the related entity from the persisted entity
223
            let relatedEntity = relation.getEntityValue(subject.entity!)
87,188✔
224

225
            // we don't perform operation over undefined properties (but we DO need null properties!)
226
            if (relatedEntity === undefined) return
87,188✔
227

228
            // if there is no database entity then all relational columns are treated as new, e.g. changed
229
            if (subject.databaseEntity) {
30,483✔
230
                // here we cover two scenarios:
231
                // 1. related entity can be another entity which is natural way
232
                // 2. related entity can be just an entity id
233
                // if relation entity is just a relation id set (for example post.tag = 1)
234
                // then we create an id map from it to make a proper comparision
235
                let relatedEntityRelationIdMap: ObjectLiteral = relatedEntity
2,074✔
236
                if (
2,074✔
237
                    relatedEntityRelationIdMap !== null &&
3,910✔
238
                    ObjectUtils.isObject(relatedEntityRelationIdMap)
239
                )
240
                    relatedEntityRelationIdMap = relation.getRelationIdMap(
1,762✔
241
                        relatedEntityRelationIdMap,
242
                    )!
243

244
                // get database related entity. Since loadRelationIds are used on databaseEntity
245
                // related entity will contain only its relation ids
246
                const databaseRelatedEntityRelationIdMap =
247
                    relation.getEntityValue(subject.databaseEntity)
2,074✔
248

249
                // if relation ids are equal then we don't need to update anything
250
                const areRelatedIdsEqual = OrmUtils.compareIds(
2,074✔
251
                    relatedEntityRelationIdMap,
252
                    databaseRelatedEntityRelationIdMap,
253
                )
254
                if (areRelatedIdsEqual) {
2,074✔
255
                    return
575✔
256
                } else {
257
                    subject.diffRelations.push(relation)
1,499✔
258
                }
259
            }
260

261
            // if there is an inserted subject for the related entity of the persisted entity then use it as related entity
262
            // this code is used for related entities without ids to be properly inserted (and then updated if needed)
263
            const valueSubject = allSubjects.find(
29,908✔
264
                (subject) =>
265
                    subject.mustBeInserted && subject.entity === relatedEntity,
54,229✔
266
            )
267
            if (valueSubject) relatedEntity = valueSubject
29,908✔
268

269
            // find if there is already a relation to be changed
270
            const changeMap = subject.changeMaps.find(
29,908✔
271
                (changeMap) => changeMap.relation === relation,
62,629✔
272
            )
273
            if (changeMap) {
29,908✔
274
                // and update its value if it was found
275
                changeMap.value = relatedEntity
222✔
276
            } else {
277
                // if it wasn't found add a new relation for change
278
                subject.changeMaps.push({
29,686✔
279
                    relation: relation,
280
                    value: relatedEntity,
281
                })
282
            }
283
        })
284
    }
285
}
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

© 2026 Coveralls, Inc