Skip to content

Commit

Permalink
fix: incorrect composite UNIQUE constraints detection (#8364)
Browse files Browse the repository at this point in the history
* added fix and test for #8158

* fixed for other drivers

* fixed column length for Oracle
  • Loading branch information
AlexMesser committed Nov 14, 2021
1 parent b2c8168 commit 29cb891
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 86 deletions.
36 changes: 17 additions & 19 deletions src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts
Expand Up @@ -1270,27 +1270,25 @@ export class AuroraDataApiQueryRunner extends BaseQueryRunner implements QueryRu
.filter(dbColumn => dbColumn["TABLE_NAME"] === dbTable["TABLE_NAME"] && dbColumn["TABLE_SCHEMA"] === dbTable["TABLE_SCHEMA"])
.map(dbColumn => {

const columnUniqueIndex = dbIndices.find(dbIndex => {
if (dbIndex["TABLE_NAME"] !== dbTable["TABLE_NAME"] || dbIndex["TABLE_SCHEMA"] !== dbTable["TABLE_SCHEMA"]) {
return false;
}

// Index is not for this column
if (dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"]) {
return false;
}

const nonUnique = parseInt(dbIndex["NON_UNIQUE"], 10);
return nonUnique === 0;
});
const columnUniqueIndices = dbIndices.filter(dbIndex => {
return dbIndex["TABLE_NAME"] === dbTable["TABLE_NAME"]
&& dbIndex["TABLE_SCHEMA"] === dbTable["TABLE_SCHEMA"]
&& dbIndex["COLUMN_NAME"] === dbColumn["COLUMN_NAME"]
&& parseInt(dbIndex["NON_UNIQUE"], 10) === 0
})

const tableMetadata = this.connection.entityMetadatas.find(metadata => this.getTablePath(table) === this.getTablePath(metadata));
const hasIgnoredIndex = columnUniqueIndex && tableMetadata && tableMetadata.indices
.some(index => index.name === columnUniqueIndex["INDEX_NAME"] && index.synchronize === false);
const hasIgnoredIndex = columnUniqueIndices.length > 0
&& tableMetadata
&& tableMetadata.indices.some(index => {
return columnUniqueIndices.some(uniqueIndex => {
return index.name === uniqueIndex["INDEX_NAME"] && index.synchronize === false;
});
});

const isConstraintComposite = columnUniqueIndex
? !!dbIndices.find(dbIndex => dbIndex["INDEX_NAME"] === columnUniqueIndex["INDEX_NAME"] && dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"])
: false;
const isConstraintComposite = columnUniqueIndices.every((uniqueIndex) => {
return dbIndices.some(dbIndex => dbIndex["INDEX_NAME"] === uniqueIndex["INDEX_NAME"] && dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"]);
})

const tableColumn = new TableColumn();
tableColumn.name = dbColumn["COLUMN_NAME"];
Expand Down Expand Up @@ -1318,7 +1316,7 @@ export class AuroraDataApiQueryRunner extends BaseQueryRunner implements QueryRu
tableColumn.generatedType = dbColumn["EXTRA"].indexOf("VIRTUAL") !== -1 ? "VIRTUAL" : "STORED";
}

tableColumn.isUnique = !!columnUniqueIndex && !hasIgnoredIndex && !isConstraintComposite;
tableColumn.isUnique = columnUniqueIndices.length > 0 && !hasIgnoredIndex && !isConstraintComposite;
tableColumn.isNullable = dbColumn["IS_NULLABLE"] === "YES";
tableColumn.isPrimary = dbPrimaryKeys.some(dbPrimaryKey => {
return (
Expand Down
10 changes: 5 additions & 5 deletions src/driver/cockroachdb/CockroachQueryRunner.ts
Expand Up @@ -1556,13 +1556,13 @@ export class CockroachQueryRunner extends BaseQueryRunner implements QueryRunner
tableColumn.isNullable = dbColumn["is_nullable"] === "YES";
tableColumn.isPrimary = !!columnConstraints.find(constraint => constraint["constraint_type"] === "PRIMARY");

const uniqueConstraint = columnConstraints.find(constraint => constraint["constraint_type"] === "UNIQUE");
const isConstraintComposite = uniqueConstraint
? !!dbConstraints.find(dbConstraint => dbConstraint["constraint_type"] === "UNIQUE"
const uniqueConstraints = columnConstraints.filter(constraint => constraint["constraint_type"] === "UNIQUE");
const isConstraintComposite = uniqueConstraints.every((uniqueConstraint) => {
return dbConstraints.some(dbConstraint => dbConstraint["constraint_type"] === "UNIQUE"
&& dbConstraint["constraint_name"] === uniqueConstraint["constraint_name"]
&& dbConstraint["column_name"] !== dbColumn["column_name"])
: false;
tableColumn.isUnique = !!uniqueConstraint && !isConstraintComposite;
})
tableColumn.isUnique = uniqueConstraints.length > 0 && !isConstraintComposite;

if (dbColumn["column_default"] !== null && dbColumn["column_default"] !== undefined) {
if (dbColumn["column_default"] === "unique_rowid()") {
Expand Down
36 changes: 17 additions & 19 deletions src/driver/mysql/MysqlQueryRunner.ts
Expand Up @@ -1451,27 +1451,25 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
.filter(dbColumn => dbColumn["TABLE_NAME"] === dbTable["TABLE_NAME"] && dbColumn["TABLE_SCHEMA"] === dbTable["TABLE_SCHEMA"])
.map(dbColumn => {

const columnUniqueIndex = dbIndices.find(dbIndex => {
if (dbIndex["TABLE_NAME"] !== dbTable["TABLE_NAME"] || dbIndex["TABLE_SCHEMA"] !== dbTable["TABLE_SCHEMA"]) {
return false;
}

// Index is not for this column
if (dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"]) {
return false;
}

const nonUnique = parseInt(dbIndex["NON_UNIQUE"], 10);
return nonUnique === 0;
});
const columnUniqueIndices = dbIndices.filter(dbIndex => {
return dbIndex["TABLE_NAME"] === dbTable["TABLE_NAME"]
&& dbIndex["TABLE_SCHEMA"] === dbTable["TABLE_SCHEMA"]
&& dbIndex["COLUMN_NAME"] === dbColumn["COLUMN_NAME"]
&& parseInt(dbIndex["NON_UNIQUE"], 10) === 0
})

const tableMetadata = this.connection.entityMetadatas.find(metadata => this.getTablePath(table) === this.getTablePath(metadata));
const hasIgnoredIndex = columnUniqueIndex && tableMetadata && tableMetadata.indices
.some(index => index.name === columnUniqueIndex["INDEX_NAME"] && index.synchronize === false);
const hasIgnoredIndex = columnUniqueIndices.length > 0
&& tableMetadata
&& tableMetadata.indices.some(index => {
return columnUniqueIndices.some(uniqueIndex => {
return index.name === uniqueIndex["INDEX_NAME"] && index.synchronize === false;
});
});

const isConstraintComposite = columnUniqueIndex
? !!dbIndices.find(dbIndex => dbIndex["INDEX_NAME"] === columnUniqueIndex["INDEX_NAME"] && dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"])
: false;
const isConstraintComposite = columnUniqueIndices.every((uniqueIndex) => {
return dbIndices.some(dbIndex => dbIndex["INDEX_NAME"] === uniqueIndex["INDEX_NAME"] && dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"]);
})

const tableColumn = new TableColumn();
tableColumn.name = dbColumn["COLUMN_NAME"];
Expand Down Expand Up @@ -1512,7 +1510,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner {
tableColumn.generatedType = dbColumn["EXTRA"].indexOf("VIRTUAL") !== -1 ? "VIRTUAL" : "STORED";
}

tableColumn.isUnique = !!columnUniqueIndex && !hasIgnoredIndex && !isConstraintComposite;
tableColumn.isUnique = columnUniqueIndices.length > 0 && !hasIgnoredIndex && !isConstraintComposite;
tableColumn.isNullable = dbColumn["IS_NULLABLE"] === "YES";
tableColumn.isPrimary = dbPrimaryKeys.some(dbPrimaryKey => {
return (
Expand Down
2 changes: 1 addition & 1 deletion src/driver/oracle/OracleDriver.ts
Expand Up @@ -697,7 +697,7 @@ export class OracleDriver implements Driver {

const isColumnChanged = tableColumn.name !== columnMetadata.databaseName
|| tableColumn.type !== this.normalizeType(columnMetadata)
|| tableColumn.length !== columnMetadata.length
|| tableColumn.length !== this.getColumnLength(columnMetadata)
|| tableColumn.precision !== columnMetadata.precision
|| tableColumn.scale !== columnMetadata.scale
// || tableColumn.comment !== columnMetadata.comment
Expand Down
22 changes: 10 additions & 12 deletions src/driver/oracle/OracleQueryRunner.ts
Expand Up @@ -1367,18 +1367,16 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
)
);

const uniqueConstraint = columnConstraints.find(constraint => constraint["CONSTRAINT_TYPE"] === "U");
const isConstraintComposite = uniqueConstraint
? !!dbConstraints.find(dbConstraint => (
dbConstraint["OWNER"] === dbColumn["OWNER"] &&
dbConstraint["TABLE_NAME"] === dbColumn["TABLE_NAME"] &&
dbConstraint["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"] &&
dbConstraint["CONSTRAINT_NAME"] === uniqueConstraint["CONSTRAINT_NAME"] &&
dbConstraint["CONSTRAINT_TYPE"] === "U"
)
const uniqueConstraints = columnConstraints.filter(constraint => constraint["CONSTRAINT_TYPE"] === "U");
const isConstraintComposite = uniqueConstraints.every((uniqueConstraint) => {
return dbConstraints.some(dbConstraint => (
dbConstraint["OWNER"] === dbColumn["OWNER"] &&
dbConstraint["TABLE_NAME"] === dbColumn["TABLE_NAME"] &&
dbConstraint["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"] &&
dbConstraint["CONSTRAINT_NAME"] === uniqueConstraint["CONSTRAINT_NAME"] &&
dbConstraint["CONSTRAINT_TYPE"] === "U")
)
: false;
const isUnique = !!uniqueConstraint && !isConstraintComposite;
})

const isPrimary = !!columnConstraints.find(constraint => constraint["CONSTRAINT_TYPE"] === "P");

Expand Down Expand Up @@ -1411,7 +1409,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
&& dbColumn["DATA_DEFAULT"].trim() !== "NULL" ? tableColumn.default = dbColumn["DATA_DEFAULT"].trim() : undefined;

tableColumn.isNullable = dbColumn["NULLABLE"] === "Y";
tableColumn.isUnique = isUnique;
tableColumn.isUnique = uniqueConstraints.length > 0 && !isConstraintComposite;
tableColumn.isPrimary = isPrimary;
tableColumn.isGenerated = dbColumn["IDENTITY_COLUMN"] === "YES";
if (tableColumn.isGenerated) {
Expand Down
14 changes: 7 additions & 7 deletions src/driver/postgres/PostgresQueryRunner.ts
Expand Up @@ -1901,13 +1901,13 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
tableColumn.isNullable = dbColumn["is_nullable"] === "YES";
tableColumn.isPrimary = !!columnConstraints.find(constraint => constraint["constraint_type"] === "PRIMARY");

const uniqueConstraint = columnConstraints.find(constraint => constraint["constraint_type"] === "UNIQUE");
const isConstraintComposite = uniqueConstraint
? !!dbConstraints.find(dbConstraint => dbConstraint["constraint_type"] === "UNIQUE"
&& dbConstraint["constraint_name"] === uniqueConstraint["constraint_name"]
&& dbConstraint["column_name"] !== dbColumn["column_name"])
: false;
tableColumn.isUnique = !!uniqueConstraint && !isConstraintComposite;
const uniqueConstraints = columnConstraints.filter(constraint => constraint["constraint_type"] === "UNIQUE");
const isConstraintComposite = uniqueConstraints.every((uniqueConstraint) => {
return dbConstraints.some(dbConstraint => dbConstraint["constraint_type"] === "UNIQUE"
&& dbConstraint["constraint_name"] === uniqueConstraint["constraint_name"]
&& dbConstraint["column_name"] !== dbColumn["column_name"])
})
tableColumn.isUnique = uniqueConstraints.length > 0 && !isConstraintComposite;

if (dbColumn.is_identity === "YES") { // Postgres 10+ Identity column
tableColumn.isGenerated = true;
Expand Down
35 changes: 17 additions & 18 deletions src/driver/sap/SapQueryRunner.ts
Expand Up @@ -1588,26 +1588,25 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner {
dbConstraint["COLUMN_NAME"] === dbColumn["COLUMN_NAME"]
));

const columnUniqueIndex = dbIndices.find(dbIndex => {
if (dbIndex["TABLE_NAME"] !== dbTable["TABLE_NAME"] || dbIndex["SCHEMA_NAME"] !== dbTable["SCHEMA_NAME"]) {
return false;
}

// Index is not for this column
if (dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"]) {
return false;
}

return dbIndex["CONSTRAINT"] && dbIndex["CONSTRAINT"].indexOf("UNIQUE") !== -1;
});
const columnUniqueIndices = dbIndices.filter(dbIndex => {
return dbIndex["TABLE_NAME"] === dbTable["TABLE_NAME"]
&& dbIndex["SCHEMA_NAME"] === dbTable["SCHEMA_NAME"]
&& dbIndex["COLUMN_NAME"] === dbColumn["COLUMN_NAME"]
&& dbIndex["CONSTRAINT"] && dbIndex["CONSTRAINT"].indexOf("UNIQUE") !== -1
})

const tableMetadata = this.connection.entityMetadatas.find(metadata => this.getTablePath(table) === this.getTablePath(metadata));
const hasIgnoredIndex = columnUniqueIndex && tableMetadata && tableMetadata.indices
.some(index => index.name === columnUniqueIndex["INDEX_NAME"] && index.synchronize === false);
const hasIgnoredIndex = columnUniqueIndices.length > 0
&& tableMetadata
&& tableMetadata.indices.some(index => {
return columnUniqueIndices.some(uniqueIndex => {
return index.name === uniqueIndex["INDEX_NAME"] && index.synchronize === false;
})
});

const isConstraintComposite = columnUniqueIndex
? !!dbIndices.find(dbIndex => dbIndex["INDEX_NAME"] === columnUniqueIndex["INDEX_NAME"] && dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"])
: false;
const isConstraintComposite = columnUniqueIndices.every((uniqueIndex) => {
return dbIndices.some(dbIndex => dbIndex["INDEX_NAME"] === uniqueIndex["INDEX_NAME"] && dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"]);
})

const tableColumn = new TableColumn();
tableColumn.name = dbColumn["COLUMN_NAME"];
Expand Down Expand Up @@ -1638,7 +1637,7 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner {
const length = dbColumn["LENGTH"].toString();
tableColumn.length = !this.isDefaultColumnLength(table, tableColumn, length) ? length : "";
}
tableColumn.isUnique = !!columnUniqueIndex && !hasIgnoredIndex && !isConstraintComposite;
tableColumn.isUnique = columnUniqueIndices.length > 0 && !hasIgnoredIndex && !isConstraintComposite;
tableColumn.isNullable = dbColumn["IS_NULLABLE"] === "TRUE";
tableColumn.isPrimary = !!columnConstraints.find(constraint => constraint["IS_PRIMARY_KEY"] === "TRUE");
tableColumn.isGenerated = dbColumn["GENERATION_TYPE"] === "ALWAYS AS IDENTITY";
Expand Down
10 changes: 5 additions & 5 deletions src/driver/sqlserver/SqlServerQueryRunner.ts
Expand Up @@ -1745,14 +1745,14 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner
dbConstraint["COLUMN_NAME"] === dbColumn["COLUMN_NAME"]
));

const uniqueConstraint = columnConstraints.find(constraint => constraint["CONSTRAINT_TYPE"] === "UNIQUE");
const isConstraintComposite = uniqueConstraint
? !!dbConstraints.find(dbConstraint => dbConstraint["CONSTRAINT_TYPE"] === "UNIQUE"
const uniqueConstraints = columnConstraints.filter(constraint => constraint["CONSTRAINT_TYPE"] === "UNIQUE");
const isConstraintComposite = uniqueConstraints.every((uniqueConstraint) => {
return dbConstraints.some(dbConstraint => dbConstraint["CONSTRAINT_TYPE"] === "UNIQUE"
&& dbConstraint["CONSTRAINT_NAME"] === uniqueConstraint["CONSTRAINT_NAME"]
&& dbConstraint["TABLE_SCHEMA"] === dbColumn["TABLE_SCHEMA"]
&& dbConstraint["TABLE_CATALOG"] === dbColumn["TABLE_CATALOG"]
&& dbConstraint["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"])
: false;
})

const isPrimary = !!columnConstraints.find(constraint => constraint["CONSTRAINT_TYPE"] === "PRIMARY KEY");
const isGenerated = !!dbIdentityColumns.find(column => (
Expand Down Expand Up @@ -1809,7 +1809,7 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner
: undefined;
tableColumn.isNullable = dbColumn["IS_NULLABLE"] === "YES";
tableColumn.isPrimary = isPrimary;
tableColumn.isUnique = !!uniqueConstraint && !isConstraintComposite;
tableColumn.isUnique = uniqueConstraints.length > 0 && !isConstraintComposite;
tableColumn.isGenerated = isGenerated;
if (isGenerated)
tableColumn.generationStrategy = "increment";
Expand Down
27 changes: 27 additions & 0 deletions test/github-issues/8158/entity/User.ts
@@ -0,0 +1,27 @@
import {Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, Unique} from "../../../../src";
import {UserMeta} from "./UserMeta";

@Entity()
@Unique(["firstName"])
@Unique(["id", "firstName"])
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column()
firstName: string;

@Column()
lastName: string;

@Column()
age: number;

@Column({ nullable: false, })
userMetaId: number;

@OneToOne(() => UserMeta)
@JoinColumn({ name: "userMetaId", referencedColumnName: "id" })
userMeta: UserMeta;

}
21 changes: 21 additions & 0 deletions test/github-issues/8158/entity/UserMeta.ts
@@ -0,0 +1,21 @@
import {Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn} from "../../../../src";
import {User} from "./User";

@Entity()
export class UserMeta {
@PrimaryGeneratedColumn()
id: number;

@Column()
foo: number;

@Column()
bar: number;

@Column({ nullable: false, })
userId: number;

@OneToOne(() => User)
@JoinColumn({ name: "userId", referencedColumnName: "id" })
user: User;
}

0 comments on commit 29cb891

Please sign in to comment.