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

typeorm / typeorm / 15089093306

17 May 2025 09:03PM CUT 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

76.88
/src/metadata-builder/EntityMetadataValidator.ts
1
import { EntityMetadata } from "../metadata/EntityMetadata"
2
import { MissingPrimaryColumnError } from "../error/MissingPrimaryColumnError"
6✔
3
import { CircularRelationsError } from "../error/CircularRelationsError"
6✔
4
import { DepGraph } from "../util/DepGraph"
6✔
5
import { Driver } from "../driver/Driver"
6
import { DataTypeNotSupportedError } from "../error/DataTypeNotSupportedError"
6✔
7
import { ColumnType } from "../driver/types/ColumnTypes"
8
import { NoConnectionOptionError } from "../error/NoConnectionOptionError"
6✔
9
import { InitializedRelationError } from "../error/InitializedRelationError"
6✔
10
import { TypeORMError } from "../error"
6✔
11
import { DriverUtils } from "../driver/DriverUtils"
6✔
12

13
/// todo: add check if there are multiple tables with the same name
14
/// todo: add checks when generated column / table names are too long for the specific driver
15
// todo: type in function validation, inverse side function validation
16
// todo: check on build for duplicate names, since naming checking was removed from MetadataStorage
17
// todo: duplicate name checking for: table, relation, column, index, naming strategy, join tables/columns?
18
// todo: check if multiple tree parent metadatas in validator
19
// todo: tree decorators can be used only on closure table (validation)
20
// todo: throw error if parent tree metadata was not specified in a closure table
21

22
// todo: MetadataArgsStorage: type in function validation, inverse side function validation
23
// todo: MetadataArgsStorage: check on build for duplicate names, since naming checking was removed from MetadataStorage
24
// todo: MetadataArgsStorage: duplicate name checking for: table, relation, column, index, naming strategy, join tables/columns?
25
// todo: MetadataArgsStorage: check for duplicate targets too since this check has been removed too
26
// todo: check if relation decorator contains primary: true and nullable: true
27
// todo: check column length, precision. scale
28
// todo: MySQL index can be unique or spatial or fulltext
29

30
/**
31
 * Validates built entity metadatas.
32
 */
