From 6d4a97d39a46b95c5d68c2a857c9bb191ea1a3b7 Mon Sep 17 00:00:00 2001 From: Kononnable Date: Fri, 11 Oct 2019 10:05:21 +0200 Subject: [PATCH] postgres implementation --- src/drivers/PostgresDriver.ts | 544 +++++++++--------- .../entityTypes/postgres/entity/Post.ts | 10 +- .../entityTypes/postgres/entity/PostArrays.ts | 10 +- 3 files changed, 290 insertions(+), 274 deletions(-) diff --git a/src/drivers/PostgresDriver.ts b/src/drivers/PostgresDriver.ts index 02c5615..7571591 100644 --- a/src/drivers/PostgresDriver.ts +++ b/src/drivers/PostgresDriver.ts @@ -11,6 +11,10 @@ import IndexColumnInfo from "../oldModels/IndexColumnInfo"; import RelationTempInfo from "../oldModels/RelationTempInfo"; import IConnectionOptions from "../IConnectionOptions"; import { Entity } from "../models/Entity"; +import { Column } from "../models/Column"; +import { Index } from "../models/Index"; +import IGenerationOptions from "../IGenerationOptions"; +import { RelationInternal } from "../models/RelationInternal"; export default class PostgresDriver extends AbstractDriver { public defaultValues: DataTypeDefaults = new TypeormDriver.PostgresDriver({ @@ -40,125 +44,131 @@ export default class PostgresDriver extends AbstractDriver { entities: Entity[], schema: string ): Promise { - throw new Error(); - // TODO: Remove - // const response: { - // table_name: string; - // column_name: string; - // udt_name: string; - // column_default: string; - // is_nullable: string; - // data_type: string; - // character_maximum_length: number; - // numeric_precision: number; - // numeric_scale: number; - // isidentity: string; - // isunique: string; - // enumvalues: string | null; - // }[] = (await this.Connection - // .query(`SELECT table_name,column_name,udt_name,column_default,is_nullable, - // data_type,character_maximum_length,numeric_precision,numeric_scale, - // case when column_default LIKE 'nextval%' then 'YES' else 'NO' end isidentity, - // (SELECT count(*) - // FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc - // inner join INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE cu - // on cu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME - // where - // tc.CONSTRAINT_TYPE = 'UNIQUE' - // and tc.TABLE_NAME = c.TABLE_NAME - // and cu.COLUMN_NAME = c.COLUMN_NAME - // and tc.TABLE_SCHEMA=c.TABLE_SCHEMA) IsUnique, - // (SELECT - // string_agg(enumlabel, ',') - // FROM "pg_enum" "e" - // INNER JOIN "pg_type" "t" ON "t"."oid" = "e"."enumtypid" - // INNER JOIN "pg_namespace" "n" ON "n"."oid" = "t"."typnamespace" - // WHERE "n"."nspname" = table_schema AND "t"."typname"=udt_name - // ) enumValues - // FROM INFORMATION_SCHEMA.COLUMNS c - // where table_schema in (${schema}) - // order by ordinal_position`)).rows; - // entities.forEach(ent => { - // response - // .filter(filterVal => filterVal.table_name === ent.tsEntityName) - // .forEach(resp => { - // const colInfo: ColumnInfo = new ColumnInfo(); - // colInfo.tsName = resp.column_name; - // colInfo.options.name = resp.column_name; - // colInfo.options.nullable = resp.is_nullable === "YES"; - // colInfo.options.generated = resp.isidentity === "YES"; - // colInfo.options.unique = resp.isunique === "1"; - // colInfo.options.default = colInfo.options.generated - // ? null - // : PostgresDriver.ReturnDefaultValueFunction( - // resp.column_default - // ); + const response: { + table_name: string; + column_name: string; + udt_name: string; + column_default: string; + is_nullable: string; + data_type: string; + character_maximum_length: number; + numeric_precision: number; + numeric_scale: number; + isidentity: string; + isunique: string; + enumvalues: string | null; + }[] = (await this.Connection + .query(`SELECT table_name,column_name,udt_name,column_default,is_nullable, + data_type,character_maximum_length,numeric_precision,numeric_scale, + case when column_default LIKE 'nextval%' then 'YES' else 'NO' end isidentity, + (SELECT count(*) + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + inner join INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE cu + on cu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + where + tc.CONSTRAINT_TYPE = 'UNIQUE' + and tc.TABLE_NAME = c.TABLE_NAME + and cu.COLUMN_NAME = c.COLUMN_NAME + and tc.TABLE_SCHEMA=c.TABLE_SCHEMA) IsUnique, + (SELECT + string_agg(enumlabel, ',') + FROM "pg_enum" "e" + INNER JOIN "pg_type" "t" ON "t"."oid" = "e"."enumtypid" + INNER JOIN "pg_namespace" "n" ON "n"."oid" = "t"."typnamespace" + WHERE "n"."nspname" = table_schema AND "t"."typname"=udt_name + ) enumValues + FROM INFORMATION_SCHEMA.COLUMNS c + where table_schema in (${schema}) + order by ordinal_position`)).rows; + entities.forEach(ent => { + response + .filter(filterVal => filterVal.table_name === ent.tscName) + .forEach(resp => { + const tscName = resp.column_name; + const options: Partial = {}; + options.name = resp.column_name; + if (resp.is_nullable === "YES") options.nullable = true; + if (resp.isunique === "1") options.unique = true; - // const columnTypes = this.MatchColumnTypes( - // resp.data_type, - // resp.udt_name, - // resp.enumvalues - // ); - // if (!columnTypes.sqlType || !columnTypes.tsType) { - // if ( - // resp.data_type === "USER-DEFINED" || - // resp.data_type === "ARRAY" - // ) { - // TomgUtils.LogError( - // `Unknown ${resp.data_type} column type: ${resp.udt_name} table name: ${resp.table_name} column name: ${resp.column_name}` - // ); - // } else { - // TomgUtils.LogError( - // `Unknown column type: ${resp.data_type} table name: ${resp.table_name} column name: ${resp.column_name}` - // ); - // } - // return; - // } - // colInfo.options.type = columnTypes.sqlType as any; - // colInfo.tsType = columnTypes.tsType; - // colInfo.options.array = columnTypes.isArray; - // colInfo.options.enum = columnTypes.enumValues; - // if (colInfo.options.array) { - // colInfo.tsType = colInfo.tsType - // .split("|") - // .map(x => `${x.replace("|", "").trim()}[]`) - // .join(" | ") as any; - // } + const generated = + resp.isidentity === "YES" ? true : undefined; + const defaultValue = generated + ? undefined + : PostgresDriver.ReturnDefaultValueFunction( + resp.column_default + ); - // if ( - // this.ColumnTypesWithPrecision.some( - // v => v === colInfo.options.type - // ) - // ) { - // colInfo.options.precision = resp.numeric_precision; - // colInfo.options.scale = resp.numeric_scale; - // } - // if ( - // this.ColumnTypesWithLength.some( - // v => v === colInfo.options.type - // ) - // ) { - // colInfo.options.length = - // resp.character_maximum_length > 0 - // ? resp.character_maximum_length - // : undefined; - // } - // if ( - // this.ColumnTypesWithWidth.some( - // v => v === colInfo.options.type - // ) - // ) { - // colInfo.options.width = - // resp.character_maximum_length > 0 - // ? resp.character_maximum_length - // : undefined; - // } - // if (colInfo.options.type && colInfo.tsType) { - // ent.Columns.push(colInfo); - // } - // }); - // }); - // return entities; + const columnTypes = this.MatchColumnTypes( + resp.data_type, + resp.udt_name, + resp.enumvalues + ); + if (!columnTypes.sqlType || !columnTypes.tsType) { + if ( + resp.data_type === "USER-DEFINED" || + resp.data_type === "ARRAY" + ) { + TomgUtils.LogError( + `Unknown ${resp.data_type} column type: ${resp.udt_name} table name: ${resp.table_name} column name: ${resp.column_name}` + ); + } else { + TomgUtils.LogError( + `Unknown column type: ${resp.data_type} table name: ${resp.table_name} column name: ${resp.column_name}` + ); + } + return; + } + const columnType = columnTypes.sqlType as any; + let tscType = columnTypes.tsType; + if (columnTypes.isArray) options.array = true; + if (columnTypes.enumValues.length > 0) + options.enum = columnTypes.enumValues; + if (options.array) { + tscType = tscType + .split("|") + .map(x => `${x.replace("|", "").trim()}[]`) + .join(" | ") as any; + } + + if ( + this.ColumnTypesWithPrecision.some( + v => v === columnType + ) + ) { + if (resp.numeric_precision !== null) { + options.precision = resp.numeric_precision; + } + if (resp.numeric_scale !== null) { + options.scale = resp.numeric_scale; + } + } + if ( + this.ColumnTypesWithLength.some(v => v === columnType) + ) { + options.length = + resp.character_maximum_length > 0 + ? resp.character_maximum_length + : undefined; + } + if (this.ColumnTypesWithWidth.some(v => v === columnType)) { + options.width = + resp.character_maximum_length > 0 + ? resp.character_maximum_length + : undefined; + } + if (columnType && tscType) { + ent.columns.push({ + generated, + type: columnType, + default: defaultValue, + options: { name: "", ...options }, // TODO: Change + tscName, + tscType + }); + } + }); + }); + return entities; } public MatchColumnTypes( @@ -167,7 +177,7 @@ export default class PostgresDriver extends AbstractDriver { enumValues: string | null ) { let ret: { - tsType?: ColumnInfo["tsType"]; + tsType?: Column["tscType"]; sqlType: string | null; isArray: boolean; enumValues: string[]; @@ -382,9 +392,7 @@ export default class PostgresDriver extends AbstractDriver { .split(",") .join('" | "')}"` as never) as string; ret.sqlType = "enum"; - ret.enumValues = (`"${enumValues - .split(",") - .join('","')}"` as never) as string[]; + ret.enumValues = enumValues.split(","); } else { ret.tsType = undefined; ret.sqlType = null; @@ -404,152 +412,162 @@ export default class PostgresDriver extends AbstractDriver { entities: Entity[], schema: string ): Promise { - throw new Error(); - // TODO: Remove - // const response: { - // tablename: string; - // indexname: string; - // columnname: string; - // is_unique: number; - // is_primary_key: number; - // }[] = (await this.Connection.query(`SELECT - // c.relname AS tablename, - // i.relname as indexname, - // f.attname AS columnname, - // CASE - // WHEN ix.indisunique = true THEN 1 - // ELSE 0 - // END AS is_unique, - // CASE - // WHEN ix.indisprimary='true' THEN 1 - // ELSE 0 - // END AS is_primary_key - // FROM pg_attribute f - // JOIN pg_class c ON c.oid = f.attrelid - // JOIN pg_type t ON t.oid = f.atttypid - // LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = f.attnum - // LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - // LEFT JOIN pg_index AS ix ON f.attnum = ANY(ix.indkey) and c.oid = f.attrelid and c.oid = ix.indrelid - // LEFT JOIN pg_class AS i ON ix.indexrelid = i.oid - // WHERE c.relkind = 'r'::char - // AND n.nspname in (${schema}) - // AND f.attnum > 0 - // AND i.oid<>0 - // ORDER BY c.relname,f.attname;`)).rows; - // entities.forEach(ent => { - // response - // .filter(filterVal => filterVal.tablename === ent.tsEntityName) - // .forEach(resp => { - // let indexInfo: IndexInfo = {} as IndexInfo; - // const indexColumnInfo: IndexColumnInfo = {} as IndexColumnInfo; - // if ( - // ent.Indexes.filter( - // filterVal => filterVal.name === resp.indexname - // ).length > 0 - // ) { - // indexInfo = ent.Indexes.find( - // filterVal => filterVal.name === resp.indexname - // )!; - // } else { - // indexInfo.columns = [] as IndexColumnInfo[]; - // indexInfo.name = resp.indexname; - // indexInfo.isUnique = resp.is_unique === 1; - // indexInfo.isPrimaryKey = resp.is_primary_key === 1; - // ent.Indexes.push(indexInfo); - // } - // indexColumnInfo.name = resp.columnname; - // if (resp.is_primary_key === 0) { - // indexInfo.isPrimaryKey = false; - // } - // indexInfo.columns.push(indexColumnInfo); - // }); - // }); + const response: { + tablename: string; + indexname: string; + columnname: string; + is_unique: number; + is_primary_key: number; + }[] = (await this.Connection.query(`SELECT + c.relname AS tablename, + i.relname as indexname, + f.attname AS columnname, + CASE + WHEN ix.indisunique = true THEN 1 + ELSE 0 + END AS is_unique, + CASE + WHEN ix.indisprimary='true' THEN 1 + ELSE 0 + END AS is_primary_key + FROM pg_attribute f + JOIN pg_class c ON c.oid = f.attrelid + JOIN pg_type t ON t.oid = f.atttypid + LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = f.attnum + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + LEFT JOIN pg_index AS ix ON f.attnum = ANY(ix.indkey) and c.oid = f.attrelid and c.oid = ix.indrelid + LEFT JOIN pg_class AS i ON ix.indexrelid = i.oid + WHERE c.relkind = 'r'::char + AND n.nspname in (${schema}) + AND f.attnum > 0 + AND i.oid<>0 + ORDER BY c.relname,f.attname;`)).rows; + entities.forEach(ent => { + const entityIndices = response.filter( + filterVal => filterVal.tablename === ent.tscName + ); + const indexNames = new Set(entityIndices.map(v => v.indexname)); + indexNames.forEach(indexName => { + const records = entityIndices.filter( + v => v.indexname === indexName + ); + const indexInfo: Index = { + columns: [], + options: {}, + name: records[0].indexname + }; + if (records[0].is_primary_key === 1) indexInfo.primary = true; + if (records[0].is_unique === 1) indexInfo.options.unique = true; + records.forEach(record => { + indexInfo.columns.push(record.columnname); + }); + ent.indices.push(indexInfo); + }); + }); - // return entities; + return entities; } public async GetRelations( entities: Entity[], - schema: string + schema: string, + dbNames: string, + generationOptions: IGenerationOptions ): Promise { - throw new Error(); - // TODO: Remove - // const response: { - // tablewithforeignkey: string; - // fk_partno: number; - // foreignkeycolumn: string; - // tablereferenced: string; - // foreignkeycolumnreferenced: string; - // ondelete: "RESTRICT" | "CASCADE" | "SET NULL" | "NO ACTION"; - // onupdate: "RESTRICT" | "CASCADE" | "SET NULL" | "NO ACTION"; - // object_id: string; - // // Distinct because of note in https://www.postgresql.org/docs/9.1/information-schema.html - // }[] = (await this.Connection.query(`SELECT DISTINCT - // con.relname AS tablewithforeignkey, - // att.attnum as fk_partno, - // att2.attname AS foreignkeycolumn, - // cl.relname AS tablereferenced, - // att.attname AS foreignkeycolumnreferenced, - // delete_rule as ondelete, - // update_rule as onupdate, - // concat(con.conname,con.conrelid,con.confrelid) as object_id - // FROM ( - // SELECT - // unnest(con1.conkey) AS parent, - // unnest(con1.confkey) AS child, - // con1.confrelid, - // con1.conrelid, - // cl_1.relname, - // con1.conname, - // nspname - // FROM - // pg_class cl_1, - // pg_namespace ns, - // pg_constraint con1 - // WHERE - // con1.contype = 'f'::"char" - // AND cl_1.relnamespace = ns.oid - // AND con1.conrelid = cl_1.oid - // and nspname in (${schema}) - // ) con, - // pg_attribute att, - // pg_class cl, - // pg_attribute att2, - // information_schema.referential_constraints rc - // WHERE - // att.attrelid = con.confrelid - // AND att.attnum = con.child - // AND cl.oid = con.confrelid - // AND att2.attrelid = con.conrelid - // AND att2.attnum = con.parent - // AND rc.constraint_name= con.conname AND constraint_catalog=current_database() AND rc.constraint_schema=nspname - // `)).rows; - // const relationsTemp: RelationTempInfo[] = [] as RelationTempInfo[]; - // response.forEach(resp => { - // let rels = relationsTemp.find( - // val => val.objectId === resp.object_id - // ); - // if (rels === undefined) { - // rels = {} as RelationTempInfo; - // rels.ownerColumnsNames = []; - // rels.referencedColumnsNames = []; - // rels.actionOnDelete = - // resp.ondelete === "NO ACTION" ? null : resp.ondelete; - // rels.actionOnUpdate = - // resp.onupdate === "NO ACTION" ? null : resp.onupdate; - // rels.objectId = resp.object_id; - // rels.ownerTable = resp.tablewithforeignkey; - // rels.referencedTable = resp.tablereferenced; - // relationsTemp.push(rels); - // } - // rels.ownerColumnsNames.push(resp.foreignkeycolumn); - // rels.referencedColumnsNames.push(resp.foreignkeycolumnreferenced); - // }); - // const retVal = PostgresDriver.GetRelationsFromRelationTempInfo( - // relationsTemp, - // entities - // ); - // return retVal; + const response: { + tablewithforeignkey: string; + fk_partno: number; + foreignkeycolumn: string; + tablereferenced: string; + foreignkeycolumnreferenced: string; + ondelete: "RESTRICT" | "CASCADE" | "SET NULL" | "NO ACTION"; + onupdate: "RESTRICT" | "CASCADE" | "SET NULL" | "NO ACTION"; + object_id: string; + // Distinct because of note in https://www.postgresql.org/docs/9.1/information-schema.html + }[] = (await this.Connection.query(`SELECT DISTINCT + con.relname AS tablewithforeignkey, + att.attnum as fk_partno, + att2.attname AS foreignkeycolumn, + cl.relname AS tablereferenced, + att.attname AS foreignkeycolumnreferenced, + delete_rule as ondelete, + update_rule as onupdate, + concat(con.conname,con.conrelid,con.confrelid) as object_id + FROM ( + SELECT + unnest(con1.conkey) AS parent, + unnest(con1.confkey) AS child, + con1.confrelid, + con1.conrelid, + cl_1.relname, + con1.conname, + nspname + FROM + pg_class cl_1, + pg_namespace ns, + pg_constraint con1 + WHERE + con1.contype = 'f'::"char" + AND cl_1.relnamespace = ns.oid + AND con1.conrelid = cl_1.oid + and nspname in (${schema}) + ) con, + pg_attribute att, + pg_class cl, + pg_attribute att2, + information_schema.referential_constraints rc + WHERE + att.attrelid = con.confrelid + AND att.attnum = con.child + AND cl.oid = con.confrelid + AND att2.attrelid = con.conrelid + AND att2.attnum = con.parent + AND rc.constraint_name= con.conname AND constraint_catalog=current_database() AND rc.constraint_schema=nspname + `)).rows; + + const relationsTemp: RelationInternal[] = [] as RelationInternal[]; + const relationKeys = new Set(response.map(v => v.object_id)); + + relationKeys.forEach(relationId => { + const rows = response.filter(v => v.object_id === relationId); + const ownerTable = entities.find( + v => v.sqlName === rows[0].tablewithforeignkey + ); + const relatedTable = entities.find( + v => v.sqlName === rows[0].tablereferenced + ); + if (!ownerTable || !relatedTable) { + TomgUtils.LogError( + `Relation between tables ${rows[0].tablewithforeignkey} and ${rows[0].tablereferenced} wasn't found in entity model.`, + true + ); + return; + } + const internal: RelationInternal = { + ownerColumns: [], + relatedColumns: [], + ownerTable, + relatedTable + }; + if (rows[0].ondelete !== "NO ACTION") { + internal.onDelete = rows[0].ondelete; + } + if (rows[0].onupdate !== "NO ACTION") { + internal.onUpdate = rows[0].onupdate; + } + rows.forEach(row => { + internal.ownerColumns.push(row.foreignkeycolumn); + internal.relatedColumns.push(row.foreignkeycolumnreferenced); + }); + relationsTemp.push(internal); + }); + + const retVal = PostgresDriver.GetRelationsFromRelationTempInfo( + relationsTemp, + entities, + generationOptions + ); + return retVal; } public async DisconnectFromServer() { @@ -623,10 +641,10 @@ export default class PostgresDriver extends AbstractDriver { private static ReturnDefaultValueFunction( defVal: string | null - ): string | null { + ): string | undefined { let defaultValue = defVal; if (!defaultValue) { - return null; + return undefined; } defaultValue = defaultValue.replace(/'::[\w ]*/, "'"); if (defaultValue.startsWith(`'`)) { diff --git a/test/integration/entityTypes/postgres/entity/Post.ts b/test/integration/entityTypes/postgres/entity/Post.ts index 1f1cc56..77012f0 100644 --- a/test/integration/entityTypes/postgres/entity/Post.ts +++ b/test/integration/entityTypes/postgres/entity/Post.ts @@ -81,7 +81,7 @@ export class Post { varbit: string; @Column("bit varying") - bit_varying: string; + bitVarying: string; @Column("timetz") timetz: string; @@ -93,10 +93,10 @@ export class Post { timestamp: Date; @Column("timestamp without time zone") - timestamp_without_time_zone: Date; + timestampWithoutTimeZone: Date; @Column("timestamp with time zone") - timestamp_with_time_zone: Date; + timestampWithTimeZone: Date; @Column("date") date: string; @@ -104,10 +104,10 @@ export class Post { @Column("time") time: string; @Column("time without time zone") - time_without_time_zone: string; + timeWithoutTimeZone: string; @Column("time with time zone") - time_with_time_zone: string; + timeWithTimeZone: string; @Column("interval") interval: any; diff --git a/test/integration/entityTypes/postgres/entity/PostArrays.ts b/test/integration/entityTypes/postgres/entity/PostArrays.ts index fde1012..5b7a6ad 100644 --- a/test/integration/entityTypes/postgres/entity/PostArrays.ts +++ b/test/integration/entityTypes/postgres/entity/PostArrays.ts @@ -2,7 +2,6 @@ import { Entity, PrimaryColumn, Column } from "typeorm"; @Entity("PostArrays") export class PostArrays { - @PrimaryColumn() id: number; @@ -82,7 +81,7 @@ export class PostArrays { varbit: string[]; @Column("bit varying", { array: true }) - bit_varying: string[]; + bitVarying: string[]; @Column("timetz", { array: true }) timetz: string[]; @@ -97,7 +96,7 @@ export class PostArrays { // timestamp_without_time_zone: Date[]; @Column("timestamp with time zone", { array: true }) - timestamp_with_time_zone: Date[]; + timestampWithTimeZone: Date[]; @Column("date", { array: true }) date: string[]; @@ -105,10 +104,10 @@ export class PostArrays { @Column("time", { array: true }) time: string[]; @Column("time without time zone", { array: true }) - time_without_time_zone: string[]; + timeWithoutTimeZone: string[]; @Column("time with time zone", { array: true }) - time_with_time_zone: string[]; + timeWithTimeZone: string[]; @Column("interval", { array: true }) interval: any[]; @@ -187,5 +186,4 @@ export class PostArrays { @Column("daterange", { array: true }) daterange: string[]; - }