diff --git a/docker-compose.yml b/docker-compose.yml index a82254ec09..f2a93e4e23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: # mariadb mariadb: - image: "mariadb:10.8.4" + image: "mariadb:10.10.3" container_name: "typeorm-mariadb" ports: - "3307:3306" diff --git a/docs/entities.md b/docs/entities.md index 43f4c94298..c061f74ef2 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -369,7 +369,10 @@ or `timestamp`, `time`, `year`, `char`, `nchar`, `national char`, `varchar`, `nvarchar`, `national varchar`, `text`, `tinytext`, `mediumtext`, `blob`, `longtext`, `tinyblob`, `mediumblob`, `longblob`, `enum`, `set`, `json`, `binary`, `varbinary`, `geometry`, `point`, `linestring`, `polygon`, `multipoint`, `multilinestring`, -`multipolygon`, `geometrycollection` +`multipolygon`, `geometrycollection`, `uuid`, `inet4`, `inet6` + +> Note: UUID, INET4, and INET6 are only available for mariadb and for respective versions that made them available. + ### Column types for `postgres` diff --git a/src/driver/mysql/MysqlDriver.ts b/src/driver/mysql/MysqlDriver.ts index 29766325c1..144398bab6 100644 --- a/src/driver/mysql/MysqlDriver.ts +++ b/src/driver/mysql/MysqlDriver.ts @@ -152,6 +152,10 @@ export class MysqlDriver implements Driver { "multilinestring", "multipolygon", "geometrycollection", + // additional data types for mariadb + "uuid", + "inet4", + "inet6", ] /** @@ -331,6 +335,9 @@ export class MysqlDriver implements Driver { update: false, } + /** MariaDB supports uuid type for version 10.7.0 and up */ + private uuidColumnTypeSuported = false + // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- @@ -421,6 +428,9 @@ export class MysqlDriver implements Driver { if (VersionUtils.isGreaterOrEqual(dbVersion, "10.2.0")) { this.cteCapabilities.enabled = true } + if (VersionUtils.isGreaterOrEqual(dbVersion, "10.7.0")) { + this.uuidColumnTypeSuported = true + } } else if (this.options.type === "mysql") { if (VersionUtils.isGreaterOrEqual(dbVersion, "8.0.0")) { this.cteCapabilities.enabled = true @@ -720,7 +730,7 @@ export class MysqlDriver implements Driver { return "blob" } else if (column.type === Boolean) { return "tinyint" - } else if (column.type === "uuid") { + } else if (column.type === "uuid" && !this.uuidColumnTypeSuported) { return "varchar" } else if (column.type === "json" && this.options.type === "mariadb") { /* @@ -825,8 +835,10 @@ export class MysqlDriver implements Driver { /** * fix https://github.com/typeorm/typeorm/issues/1139 + * note that if the db did support uuid column type it wouldn't have been defaulted to varchar */ - if (column.generationStrategy === "uuid") return "36" + if (column.generationStrategy === "uuid" && column.type === "varchar") + return "36" switch (column.type) { case String: diff --git a/src/driver/types/ColumnTypes.ts b/src/driver/types/ColumnTypes.ts index 105d4571bd..f40ec1a4f1 100644 --- a/src/driver/types/ColumnTypes.ts +++ b/src/driver/types/ColumnTypes.ts @@ -177,13 +177,15 @@ export type SimpleColumnType = | "set" // mysql | "cidr" // postgres | "inet" // postgres, cockroachdb + | "inet4" // mariadb + | "inet6" // mariadb | "macaddr" // postgres | "bit" // postgres, mssql | "bit varying" // postgres | "varbit" // postgres | "tsvector" // postgres | "tsquery" // postgres - | "uuid" // postgres, cockroachdb + | "uuid" // postgres, cockroachdb, mariadb | "xml" // mssql, postgres | "json" // mysql, postgres, cockroachdb, spanner | "jsonb" // postgres, cockroachdb diff --git a/src/metadata-builder/JunctionEntityMetadataBuilder.ts b/src/metadata-builder/JunctionEntityMetadataBuilder.ts index d80632198a..6e1424ae6f 100644 --- a/src/metadata-builder/JunctionEntityMetadataBuilder.ts +++ b/src/metadata-builder/JunctionEntityMetadataBuilder.ts @@ -102,6 +102,10 @@ export class JunctionEntityMetadataBuilder { ) || this.connection.driver.options.type === "aurora-mysql") && + // some versions of mariadb support the column type and should not try to provide the length property + this.connection.driver.normalizeType( + referencedColumn, + ) !== "uuid" && (referencedColumn.generationStrategy === "uuid" || referencedColumn.type === "uuid") ? "36" @@ -166,6 +170,10 @@ export class JunctionEntityMetadataBuilder { ) || this.connection.driver.options.type === "aurora-mysql") && + // some versions of mariadb support the column type and should not try to provide the length property + this.connection.driver.normalizeType( + inverseReferencedColumn, + ) !== "uuid" && (inverseReferencedColumn.generationStrategy === "uuid" || inverseReferencedColumn.type === "uuid") diff --git a/src/metadata-builder/RelationJoinColumnBuilder.ts b/src/metadata-builder/RelationJoinColumnBuilder.ts index 5d32530ff3..143d405523 100644 --- a/src/metadata-builder/RelationJoinColumnBuilder.ts +++ b/src/metadata-builder/RelationJoinColumnBuilder.ts @@ -208,6 +208,10 @@ export class RelationJoinColumnBuilder { ) || this.connection.driver.options.type === "aurora-mysql") && + // some versions of mariadb support the column type and should not try to provide the length property + this.connection.driver.normalizeType( + referencedColumn, + ) !== "uuid" && (referencedColumn.generationStrategy === "uuid" || referencedColumn.type === "uuid") diff --git a/test/github-issues/6540/entity/order.entity.ts.ts b/test/github-issues/6540/entity/order.entity.ts.ts index a10ba9b587..1592ef9a9b 100644 --- a/test/github-issues/6540/entity/order.entity.ts.ts +++ b/test/github-issues/6540/entity/order.entity.ts.ts @@ -16,7 +16,11 @@ export enum OrderStatus { @Entity() export class Order extends BaseEntity { - @PrimaryGeneratedColumn("uuid") + /** + * modified to remove the uuid since some versions of mariadb have uuid as a type + * which would create an additional upsert between the tests -> https://github.com/typeorm/typeorm/issues/8832 + */ + @PrimaryGeneratedColumn() id: string @Column({ type: "enum", enum: OrderStatus }) diff --git a/test/github-issues/8832/badEntity/BadInet4.ts b/test/github-issues/8832/badEntity/BadInet4.ts new file mode 100644 index 0000000000..35bace3c73 --- /dev/null +++ b/test/github-issues/8832/badEntity/BadInet4.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "../../../../src" + +@Entity() +export class BadInet4 { + @PrimaryGeneratedColumn("uuid") + id?: string + + @Column({ type: "inet4", length: "36" }) + inet4: string +} diff --git a/test/github-issues/8832/badEntity/BadInet6.ts b/test/github-issues/8832/badEntity/BadInet6.ts new file mode 100644 index 0000000000..bf5ecc7a8e --- /dev/null +++ b/test/github-issues/8832/badEntity/BadInet6.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "../../../../src" + +@Entity() +export class BadInet6 { + @PrimaryGeneratedColumn("uuid") + id?: string + + @Column({ type: "inet6", length: "36" }) + inet6: string +} diff --git a/test/github-issues/8832/badEntity/BadUuid.ts b/test/github-issues/8832/badEntity/BadUuid.ts new file mode 100644 index 0000000000..961a50baa8 --- /dev/null +++ b/test/github-issues/8832/badEntity/BadUuid.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "../../../../src" + +@Entity() +export class BadUuid { + @PrimaryGeneratedColumn("uuid") + id?: string + + @Column({ type: "uuid", length: "36" }) + uuid: string +} diff --git a/test/github-issues/8832/entity/Address.ts b/test/github-issues/8832/entity/Address.ts new file mode 100644 index 0000000000..b8256ebd69 --- /dev/null +++ b/test/github-issues/8832/entity/Address.ts @@ -0,0 +1,22 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, +} from "../../../../src" +import { User } from "./User" + +@Entity() +export class Address { + @PrimaryGeneratedColumn("increment") + id?: number + + @Column() + city: string + + @Column() + state: string + + @ManyToOne(() => User, (user) => user.addresses) + user: User +} diff --git a/test/github-issues/8832/entity/User.ts b/test/github-issues/8832/entity/User.ts new file mode 100644 index 0000000000..06d3fb9a2c --- /dev/null +++ b/test/github-issues/8832/entity/User.ts @@ -0,0 +1,30 @@ +import { + Column, + Entity, + OneToMany, + PrimaryGeneratedColumn, +} from "../../../../src" +import { Address } from "./Address" + +@Entity() +export class User { + @PrimaryGeneratedColumn("uuid") + id?: string + + /** can use a default but testing against mysql since they're shared drivers */ + @Column({ type: "uuid" }) + uuid: string + + @Column({ type: "inet4" }) + inet4: string + + @Column({ type: "inet6" }) + inet6: string + + /** testing generation */ + @Column({ type: "uuid", generated: "uuid" }) + another_uuid_field?: string + + @OneToMany(() => Address, (address) => address.user) + addresses?: Address[] +} diff --git a/test/github-issues/8832/entity/UuidEntity.ts b/test/github-issues/8832/entity/UuidEntity.ts new file mode 100644 index 0000000000..d887673025 --- /dev/null +++ b/test/github-issues/8832/entity/UuidEntity.ts @@ -0,0 +1,7 @@ +import { Entity, PrimaryGeneratedColumn } from "../../../../src" + +@Entity() +export class UuidEntity { + @PrimaryGeneratedColumn("uuid") + id?: string +} diff --git a/test/github-issues/8832/issue-8832.ts b/test/github-issues/8832/issue-8832.ts new file mode 100644 index 0000000000..214e4f35c1 --- /dev/null +++ b/test/github-issues/8832/issue-8832.ts @@ -0,0 +1,248 @@ +import "../../utils/test-setup" +import { + createTestingConnections, + closeTestingConnections, + reloadTestingDatabases, +} from "../../utils/test-utils" +import { DataSource } from "../../../src/index" +import { expect } from "chai" +import { User } from "../8832/entity/User" +import { Address } from "./entity/Address" +import { ConnectionMetadataBuilder } from "../../../src/connection/ConnectionMetadataBuilder" +import { EntityMetadataValidator } from "../../../src/metadata-builder/EntityMetadataValidator" +import { UuidEntity } from "./entity/UuidEntity" + +describe("github issues > #8832 Add uuid, inet4, and inet6 types for mariadb", () => { + let connections: DataSource[] + + afterEach(() => closeTestingConnections(connections)) + + describe("basic use of new maria db types", () => { + const newUser: User = { + uuid: "ceb2897c-a1cf-11ed-8dbd-040300000000", + inet4: "192.0.2.146", + inet6: "2001:0db8:0000:0000:0000:ff00:0042:8329", + } + + const expectedInet6 = "2001:db8::ff00:42:8329" + + before( + async () => + (connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchema: true, + enabledDrivers: ["mariadb"], + })), + ) + beforeEach(() => reloadTestingDatabases(connections)) + + it("should create table with uuid, inet4, and inet6 type set to column for relevant mariadb versions", () => + Promise.all( + connections.map(async (connection) => { + const userRepository = connection.getRepository(User) + + // seems there is an issue with the persisting id that crosses over from mysql to mariadb + await userRepository.save(newUser) + + const savedUser = await userRepository.findOneOrFail({ + where: { uuid: newUser.uuid }, + }) + + const foundUser = await userRepository.findOne({ + where: { id: savedUser.id }, + }) + + expect(foundUser).to.not.be.null + expect(foundUser!.uuid).to.deep.equal(newUser.uuid) + expect(foundUser!.inet4).to.deep.equal(newUser.inet4) + expect(foundUser!.inet6).to.deep.equal(expectedInet6) + expect(foundUser!.another_uuid_field).to.not.be.undefined + + const columnTypes: { + COLUMN_NAME: string + DATA_TYPE: string + }[] = await connection.query( + ` + SELECT + COLUMN_NAME, + DATA_TYPE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE + TABLE_SCHEMA = ? + AND TABLE_NAME = ? + AND COLUMN_NAME IN (?, ?, ?, ?) + `, + [ + connection.driver.database, + "user", + "id", + "uuid", + "inet4", + "inet6", + "anotherUuid", + ], + ) + const expectedColumnTypes: Record = { + id: "uuid", + uuid: "uuid", + inet4: "inet4", + inet6: "inet6", + another_uuid_field: "uuid", + } + + columnTypes.forEach(({ COLUMN_NAME, DATA_TYPE }) => { + expect(DATA_TYPE).to.equal( + expectedColumnTypes[COLUMN_NAME], + ) + }) + + // save a relation + const addressRepository = connection.getRepository(Address) + + const newAddress: Address = { + city: "Codersville", + state: "Coderado", + user: foundUser!, + } + + await addressRepository.save(newAddress) + + const foundAddress = await addressRepository.findOne({ + where: { user: { id: foundUser!.id } }, + }) + + expect(foundAddress).to.not.be.null + }), + )) + }) + + describe("regression test mysql uuid generation", () => { + const uuidEntity: UuidEntity = { + id: "ceb2897c-a1cf-11ed-8dbd-040300000000", + } + + before( + async () => + (connections = await createTestingConnections({ + entities: [UuidEntity], + schemaCreate: true, + dropSchema: true, + enabledDrivers: ["mysql", "mariadb"], + })), + ) + beforeEach(() => reloadTestingDatabases(connections)) + + it("should create table with with varchar with length 36 when version is mysql", () => + Promise.all( + connections.map(async (connection) => { + const uuidRepository = connection.getRepository(UuidEntity) + + // seems there is an issue with the persisting id that crosses over from mysql to mariadb + await uuidRepository.save(uuidEntity) + + const columnTypes: { + DATA_TYPE: string + CHARACTER_MAXIMUM_LENGTH: string + }[] = await connection.query( + ` + SELECT + DATA_TYPE, + CHARACTER_MAXIMUM_LENGTH + FROM INFORMATION_SCHEMA.COLUMNS + WHERE + TABLE_SCHEMA = ? + AND TABLE_NAME = ? + AND COLUMN_NAME = ? + `, + [connection.driver.database, "UuidEntity", "id"], + ) + + const isMysql = connection.driver.options.type === "mysql" + const expectedType = isMysql ? "varchar" : "uuid" + const expectedLength = isMysql ? "36" : null + + columnTypes.forEach( + ({ DATA_TYPE, CHARACTER_MAXIMUM_LENGTH }) => { + expect(DATA_TYPE).to.equal(expectedType) + expect(CHARACTER_MAXIMUM_LENGTH).to.equal( + expectedLength, + ) + }, + ) + }), + )) + }) + + describe("entity-metadata-validator", () => { + it("should throw error if mariadb uuid is supported and length is provided to property", async () => { + Promise.all( + ["BadInet4", "BadInet6", "BadUuid"].map(async (entity) => { + const entityLocation = `${__dirname}/badEntity/${entity}{.js,.ts}"` + const connection = new DataSource({ + // dummy connection options, connection won't be established anyway + type: "mariadb", + host: "localhost", + username: "test", + password: "test", + database: "test", + entities: [entityLocation], + }) + + // version supports all the new types + connection.driver.version = "10.10.0" + + const connectionMetadataBuilder = + new ConnectionMetadataBuilder(connection) + const entityMetadatas = + await connectionMetadataBuilder.buildEntityMetadatas([ + entityLocation, + ]) + const entityMetadataValidator = + new EntityMetadataValidator() + expect(() => + entityMetadataValidator.validateMany( + entityMetadatas, + connection.driver, + ), + ).to.throw(Error) + }), + ) + }) + + it("should not throw error for mysql when uuid is provided and a length property is provided", async () => { + Promise.all( + ["BadInet4", "BadInet6", "BadUuid"].map(async (entity) => { + const entityLocation = `${__dirname}/badEntity/${entity}{.js,.ts}"` + const connection = new DataSource({ + // dummy connection options, connection won't be established anyway + type: "mysql", + host: "localhost", + username: "test", + password: "test", + database: "test", + entities: [entityLocation], + }) + + // version supports all the new types + connection.driver.version = "10.10.0" + + const connectionMetadataBuilder = + new ConnectionMetadataBuilder(connection) + const entityMetadatas = + await connectionMetadataBuilder.buildEntityMetadatas([ + entityLocation, + ]) + const entityMetadataValidator = + new EntityMetadataValidator() + expect(() => + entityMetadataValidator.validateMany( + entityMetadatas, + connection.driver, + ), + ).not.to.throw() + }), + ) + }) + }) +})