33
export class EntityMetadataValidator {
6✔
34
    // -------------------------------------------------------------------------
35
    // Public Methods
36
    // -------------------------------------------------------------------------
37

38
    /**
39
     * Validates all given entity metadatas.
40
     */
41
    validateMany(entityMetadatas: EntityMetadata[], driver: Driver) {
42
        entityMetadatas.forEach((entityMetadata) =>
2,978✔
43
            this.validate(entityMetadata, entityMetadatas, driver),
7,467✔
44
        )
45
        this.validateDependencies(entityMetadatas)
2,948✔
46
        this.validateEagerRelations(entityMetadatas)
2,942✔
47
    }
48

49
    /**
50
     * Validates given entity metadata.
51
     */
52
    validate(
53
        entityMetadata: EntityMetadata,
54
        allEntityMetadatas: EntityMetadata[],
55
        driver: Driver,
56
    ) {
57
        // check if table metadata has an id
58
        if (!entityMetadata.primaryColumns.length && !entityMetadata.isJunction)
7,467!
59
            throw new MissingPrimaryColumnError(entityMetadata)
×
60

61
        // if entity has multiple primary keys and uses custom constraint name,
62
        // then all primary keys should have the same constraint name
63
        if (entityMetadata.primaryColumns.length > 1) {
7,467✔
64
            const areConstraintNamesEqual = entityMetadata.primaryColumns.every(
1,155✔
65
                (columnMetadata, i, columnMetadatas) =>
66
                    columnMetadata.primaryKeyConstraintName ===
2,558✔
67
                    columnMetadatas[0].primaryKeyConstraintName,
68
            )
69
            if (!areConstraintNamesEqual) {
1,155!
70
                throw new TypeORMError(
×
71
                    `Entity ${entityMetadata.name} has multiple primary columns with different constraint names. Constraint names should be the equal.`,
72
                )
73
            }
74
        }
75

76
        // validate if table is using inheritance it has a discriminator
77
        // also validate if discriminator values are not empty and not repeated
78
        if (
7,467✔
79
            entityMetadata.inheritancePattern === "STI" ||
14,487✔
80
            entityMetadata.tableType === "entity-child"
81
        ) {
82
            if (!entityMetadata.discriminatorColumn)
447!
83
                throw new TypeORMError(
×
84
                    `Entity ${entityMetadata.name} using single-table inheritance, it should also have a discriminator column. Did you forget to put discriminator column options?`,
85
                )
86

87
            if (typeof entityMetadata.discriminatorValue === "undefined")
447!
88
                throw new TypeORMError(
×
89
                    `Entity ${entityMetadata.name} has an undefined discriminator value. Discriminator value should be defined.`,
90
                )
91

92
            const sameDiscriminatorValueEntityMetadata =
93
                allEntityMetadatas.find((metadata) => {
447✔
94
                    return (
2,447✔
95
                        metadata !== entityMetadata &&
7,485✔
96
                        (metadata.inheritancePattern === "STI" ||
97
                            metadata.tableType === "entity-child") &&
98
                        metadata.tableName === entityMetadata.tableName &&
99
                        metadata.discriminatorValue ===
100
                            entityMetadata.discriminatorValue &&
101
                        metadata.inheritanceTree.some(
102
                            (parent) =>
103
                                entityMetadata.inheritanceTree.indexOf(
12✔
104
                                    parent,
105
                                ) !== -1,
106
                        )
107
                    )
108
                })
109
            if (sameDiscriminatorValueEntityMetadata)
447✔
110
                throw new TypeORMError(
6✔
111
                    `Entities ${entityMetadata.name} and ${sameDiscriminatorValueEntityMetadata.name} have the same discriminator values. Make sure they are different while using the @ChildEntity decorator.`,
112
                )
113
        }
114

115
        entityMetadata.relationCounts.forEach((relationCount) => {
7,461✔
116
            if (
72✔
117
                relationCount.relation.isManyToOne ||
144✔
118
                relationCount.relation.isOneToOne
119
            )
120
                throw new TypeORMError(
6✔
121
                    `Relation count can not be implemented on ManyToOne or OneToOne relations.`,
122
                )
123
        })
124

125
        if (!(driver.options.type === "mongodb")) {
7,455✔
126
            entityMetadata.columns
7,455✔
127
                .filter((column) => !column.isVirtualProperty)
23,238✔
128
                .forEach((column) => {
129
                    const normalizedColumn = driver.normalizeType(
23,220✔
130
                        column,
131
                    ) as ColumnType
132
                    if (
23,220!
133
                        driver.supportedDataTypes.indexOf(normalizedColumn) ===
134
                        -1
135
                    )
136
                        throw new DataTypeNotSupportedError(
×
137
                            column,
138
                            normalizedColumn,
139
                            driver.options.type,
140
                        )
141
                    if (
23,220!
142
                        column.length &&
23,632✔
143
                        driver.withLengthColumnTypes.indexOf(
144
                            normalizedColumn,
145
                        ) === -1
146
                    )
147
                        throw new TypeORMError(
×
148
                            `Column ${column.propertyName} of Entity ${entityMetadata.name} does not support length property.`,
149
                        )
150
                    if (
23,220!
151
                        column.type === "enum" &&
23,291✔
152
                        !column.enum &&
153
                        !column.enumName
154
                    )
155
                        throw new TypeORMError(
×
156
                            `Column "${column.propertyName}" of Entity "${entityMetadata.name}" is defined as enum, but missing "enum" or "enumName" properties.`,
157
                        )
158
                })
159
        }
160

161
        if (
7,455✔
162
            DriverUtils.isMySQLFamily(driver) ||
14,814✔
163
            driver.options.type === "aurora-mysql"
164
        ) {
165
            const generatedColumns = entityMetadata.columns.filter(
96✔
166
                (column) =>
167
                    column.isGenerated && column.generationStrategy !== "uuid",
252✔
168
            )
169
            if (generatedColumns.length > 1)
96!
170
                throw new TypeORMError(
×
171
                    `Error in ${entityMetadata.name} entity. There can be only one auto-increment column in MySql table.`,
172
                )
173
        }
174

175
        // for mysql we are able to not define a default selected database, instead all entities can have their database
176
        // defined in their decorators. To make everything work either all entities must have database define and we
177
        // can live without database set in the connection options, either database in the connection options must be set
178
        if (DriverUtils.isMySQLFamily(driver)) {
7,455✔
179
            const metadatasWithDatabase = allEntityMetadatas.filter(
96✔
180
                (metadata) => metadata.database,
282✔
181
            )
182
            if (metadatasWithDatabase.length === 0 && !driver.database)
96!
183
                throw new NoConnectionOptionError("database")
×
184
        }
185

186
        if (driver.options.type === "mssql") {
7,455!
UNCOV
187
            const charsetColumns = entityMetadata.columns.filter(
×
UNCOV
188
                (column) => column.charset,
×
189
            )
UNCOV
190
            if (charsetColumns.length > 1)
×
191
                throw new TypeORMError(
×
192
                    `Character set specifying is not supported in Sql Server`,
193
                )
194
        }
195

196
        // Postgres supports only STORED generated columns.
197
        if (driver.options.type === "postgres") {
7,455✔
198
            const virtualColumn = entityMetadata.columns.find(
1,547✔
199
                (column) =>
200
                    column.asExpression &&
4,976✔
201
                    (!column.generatedType ||
202
                        column.generatedType === "VIRTUAL"),
203
            )
204
            if (virtualColumn)
1,547!
205
                throw new TypeORMError(
×
206
                    `Column "${virtualColumn.propertyName}" of Entity "${entityMetadata.name}" is defined as VIRTUAL, but Postgres supports only STORED generated columns.`,
207
                )
208
        }
209

210
        // check if relations are all without initialized properties
211
        const entityInstance = entityMetadata.create(undefined, {
7,455✔
212
            fromDeserializer: true,
213
        })
214
        entityMetadata.relations.forEach((relation) => {
7,449✔
215
            if (relation.isManyToMany || relation.isOneToMany) {
4,830✔
216
                // we skip relations for which persistence is disabled since initialization in them cannot harm somehow
217
                if (relation.persistenceEnabled === false) return
2,240✔
218

219
                // get entity relation value and check if its an array
220
                const relationInitializedValue =
221
                    relation.getEntityValue(entityInstance)
2,234✔
222
                if (Array.isArray(relationInitializedValue))
2,234✔
223
                    throw new InitializedRelationError(relation)
12✔
224
            }
225
        })
226

227
        // validate relations
228
        entityMetadata.relations.forEach((relation) => {
7,437✔
229
            // check OnDeleteTypes
230
            if (
4,812!
231
                driver.supportedOnDeleteTypes &&
4,812!
232
                relation.onDelete &&
233
                !driver.supportedOnDeleteTypes.includes(relation.onDelete)
234
            ) {
UNCOV
235
                throw new TypeORMError(
×
236
                    `OnDeleteType "${relation.onDelete}" is not supported for ${driver.options.type}!`,
237
                )
238
            }
239

240
            // check OnUpdateTypes
241
            if (
4,812!
242
                driver.supportedOnUpdateTypes &&
4,812!
243
                relation.onUpdate &&
244
                !driver.supportedOnUpdateTypes.includes(relation.onUpdate)
245
            ) {
246
                throw new TypeORMError(
×
247
                    `OnUpdateType "${relation.onUpdate}" is not valid for ${driver.options.type}!`,
248
                )
249
            }
250

251
            // check join tables:
252
            // using JoinTable is possible only on one side of the many-to-many relation
253
            // todo(dima): fix
254
            // if (relation.joinTable) {
255
            //     if (!relation.isManyToMany)
256
            //         throw new UsingJoinTableIsNotAllowedError(entityMetadata, relation);
257
            //     // if there is inverse side of the relation, then check if it does not have join table too
258
            //     if (relation.hasInverseSide && relation.inverseRelation.joinTable)
259
            //         throw new UsingJoinTableOnlyOnOneSideAllowedError(entityMetadata, relation);
260
            // }
261
            // check join columns:
262
            // using JoinColumn is possible only on one side of the relation and on one-to-one, many-to-one relation types
263
            // first check if relation is one-to-one or many-to-one
264
            // todo(dima): fix
265
            /*if (relation.joinColumn) {
266

267
                // join column can be applied only on one-to-one and many-to-one relations
268
                if (!relation.isOneToOne && !relation.isManyToOne)
269
                    throw new UsingJoinColumnIsNotAllowedError(entityMetadata, relation);
270

271
                // if there is inverse side of the relation, then check if it does not have join table too
272
                if (relation.hasInverseSide && relation.inverseRelation.joinColumn && relation.isOneToOne)
273
                    throw new UsingJoinColumnOnlyOnOneSideAllowedError(entityMetadata, relation);
274

275
                // check if join column really has referenced column
276
                if (relation.joinColumn && !relation.joinColumn.referencedColumn)
277
                    throw new TypeORMError(`Join column does not have referenced column set`);
278

279
            }
280

281
            // if its a one-to-one relation and JoinColumn is missing on both sides of the relation
282
            // or its one-side relation without JoinColumn we should give an error
283
            if (!relation.joinColumn && relation.isOneToOne && (!relation.hasInverseSide || !relation.inverseRelation.joinColumn))
284
                throw new MissingJoinColumnError(entityMetadata, relation);*/
285
            // if its a many-to-many relation and JoinTable is missing on both sides of the relation
286
            // or its one-side relation without JoinTable we should give an error
287
            // todo(dima): fix it
288
            // if (!relation.joinTable && relation.isManyToMany && (!relation.hasInverseSide || !relation.inverseRelation.joinTable))
289
            //     throw new MissingJoinTableError(entityMetadata, relation);
290
            // todo: validate if its one-to-one and side which does not have join column MUST have inverse side
291
            // todo: validate if its many-to-many and side which does not have join table MUST have inverse side
292
            // todo: if there is a relation, and inverse side is specified only on one side, shall we give error
293
            // todo: with message like: "Inverse side is specified only on one side of the relationship. Specify on other side too to prevent confusion".
294
            // todo: add validation if there two entities with the same target, and show error message with description of the problem (maybe file was renamed/moved but left in output directory)
295
            // todo: check if there are multiple columns on the same column applied.
296
            // todo: check column type if is missing in relational databases (throw new TypeORMError(`Column type of ${type} cannot be determined.`);)
297
            // todo: include driver-specific checks. for example in mongodb empty prefixes are not allowed
298
            // todo: if multiple columns with same name - throw exception, including cases when columns are in embeds with same prefixes or without prefix at all
299
            // todo: if multiple primary key used, at least one of them must be unique or @Index decorator must be set on entity
300
            // todo: check if entity with duplicate names, some decorators exist
301
        })
302

303
        // make sure cascade remove is not set for both sides of relationships (can be set in OneToOne decorators)
304
        entityMetadata.relations.forEach((relation) => {
7,437✔
305
            const isCircularCascadeRemove =
306
                relation.isCascadeRemove &&
4,812✔
307
                relation.inverseRelation &&
308
                relation.inverseRelation!.isCascadeRemove
309
            if (isCircularCascadeRemove)
4,812!
310
                throw new TypeORMError(
×
311
                    `Relation ${entityMetadata.name}#${
312
                        relation.propertyName
313
                    } and ${relation.inverseRelation!.entityMetadata.name}#${
314
                        relation.inverseRelation!.propertyName
315
                    } both has cascade remove set. ` +
316
                        `This may lead to unexpected circular removals. Please set cascade remove only from one side of relationship.`,
317
                )
318
        }) // todo: maybe better just deny removal from one to one relation without join column?
319

320
        entityMetadata.eagerRelations.forEach((relation) => {})
7,437✔
321
    }
322

323
    /**
324
     * Validates dependencies of the entity metadatas.
325
     */
326
    protected validateDependencies(entityMetadatas: EntityMetadata[]) {
327
        const graph = new DepGraph()
2,948✔
328
        entityMetadatas.forEach((entityMetadata) => {
2,948✔
329
            graph.addNode(entityMetadata.name)
7,413✔
330
        })
331
        entityMetadatas.forEach((entityMetadata) => {
2,948✔
332
            entityMetadata.relationsWithJoinColumns
7,413✔
333
                .filter((relation) => !relation.isNullable)
2,325✔
334
                .forEach((relation) => {
335
                    graph.addDependency(
104✔
336
                        entityMetadata.name,
337
                        relation.inverseEntityMetadata.name,
338
                    )
339
                })
340
        })
341
        try {
2,948✔
342
            graph.overallOrder()
2,948✔
343
        } catch (err) {
344
            throw new CircularRelationsError(
6✔
345
                err.toString().replace("Error: Dependency Cycle Found: ", ""),
346
            )
347
        }
348
    }
349

350
    /**
351
     * Validates eager relations to prevent circular dependency in them.
352
     */
353
    protected validateEagerRelations(entityMetadatas: EntityMetadata[]) {
354
        entityMetadatas.forEach((entityMetadata) => {
2,942✔
355
            entityMetadata.eagerRelations.forEach((relation) => {
7,377✔
356
                if (
293✔
357
                    relation.inverseRelation &&
470✔
358
                    relation.inverseRelation.isEager
359
                )
360
                    throw new TypeORMError(
6✔
361
                        `Circular eager relations are disallowed. ` +
362
                            `${entityMetadata.targetName}#${relation.propertyPath} contains "eager: true", and its inverse side ` +
363
                            `${relation.inverseEntityMetadata.targetName}#${relation.inverseRelation.propertyPath} contains "eager: true" as well.` +
364
                            ` Remove "eager: true" from one side of the relation.`,
365
                    )
366
            })
367
        })
368
    }
369
}
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