diff --git a/src/metadata-args/MetadataArgsStorage.ts b/src/metadata-args/MetadataArgsStorage.ts index 71ef21f04..e7330dc5d 100644 --- a/src/metadata-args/MetadataArgsStorage.ts +++ b/src/metadata-args/MetadataArgsStorage.ts @@ -14,6 +14,7 @@ import {InheritanceMetadataArgs} from "./InheritanceMetadataArgs"; import {DiscriminatorValueMetadataArgs} from "./DiscriminatorValueMetadataArgs"; import {EntityRepositoryMetadataArgs} from "./EntityRepositoryMetadataArgs"; import {TransactionEntityMetadataArgs} from "./TransactionEntityMetadataArgs"; +import {MetadataUtils} from "../metadata-builder/MetadataUtils"; /** * Storage all metadatas args of all available types: tables, columns, subscribers, relations, etc. @@ -147,4 +148,20 @@ export class MetadataArgsStorage { }); } + filterSingleTableChildren(target: Function|string): TableMetadataArgs[] { + return this.tables.filter(table => { + return table.target instanceof Function + && target instanceof Function + && MetadataUtils.isInherited(table.target, target) + && table.type === "single-table-child"; + }); + } + + findInheritanceType(target: Function|string): InheritanceMetadataArgs|undefined { + return this.inheritances.find(inheritance => inheritance.target === target) + } + + findDiscriminatorValue(target: Function|string): DiscriminatorValueMetadataArgs|undefined { + return this.discriminatorValues.find(discriminatorValue => discriminatorValue.target === target) + } } \ No newline at end of file diff --git a/src/metadata-builder/EntityMetadataBuilder.ts b/src/metadata-builder/EntityMetadataBuilder.ts index 284007047..4b5847ede 100644 --- a/src/metadata-builder/EntityMetadataBuilder.ts +++ b/src/metadata-builder/EntityMetadataBuilder.ts @@ -62,7 +62,7 @@ export class EntityMetadataBuilder { const allTables = entityClasses ? this.metadataArgsStorage.filterTables(entityClasses) : this.metadataArgsStorage.tables; // filter out table metadata args for those we really create entity metadatas and tables in the db - const realTables = allTables.filter(table => table.type === "regular" || table.type === "closure" || table.type === "class-table-child"); + const realTables = allTables.filter(table => table.type === "regular" || table.type === "closure" || table.type === "class-table-child" || table.type === "single-table-child"); // create entity metadatas for a user defined entities (marked with @Entity decorator or loaded from entity schemas) const entityMetadatas = realTables.map(tableArgs => this.createEntityMetadata(tableArgs)); @@ -118,9 +118,27 @@ export class EntityMetadataBuilder { entityMetadatas.push(closureJunctionEntityMetadata); }); + // after all metadatas created we set parent entity metadata for class-table inheritance + entityMetadatas + .filter(metadata => metadata.tableType === "single-table-child") + .forEach(entityMetadata => { + const inheritanceTree: any[] = entityMetadata.target instanceof Function + ? MetadataUtils.getInheritanceTree(entityMetadata.target) + : [entityMetadata.target]; + + const parentMetadata = entityMetadatas.find(metadata => { + return inheritanceTree.find(inheritance => inheritance === metadata.target) && metadata.inheritanceType === "single-table"; + }); + + if (parentMetadata) { + entityMetadata.parentEntityMetadata = parentMetadata; + entityMetadata.tableName = parentMetadata.tableName; + } + }); + // generate keys for tables with single-table inheritance entityMetadatas - .filter(metadata => metadata.inheritanceType === "single-table") + .filter(metadata => metadata.inheritanceType === "single-table" && metadata.discriminatorColumn) .forEach(entityMetadata => this.createKeysForTableInheritance(entityMetadata)); // build all indices (need to do it after relations and their join columns are built) @@ -155,11 +173,26 @@ export class EntityMetadataBuilder { // we take all "inheritance tree" from a target entity to collect all stored metadata args // (by decorators or inside entity schemas). For example for target Post < ContentModel < Unit // it will be an array of [Post, ContentModel, Unit] and we can then get all metadata args of those classes - const inheritanceTree = tableArgs.target instanceof Function + const inheritanceTree: any[] = tableArgs.target instanceof Function ? MetadataUtils.getInheritanceTree(tableArgs.target) : [tableArgs.target]; // todo: implement later here inheritance for string-targets + // if single table inheritance used, we need to copy all children columns in to parent table + const singleTableChildrenTargets: any[] = this.metadataArgsStorage + .filterSingleTableChildren(tableArgs.target) + .map(args => args.target) + .filter(target => target instanceof Function); + + inheritanceTree.push(...singleTableChildrenTargets); + const entityMetadata = new EntityMetadata({ connection: this.connection, args: tableArgs }); + + const inheritanceType = this.metadataArgsStorage.findInheritanceType(tableArgs.target); + entityMetadata.inheritanceType = inheritanceType ? inheritanceType.type : undefined; + + const discriminatorValue = this.metadataArgsStorage.findDiscriminatorValue(tableArgs.target); + entityMetadata.discriminatorValue = discriminatorValue ? discriminatorValue.value : (tableArgs.target as any).name; // todo: pass this to naming strategy to generate a name + entityMetadata.embeddeds = this.createEmbeddedsRecursively(entityMetadata, this.metadataArgsStorage.filterEmbeddeds(inheritanceTree)); entityMetadata.ownColumns = this.metadataArgsStorage.filterColumns(inheritanceTree).map(args => { return new ColumnMetadata({ entityMetadata, args }); diff --git a/src/metadata-builder/MetadataUtils.ts b/src/metadata-builder/MetadataUtils.ts index 6106036e5..7a72d6801 100644 --- a/src/metadata-builder/MetadataUtils.ts +++ b/src/metadata-builder/MetadataUtils.ts @@ -22,6 +22,16 @@ export class MetadataUtils { return tree; } + /** + * Checks if this table is inherited from another table. + */ + static isInherited(target1: Function, target2: Function) { + // we cannot use instanceOf in this method, because we need order of inherited tables, to ensure that + // properties get inherited in a right order. To achieve it we can only check a first parent of the class + // return this.target.prototype instanceof anotherTable.target; + return Object.getPrototypeOf(target1.prototype).constructor === target2; + } + /** * Filters given array of targets by a given classes. * If classes are not given, then it returns array itself. diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index bcea6c3e6..797e4bc6c 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -36,14 +36,12 @@ import {RelationCountMetadataToAttributeTransformer} from "./relation-count/Rela // todo: add selectAndMap // todo: tests for: -// todo: entityOrProperty can be a table name. implement if its a table // todo: entityOrProperty can be target name. implement proper behaviour if it is. // todo: think about subselect in joins syntax // todo: create multiple representations of QueryBuilder: UpdateQueryBuilder, DeleteQueryBuilder // qb.update() returns UpdateQueryBuilder // qb.delete() returns DeleteQueryBuilder // qb.select() returns SelectQueryBuilder -// todo: tests for leftJoinAndMap... // todo: COMPLETELY COVER QUERY BUILDER WITH TESTS // todo: SUBSELECT IMPLEMENTATION @@ -51,8 +49,6 @@ import {RelationCountMetadataToAttributeTransformer} from "./relation-count/Rela // todo: also create qb.createSubQueryBuilder() // todo: check in persistment if id exist on object and throw exception (can be in partial selection?) // todo: STREAMING -// todo: switch to embedded task? -// todo: add test for @JoinColumn({ referencedColumnName }) /** * Allows to build complex sql queries in a fashion way and execute those queries. @@ -1312,10 +1308,11 @@ export class QueryBuilder { const aliasName = this.expressionMap.mainAlias.name; if (this.expressionMap.mainAlias.hasMetadata) { - tableName = this.expressionMap.mainAlias.metadata.tableName; + const metadata = this.expressionMap.mainAlias.metadata; + tableName = metadata.tableName; - allSelects.push(...this.buildEscapedEntityColumnSelects(aliasName, this.expressionMap.mainAlias.metadata)); - excludedSelects.push(...this.findEntityColumnSelects(aliasName, this.expressionMap.mainAlias.metadata)); + allSelects.push(...this.buildEscapedEntityColumnSelects(aliasName, metadata)); + excludedSelects.push(...this.findEntityColumnSelects(aliasName, metadata)); } else { // if alias does not have metadata - selections will be from custom table tableName = this.expressionMap.mainAlias.tableName!; @@ -1337,12 +1334,12 @@ export class QueryBuilder { }); if (!this.expressionMap.ignoreParentTablesJoins && this.expressionMap.mainAlias.hasMetadata) { - if (this.expressionMap.mainAlias!.metadata.parentEntityMetadata && this.expressionMap.mainAlias!.metadata.parentIdColumns) { - const alias = "parentIdColumn_" + ea(this.expressionMap.mainAlias!.metadata.parentEntityMetadata.tableName); - this.expressionMap.mainAlias!.metadata.parentEntityMetadata.columns.forEach(column => { + const metadata = this.expressionMap.mainAlias.metadata; + if (metadata.parentEntityMetadata && metadata.parentEntityMetadata.inheritanceType === "class-table" && metadata.parentIdColumns) { + const alias = "parentIdColumn_" + metadata.parentEntityMetadata.tableName; + metadata.parentEntityMetadata.columns.forEach(column => { // TODO implement partial select - allSelects.push({ selection: ea(alias + "." + column.databaseName), aliasName: alias + "_" + column.databaseName }); - // allSelects.push(alias + "." + ec(column.fullName) + " AS " + alias + "_" + ea(column.fullName)); + allSelects.push({ selection: ea(alias) + "." + ec(column.databaseName), aliasName: alias + "_" + column.databaseName }); }); } } @@ -1445,7 +1442,7 @@ export class QueryBuilder { if (this.expressionMap.mainAlias!.hasMetadata) { const mainMetadata = this.expressionMap.mainAlias!.metadata; if (mainMetadata.discriminatorColumn) - return ` WHERE ${ conditions.length ? "(" + conditions + ") AND" : "" } ${this.escapeColumn(mainMetadata.discriminatorColumn.databaseName)}=:discriminatorColumnValue`; + return ` WHERE ${ conditions.length ? "(" + conditions + ") AND" : "" } ${this.replacePropertyNames(this.expressionMap.mainAlias!.name + "." + mainMetadata.discriminatorColumn.databaseName)}=:discriminatorColumnValue`; } if (!conditions.length) @@ -1563,7 +1560,6 @@ export class QueryBuilder { }).join(" AND "); } - return " " + joinAttr.direction + " JOIN " + et(junctionTableName) + " " + ea(junctionAlias) + " ON " + this.replacePropertyNames(junctionCondition) + " " + joinAttr.direction + " JOIN " + et(destinationTableName) + " " + ea(destinationTableAlias) + " ON " + this.replacePropertyNames(destinationCondition + appendedCondition); @@ -1572,13 +1568,13 @@ export class QueryBuilder { if (!this.expressionMap.ignoreParentTablesJoins && this.expressionMap.mainAlias!.hasMetadata) { const metadata = this.expressionMap.mainAlias!.metadata; - if (metadata.parentEntityMetadata && metadata.parentIdColumns) { + if (metadata.parentEntityMetadata && metadata.parentEntityMetadata.inheritanceType === "class-table" && metadata.parentIdColumns) { const alias = "parentIdColumn_" + metadata.parentEntityMetadata.tableName; - const parentJoin = " JOIN " + et(metadata.parentEntityMetadata.tableName) + " " + ea(alias) + " ON " + - metadata.parentIdColumns.map(parentIdColumn => { - return this.expressionMap.mainAlias!.name + "." + parentIdColumn.databaseName + "=" + ea(alias) + "." + parentIdColumn.propertyName; - }); - joins.push(parentJoin); + const condition = metadata.parentIdColumns.map(parentIdColumn => { + return this.expressionMap.mainAlias!.name + "." + parentIdColumn.databaseName + "=" + ea(alias) + "." + parentIdColumn.propertyName; + }).join(" AND "); + const join = " JOIN " + et(metadata.parentEntityMetadata.tableName) + " " + ea(alias) + " ON " + condition; + joins.push(join); } } diff --git a/src/schema-builder/SchemaBuilder.ts b/src/schema-builder/SchemaBuilder.ts index cd0087b7c..b99af07ca 100644 --- a/src/schema-builder/SchemaBuilder.ts +++ b/src/schema-builder/SchemaBuilder.ts @@ -94,7 +94,7 @@ export class SchemaBuilder { // ------------------------------------------------------------------------- protected get entityToSyncMetadatas(): EntityMetadata[] { - return this.entityMetadatas.filter(metadata => !metadata.skipSchemaSync); + return this.entityMetadatas.filter(metadata => !metadata.skipSchemaSync && metadata.tableType !== "single-table-child"); } /** diff --git a/test/github-issues/131/entity/Student.ts b/test/github-issues/131/entity/Student.ts new file mode 100644 index 000000000..77c96147c --- /dev/null +++ b/test/github-issues/131/entity/Student.ts @@ -0,0 +1,11 @@ +import {Column} from "../../../../src/decorator/columns/Column"; +import {Person} from "./Person"; +import {SingleEntityChild} from "../../../../src/decorator/entity/SingleEntityChild"; + +@SingleEntityChild() +export class Student extends Person { + + @Column() + faculty: string; + +} diff --git a/test/github-issues/131/issue-131.ts b/test/github-issues/131/issue-131.ts index a8030ee89..f96e21a79 100644 --- a/test/github-issues/131/issue-131.ts +++ b/test/github-issues/131/issue-131.ts @@ -1,11 +1,11 @@ import "reflect-metadata"; -import { createTestingConnections, closeTestingConnections, reloadTestingDatabases } from "../../utils/test-utils"; -import { Connection } from "../../../src/connection/Connection"; -import { Employee } from "./entity/Employee"; -import { expect } from "chai"; +import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../utils/test-utils"; +import {Connection} from "../../../src/connection/Connection"; +import {expect} from "chai"; +import {Student} from "./entity/Student"; +import {Employee} from "./entity/Employee"; -// unskip once table inheritance is back -describe.skip("github issues > #131 Error with single table inheritance query without additional conditions", () => { +describe("github issues > #131 Error with single table inheritance query without additional conditions", () => { let connections: Connection[]; before(async () => connections = await createTestingConnections({ @@ -13,11 +13,15 @@ describe.skip("github issues > #131 Error with single table inheritance query wi schemaCreate: true, dropSchemaOnConnection: true, })); + beforeEach(() => reloadTestingDatabases(connections)); after(() => closeTestingConnections(connections)); it("should not fail when querying for single table inheritance model without additional conditions", () => Promise.all(connections.map(async connection => { const employees = await connection.getRepository(Employee).find(); expect(employees).not.to.be.undefined; + + const students = await connection.getRepository(Student).find(); + expect(students).not.to.be.undefined; }))); });