Merge pull request #330 from AlexMesser/master

implemented partial select, issues #329 #245
This commit is contained in:
Umed Khudoiberdiev 2017-03-05 08:42:02 +05:00 committed by GitHub
commit 13f8036b2b
7 changed files with 338 additions and 109 deletions

View File

@ -16,6 +16,7 @@ import {OptimisticLockCanNotBeUsedError} from "./error/OptimisticLockCanNotBeUse
import {PostgresDriver} from "../driver/postgres/PostgresDriver";
import {MysqlDriver} from "../driver/mysql/MysqlDriver";
import {LockNotSupportedOnGivenDriverError} from "./error/LockNotSupportedOnGivenDriverError";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
/**
*/
@ -57,6 +58,14 @@ export interface JoinMapping {
isMany: boolean;
}
// todo: fix problem with long aliases eg getMaxIdentifierLength
// todo: fix replacing in .select("COUNT(post.id) AS cnt") statement
// todo: implement joinAlways in relations and relationId
// todo: implement @Where decorator
// todo: add quoting functions
// todo: .addCount and .addCountSelect()
// todo: add selectAndMap
/**
* Allows to build complex sql queries in a fashion way and execute those queries.
*/
@ -87,6 +96,7 @@ export class QueryBuilder<Entity> {
protected lockVersion?: number|Date;
protected skipNumber: number;
protected takeNumber: number;
protected enableQuoting: boolean = true;
protected ignoreParentTablesJoins: boolean = false;
/**
@ -118,6 +128,14 @@ export class QueryBuilder<Entity> {
// Public Methods
// -------------------------------------------------------------------------
/**
* Disable escaping.
*/
disableQuoting(): this {
this.enableQuoting = false;
return this;
}
/**
* Creates DELETE query.
*/
@ -150,7 +168,7 @@ export class QueryBuilder<Entity> {
if (tableNameOrEntityOrUpdateSet instanceof Function) {
const aliasName = (<any> tableNameOrEntityOrUpdateSet).name;
const aliasObj = new Alias(aliasName);
aliasObj.target = <Function> tableNameOrEntityOrUpdateSet;
aliasObj.metadata = this.connection.getMetadata(tableNameOrEntityOrUpdateSet);
this.aliasMap.addMainAlias(aliasObj);
this.fromEntity = { alias: aliasObj };
@ -230,9 +248,24 @@ export class QueryBuilder<Entity> {
return this;
}
/**
* Sets locking mode.
*/
setLock(lockMode: "optimistic", lockVersion: number): this;
/**
* Sets locking mode.
*/
setLock(lockMode: "optimistic", lockVersion: Date): this;
/**
* Sets locking mode.
*/
setLock(lockMode: "pessimistic_read"|"pessimistic_write"): this;
/**
* Sets locking mode.
*/
setLock(lockMode: "optimistic"|"pessimistic_read"|"pessimistic_write", lockVersion?: number|Date): this {
this.lockMode = lockMode;
this.lockVersion = lockVersion;
@ -245,7 +278,7 @@ export class QueryBuilder<Entity> {
*/
from(entityTarget: Function|string, alias: string): this {
const aliasObj = new Alias(alias);
aliasObj.target = entityTarget;
aliasObj.metadata = this.connection.getMetadata(entityTarget);
this.aliasMap.addMainAlias(aliasObj);
this.fromEntity = {alias: aliasObj};
return this;
@ -861,7 +894,7 @@ export class QueryBuilder<Entity> {
sql += this.createOffsetExpression();
sql += this.createLockExpression();
[sql] = this.connection.driver.escapeQueryWithParameters(sql, this.parameters);
return sql;
return sql.trim();
}
/**
@ -880,7 +913,7 @@ export class QueryBuilder<Entity> {
sql += this.createLimitExpression();
sql += this.createOffsetExpression();
sql += this.createLockExpression();
return sql;
return sql.trim();
}
/**
@ -946,11 +979,11 @@ export class QueryBuilder<Entity> {
const [sql, parameters] = this.getSqlWithParameters({ skipOrderBy: true });
const [selects, orderBys] = this.createOrderByCombinedWithSelectExpression("distinctAlias");
const distinctAlias = this.connection.driver.escapeTableName("distinctAlias");
const distinctAlias = this.escapeTable("distinctAlias");
const metadata = this.connection.getMetadata(this.fromEntity.alias.target);
let idsQuery = `SELECT `;
idsQuery += metadata.primaryColumns.map((primaryColumn, index) => {
const propertyName = this.connection.driver.escapeAliasName(mainAliasName + "_" + primaryColumn.fullName);
const propertyName = this.escapeAlias(mainAliasName + "_" + primaryColumn.fullName);
if (index === 0) {
return `DISTINCT(${distinctAlias}.${propertyName}) as ids_${primaryColumn.fullName}`;
} else {
@ -1021,7 +1054,6 @@ export class QueryBuilder<Entity> {
return this.loadRelationCounts(queryRunner as QueryRunner, results)
.then(counts => {
// console.log("counts: ", counts);
return results;
});
})
@ -1051,7 +1083,6 @@ export class QueryBuilder<Entity> {
return this.loadRelationCounts(queryRunner as QueryRunner, results)
.then(counts => {
// console.log("counts: ", counts);
return results;
});
})
@ -1090,9 +1121,9 @@ export class QueryBuilder<Entity> {
const mainAlias = this.fromTableName ? this.fromTableName : this.aliasMap.mainAlias.name; // todo: will this work with "fromTableName"?
const metadata = this.connection.getMetadata(this.fromEntity.alias.target);
const distinctAlias = this.connection.driver.escapeAliasName(mainAlias);
const distinctAlias = this.escapeAlias(mainAlias);
let countSql = `COUNT(` + metadata.primaryColumnsWithParentIdColumns.map((primaryColumn, index) => {
const propertyName = this.connection.driver.escapeColumnName(primaryColumn.fullName);
const propertyName = this.escapeColumn(primaryColumn.fullName);
if (index === 0) {
return `DISTINCT(${distinctAlias}.${propertyName})`;
} else {
@ -1226,7 +1257,10 @@ export class QueryBuilder<Entity> {
}
this.joins.forEach(join => {
const property = join.tableName || join.alias.target || (join.alias.parentAliasName + "." + join.alias.parentPropertyName);
let property = join.tableName || join.alias.target;
if (join.alias.parentAliasName && join.alias.parentPropertyName) {
property = join.alias.parentAliasName + "." + join.alias.parentPropertyName;
}
qb.join(join.type, property, join.alias.name, join.condition || "", undefined, join.mapToProperty, join.isMappingMany);
});
@ -1277,6 +1311,24 @@ export class QueryBuilder<Entity> {
return qb;
}
escapeAlias(name: string) {
if (!this.enableQuoting)
return name;
return this.connection.driver.escapeAliasName(name);
}
escapeColumn(name: string) {
if (!this.enableQuoting)
return name;
return this.connection.driver.escapeColumnName(name);
}
escapeTable(name: string) {
if (!this.enableQuoting)
return name;
return this.connection.driver.escapeTableName(name);
}
/**
* Enables special query builder options.
*/
@ -1362,7 +1414,7 @@ export class QueryBuilder<Entity> {
return queryBuilder
.select(`${parentMetadata.name + "." + parentMetadata.primaryColumn.propertyName} AS id`)
.addSelect(`COUNT(${ this.connection.driver.escapeAliasName(relation.propertyName) + "." + this.connection.driver.escapeColumnName(relation.inverseEntityMetadata.primaryColumn.fullName) }) as cnt`)
.addSelect(`COUNT(${ this.escapeAlias(relation.propertyName) + "." + this.escapeColumn(relation.inverseEntityMetadata.primaryColumn.fullName) }) as cnt`)
.from(parentMetadata.target, parentMetadata.name)
.leftJoin(parentMetadata.name + "." + relation.propertyName, relation.propertyName, relationCountMeta.condition)
.setParameters(this.parameters)
@ -1370,7 +1422,6 @@ export class QueryBuilder<Entity> {
.groupBy(parentMetadata.name + "." + parentMetadata.primaryColumn.propertyName)
.getRawMany()
.then((results: { id: any, cnt: any }[]) => {
// console.log(relationCountMeta.entities);
relationCountMeta.entities.forEach(entityWithMetadata => {
const entityId = entityWithMetadata.entity[entityWithMetadata.metadata.primaryColumn.propertyName];
const entityResult = results.find(result => {
@ -1400,57 +1451,72 @@ export class QueryBuilder<Entity> {
return transformer.transform(results);
}
protected buildEscapedEntityColumnSelects(alias: Alias): string[] {
const hasMainAlias = this.selects.some(select => select === alias.name);
const columns: ColumnMetadata[] = hasMainAlias ? alias.metadata.columns : alias.metadata.columns.filter(column => {
return this.selects.some(select => select === alias.name + "." + column.propertyName);
});
return columns.map(column => {
return this.escapeAlias(alias.name) + "." + this.escapeColumn(column.fullName) +
" AS " + this.escapeAlias(alias.name + "_" + column.fullName);
});
};
protected findEntityColumnSelects(alias: Alias): string[] {
const mainAlias = this.selects.find(select => select === alias.name);
if (mainAlias)
return [mainAlias];
return this.selects.filter(select => {
return alias.metadata.columns.some(column => select === alias.name + "." + column.propertyName);
});
};
protected createSelectExpression() {
// todo throw exception if selects or from is missing
let alias: string = "", tableName: string;
const allSelects: string[] = [];
const excludedSelects: string[] = [];
if (this.fromTableName) {
tableName = this.fromTableName;
alias = this.fromTableAlias;
// console.log("ALIAS F:", alias);
} else if (this.fromEntity) {
const metadata = this.aliasMap.getEntityMetadataByAlias(this.fromEntity.alias);
if (!metadata)
if (!this.fromEntity.alias.metadata)
throw new Error("Cannot get entity metadata for the given alias " + this.fromEntity.alias.name);
tableName = metadata.table.name;
tableName = this.fromEntity.alias.metadata.table.name;
alias = this.fromEntity.alias.name;
// console.log("ALIAS N:", this.fromEntity.alias);
// console.log("ALIAS N:", alias);
// add select from the main table
if (this.selects.indexOf(alias) !== -1) {
metadata.columns.forEach(column => {
allSelects.push(this.connection.driver.escapeAliasName(alias) + "." + this.connection.driver.escapeColumnName(column.fullName) + " AS " + this.connection.driver.escapeAliasName(alias + "_" + column.fullName));
});
}
allSelects.push(...this.buildEscapedEntityColumnSelects(this.aliasMap.mainAlias));
excludedSelects.push(...this.findEntityColumnSelects(this.aliasMap.mainAlias));
} else {
throw new Error("No from given");
}
// add selects from joins
this.joins
.filter(join => this.selects.indexOf(join.alias.name) !== -1)
.forEach(join => {
const joinMetadata = this.aliasMap.getEntityMetadataByAlias(join.alias);
if (joinMetadata) {
joinMetadata.columns.forEach(column => {
allSelects.push(this.connection.driver.escapeAliasName(join.alias.name) + "." + this.connection.driver.escapeColumnName(column.fullName) + " AS " + this.connection.driver.escapeAliasName(join.alias.name + "_" + column.fullName));
});
} else {
allSelects.push(this.connection.driver.escapeAliasName(join.alias.name));
this.joins.forEach(join => {
if (join.alias.metadata) {
allSelects.push(...this.buildEscapedEntityColumnSelects(join.alias));
excludedSelects.push(...this.findEntityColumnSelects(join.alias));
} else {
const hasMainAlias = this.selects.some(select => select === join.alias.name);
if (hasMainAlias) {
allSelects.push(this.escapeAlias(join.alias.name) + ".*");
excludedSelects.push(join.alias.name);
}
});
}
});
if (!this.ignoreParentTablesJoins && !this.fromTableName) {
const metadata = this.connection.getMetadata(this.aliasMap.mainAlias.target);
if (metadata.parentEntityMetadata && metadata.parentIdColumns) {
const alias = "parentIdColumn_" + this.connection.driver.escapeAliasName(metadata.parentEntityMetadata.table.name);
metadata.parentEntityMetadata.columns.forEach(column => {
allSelects.push(alias + "." + this.connection.driver.escapeColumnName(column.fullName) + " AS " + alias + "_" + this.connection.driver.escapeAliasName(column.fullName));
if (this.aliasMap.mainAlias.metadata.parentEntityMetadata && this.aliasMap.mainAlias.metadata.parentIdColumns) {
const alias = "parentIdColumn_" + this.escapeAlias(this.aliasMap.mainAlias.metadata.parentEntityMetadata.table.name);
this.aliasMap.mainAlias.metadata.parentEntityMetadata.columns.forEach(column => {
// TODO implement partial select
allSelects.push(alias + "." + this.escapeColumn(column.fullName) + " AS " + alias + "_" + this.escapeAlias(column.fullName));
});
}
}
@ -1464,19 +1530,17 @@ export class QueryBuilder<Entity> {
if (!foundAlias)
throw new Error(`Alias "${parentAlias}" was not found`);
const parentMetadata = this.aliasMap.getEntityMetadataByAlias(foundAlias);
if (!parentMetadata)
if (!foundAlias.metadata)
throw new Error("Cannot get entity metadata for the given alias " + foundAlias.name);
const relation = parentMetadata.findRelationWithPropertyName(join.alias.parentPropertyName);
const relation = foundAlias.metadata.findRelationWithPropertyName(join.alias.parentPropertyName);
const junctionMetadata = relation.junctionEntityMetadata;
// const junctionTable = junctionMetadata.table.name;
junctionMetadata.columns.forEach(column => {
allSelects.push(this.connection.driver.escapeAliasName(join.alias.name) + "." + this.connection.driver.escapeColumnName(column.fullName) + " AS " + this.connection.driver.escapeAliasName(join.alias.name + "_" + column.fullName));
allSelects.push(this.escapeAlias(join.alias.name) + "." + this.escapeColumn(column.fullName) + " AS " + this.escapeAlias(join.alias.name + "_" + column.fullName));
});
});
//
/*if (this.enableRelationIdValues) {
const parentMetadata = this.aliasMap.getEntityMetadataByAlias(this.aliasMap.mainAlias);
if (!parentMetadata)
@ -1487,18 +1551,17 @@ export class QueryBuilder<Entity> {
const junctionMetadata = relation.junctionEntityMetadata;
junctionMetadata.columns.forEach(column => {
const select = this.connection.driver.escapeAliasName(this.aliasMap.mainAlias.name + "_" + junctionMetadata.table.name + "_ids") + "." +
this.connection.driver.escapeColumnName(column.name) + " AS " +
this.connection.driver.escapeAliasName(this.aliasMap.mainAlias.name + "_" + relation.name + "_ids_" + column.name);
const select = this.escapeAlias(this.aliasMap.mainAlias.name + "_" + junctionMetadata.table.name + "_ids") + "." +
this.escapeColumn(column.name) + " AS " +
this.escapeAlias(this.aliasMap.mainAlias.name + "_" + relation.name + "_ids_" + column.name);
allSelects.push(select);
});
});
}*/
// add all other selects
this.selects.filter(select => {
return select !== alias && !this.joins.find(join => join.alias.name === select);
}).forEach(select => allSelects.push(this.replacePropertyNames(select)));
this.selects.filter(select => excludedSelects.indexOf(select) === -1)
.forEach(select => allSelects.push(this.replacePropertyNames(select)));
// if still selection is empty, then simply set it to all (*)
if (allSelects.length === 0)
@ -1519,10 +1582,10 @@ export class QueryBuilder<Entity> {
// create a selection query
switch (this.type) {
case "select":
return "SELECT " + allSelects.join(", ") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + this.connection.driver.escapeAliasName(alias) + lock;
return "SELECT " + allSelects.join(", ") + " FROM " + this.escapeTable(tableName) + " " + this.escapeAlias(alias) + lock;
case "delete":
return "DELETE FROM " + this.connection.driver.escapeTableName(tableName);
// return "DELETE " + (alias ? this.connection.driver.escapeAliasName(alias) : "") + " FROM " + this.connection.driver.escapeTableName(tableName) + " " + (alias ? this.connection.driver.escapeAliasName(alias) : ""); // TODO: only mysql supports aliasing, so what to do with aliases in DELETE queries? right now aliases are used however we are relaying that they will always match a table names
return "DELETE FROM " + this.escapeTable(tableName);
// return "DELETE " + (alias ? this.escapeAlias(alias) : "") + " FROM " + this.escapeTable(tableName) + " " + (alias ? this.escapeAlias(alias) : ""); // TODO: only mysql supports aliasing, so what to do with aliases in DELETE queries? right now aliases are used however we are relaying that they will always match a table names
case "update":
const updateSet = Object.keys(this.updateQuerySet).map(key => key + "=:updateQuerySet_" + key);
const params = Object.keys(this.updateQuerySet).reduce((object, key) => {
@ -1531,7 +1594,7 @@ export class QueryBuilder<Entity> {
return object;
}, {});
this.setParameters(params);
return "UPDATE " + tableName + " " + (alias ? this.connection.driver.escapeAliasName(alias) : "") + " SET " + updateSet;
return "UPDATE " + tableName + " " + (alias ? this.escapeAlias(alias) : "") + " SET " + updateSet;
}
throw new Error("No query builder type is specified.");
@ -1582,22 +1645,21 @@ export class QueryBuilder<Entity> {
*/
private replacePropertyNames(statement: string) {
this.aliasMap.aliases.forEach(alias => {
const metadata = this.aliasMap.getEntityMetadataByAlias(alias);
if (!metadata) return;
metadata.embeddeds.forEach(embedded => {
if (!alias.metadata) return;
alias.metadata.embeddeds.forEach(embedded => {
embedded.columns.forEach(column => {
const expression = alias.name + "\\." + embedded.propertyName + "\\." + column.propertyName + "([ =]|.{0}$)";
statement = statement.replace(new RegExp(expression, "gm"), this.connection.driver.escapeAliasName(alias.name) + "." + this.connection.driver.escapeColumnName(column.fullName) + "$1");
statement = statement.replace(new RegExp(expression, "gm"), this.escapeAlias(alias.name) + "." + this.escapeColumn(column.fullName) + "$1");
});
// todo: what about embedded relations here?
});
metadata.columns.filter(column => !column.isInEmbedded).forEach(column => {
alias.metadata.columns.filter(column => !column.isInEmbedded).forEach(column => {
const expression = alias.name + "\\." + column.propertyName + "([ =]|.{0}$)";
statement = statement.replace(new RegExp(expression, "gm"), this.connection.driver.escapeAliasName(alias.name) + "." + this.connection.driver.escapeColumnName(column.fullName) + "$1");
statement = statement.replace(new RegExp(expression, "gm"), this.escapeAlias(alias.name) + "." + this.escapeColumn(column.fullName) + "$1");
});
metadata.relationsWithJoinColumns/*.filter(relation => !relation.isInEmbedded)*/.forEach(relation => {
alias.metadata.relationsWithJoinColumns/*.filter(relation => !relation.isInEmbedded)*/.forEach(relation => {
const expression = alias.name + "\\." + relation.propertyName + "([ =]|.{0}$)";
statement = statement.replace(new RegExp(expression, "gm"), this.connection.driver.escapeAliasName(alias.name) + "." + this.connection.driver.escapeColumnName(relation.name) + "$1");
statement = statement.replace(new RegExp(expression, "gm"), this.escapeAlias(alias.name) + "." + this.escapeColumn(relation.name) + "$1");
});
});
return statement;
@ -1623,16 +1685,15 @@ export class QueryBuilder<Entity> {
let condition1 = "";
if (relation.isOwning) {
condition1 = this.connection.driver.escapeAliasName(junctionAlias) + "." + this.connection.driver.escapeColumnName(junctionMetadata.columns[0].fullName) + "=" + this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(joinTableColumn);
condition1 = this.escapeAlias(junctionAlias) + "." + this.escapeColumn(junctionMetadata.columns[0].fullName) + "=" + this.escapeAlias(parentAlias) + "." + this.escapeColumn(joinTableColumn);
// condition2 = joinAlias + "." + inverseJoinColumnName + "=" + junctionAlias + "." + junctionMetadata.columns[1].name;
} else {
condition1 = this.connection.driver.escapeAliasName(junctionAlias) + "." + this.connection.driver.escapeColumnName(junctionMetadata.columns[1].fullName) + "=" + this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(joinTableColumn);
condition1 = this.escapeAlias(junctionAlias) + "." + this.escapeColumn(junctionMetadata.columns[1].fullName) + "=" + this.escapeAlias(parentAlias) + "." + this.escapeColumn(joinTableColumn);
// condition2 = joinAlias + "." + inverseJoinColumnName + "=" + junctionAlias + "." + junctionMetadata.columns[0].name;
}
return " " + join.type + " JOIN " + junctionTable + " " + this.connection.driver.escapeAliasName(junctionAlias) + " ON " + condition1;
return " " + join.type + " JOIN " + junctionTable + " " + this.escapeAlias(junctionAlias) + " ON " + condition1;
// " " + joinType + " JOIN " + joinTableName + " " + joinAlias + " " + join.conditionType + " " + condition2 + appendedCondition;
// console.log(join);
// return " " + join.type + " JOIN " + joinTableName + " " + join.alias.name + " " + (join.condition ? (join.conditionType + " " + join.condition) : "");
});
}
@ -1642,16 +1703,16 @@ export class QueryBuilder<Entity> {
const joinType = join.type; // === "INNER" ? "INNER" : "LEFT";
let joinTableName: string = join.tableName;
if (!joinTableName) {
const metadata = this.aliasMap.getEntityMetadataByAlias(join.alias);
if (!metadata)
if (!join.alias.metadata)
throw new Error("Cannot get entity metadata for the given alias " + join.alias.name);
joinTableName = metadata.table.name;
joinTableName = join.alias.metadata.table.name;
}
const parentAlias = join.alias.parentAliasName;
if (!parentAlias) {
return " " + joinType + " JOIN " + this.connection.driver.escapeTableName(joinTableName) + " " + this.connection.driver.escapeAliasName(join.alias.name) + " " + (join.condition ? ( "ON " + this.replacePropertyNames(join.condition) ) : "");
return " " + joinType + " JOIN " + this.escapeTable(joinTableName) + " " + this.escapeAlias(join.alias.name) + " " + (join.condition ? ( "ON " + this.replacePropertyNames(join.condition) ) : "");
}
const foundAlias = this.aliasMap.findAliasByName(parentAlias);
@ -1676,25 +1737,25 @@ export class QueryBuilder<Entity> {
let condition1 = "", condition2 = "";
if (relation.isOwning) {
condition1 = this.connection.driver.escapeAliasName(junctionAlias) + "." + this.connection.driver.escapeColumnName(junctionMetadata.columns[0].fullName) + "=" + this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(joinTableColumn);
condition2 = this.connection.driver.escapeAliasName(joinAlias) + "." + this.connection.driver.escapeColumnName(inverseJoinColumnName) + "=" + this.connection.driver.escapeAliasName(junctionAlias) + "." + this.connection.driver.escapeColumnName(junctionMetadata.columns[1].fullName);
condition1 = this.escapeAlias(junctionAlias) + "." + this.escapeColumn(junctionMetadata.columns[0].fullName) + "=" + this.escapeAlias(parentAlias) + "." + this.escapeColumn(joinTableColumn);
condition2 = this.escapeAlias(joinAlias) + "." + this.escapeColumn(inverseJoinColumnName) + "=" + this.escapeAlias(junctionAlias) + "." + this.escapeColumn(junctionMetadata.columns[1].fullName);
} else {
condition1 = this.connection.driver.escapeAliasName(junctionAlias) + "." + this.connection.driver.escapeColumnName(junctionMetadata.columns[1].fullName) + "=" + this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(joinTableColumn);
condition2 = this.connection.driver.escapeAliasName(joinAlias) + "." + this.connection.driver.escapeColumnName(inverseJoinColumnName) + "=" + this.connection.driver.escapeAliasName(junctionAlias) + "." + this.connection.driver.escapeColumnName(junctionMetadata.columns[0].fullName);
condition1 = this.escapeAlias(junctionAlias) + "." + this.escapeColumn(junctionMetadata.columns[1].fullName) + "=" + this.escapeAlias(parentAlias) + "." + this.escapeColumn(joinTableColumn);
condition2 = this.escapeAlias(joinAlias) + "." + this.escapeColumn(inverseJoinColumnName) + "=" + this.escapeAlias(junctionAlias) + "." + this.escapeColumn(junctionMetadata.columns[0].fullName);
}
return " " + joinType + " JOIN " + this.connection.driver.escapeTableName(junctionTable) + " " + this.connection.driver.escapeAliasName(junctionAlias) + " ON " + condition1 +
" " + joinType + " JOIN " + this.connection.driver.escapeTableName(joinTableName) + " " + this.connection.driver.escapeAliasName(joinAlias) + " ON " + condition2 + appendedCondition;
return " " + joinType + " JOIN " + this.escapeTable(junctionTable) + " " + this.escapeAlias(junctionAlias) + " ON " + condition1 +
" " + joinType + " JOIN " + this.escapeTable(joinTableName) + " " + this.escapeAlias(joinAlias) + " ON " + condition2 + appendedCondition;
} else if (relation.isManyToOne || (relation.isOneToOne && relation.isOwning)) {
const joinTableColumn = relation.joinColumn.referencedColumn.fullName;
const condition = this.connection.driver.escapeAliasName(join.alias.name) + "." + this.connection.driver.escapeColumnName(joinTableColumn) + "=" + this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(relation.name);
return " " + joinType + " JOIN " + this.connection.driver.escapeTableName(joinTableName) + " " + this.connection.driver.escapeAliasName(join.alias.name) + " ON " + condition + appendedCondition;
const condition = this.escapeAlias(join.alias.name) + "." + this.escapeColumn(joinTableColumn) + "=" + this.escapeAlias(parentAlias) + "." + this.escapeColumn(relation.name);
return " " + joinType + " JOIN " + this.escapeTable(joinTableName) + " " + this.escapeAlias(join.alias.name) + " ON " + condition + appendedCondition;
} else if (relation.isOneToMany || (relation.isOneToOne && !relation.isOwning)) {
const joinTableColumn = relation.inverseRelation.joinColumn.referencedColumn.fullName;
const condition = this.connection.driver.escapeAliasName(join.alias.name) + "." + this.connection.driver.escapeColumnName(relation.inverseRelation.name) + "=" + this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(joinTableColumn);
return " " + joinType + " JOIN " + this.connection.driver.escapeTableName(joinTableName) + " " + this.connection.driver.escapeAliasName(join.alias.name) + " ON " + condition + appendedCondition;
const condition = this.escapeAlias(join.alias.name) + "." + this.escapeColumn(relation.inverseRelation.name) + "=" + this.escapeAlias(parentAlias) + "." + this.escapeColumn(joinTableColumn);
return " " + joinType + " JOIN " + this.escapeTable(joinTableName) + " " + this.escapeAlias(join.alias.name) + " ON " + condition + appendedCondition;
} else {
throw new Error("Unexpected relation type"); // this should not be possible
@ -1704,8 +1765,8 @@ export class QueryBuilder<Entity> {
if (!this.ignoreParentTablesJoins && !this.fromTableName) {
const metadata = this.connection.getMetadata(this.aliasMap.mainAlias.target);
if (metadata.parentEntityMetadata && metadata.parentIdColumns) {
const alias = this.connection.driver.escapeAliasName("parentIdColumn_" + metadata.parentEntityMetadata.table.name);
joins += " JOIN " + this.connection.driver.escapeTableName(metadata.parentEntityMetadata.table.name)
const alias = this.escapeAlias("parentIdColumn_" + metadata.parentEntityMetadata.table.name);
joins += " JOIN " + this.escapeTable(metadata.parentEntityMetadata.table.name)
+ " " + alias + " ON ";
joins += metadata.parentIdColumns.map(parentIdColumn => {
return this.aliasMap.mainAlias.name + "." + parentIdColumn.fullName + "=" + alias + "." + parentIdColumn.propertyName;
@ -1729,18 +1790,18 @@ export class QueryBuilder<Entity> {
let condition1 = "";
if (relation.isOwning) {
condition1 = this.connection.driver.escapeAliasName(junctionAlias) + "." +
this.connection.driver.escapeColumnName(junctionMetadata.columns[0].name) + "=" +
this.connection.driver.escapeAliasName(this.aliasMap.mainAlias.name) + "." +
this.connection.driver.escapeColumnName(joinTableColumn);
condition1 = this.escapeAlias(junctionAlias) + "." +
this.escapeColumn(junctionMetadata.columns[0].name) + "=" +
this.escapeAlias(this.aliasMap.mainAlias.name) + "." +
this.escapeColumn(joinTableColumn);
} else {
condition1 = this.connection.driver.escapeAliasName(junctionAlias) + "." +
this.connection.driver.escapeColumnName(junctionMetadata.columns[1].name) + "=" +
this.connection.driver.escapeAliasName(this.aliasMap.mainAlias.name) + "." +
this.connection.driver.escapeColumnName(joinTableColumn);
condition1 = this.escapeAlias(junctionAlias) + "." +
this.escapeColumn(junctionMetadata.columns[1].name) + "=" +
this.escapeAlias(this.aliasMap.mainAlias.name) + "." +
this.escapeColumn(joinTableColumn);
}
return " LEFT JOIN " + junctionTable + " " + this.connection.driver.escapeAliasName(junctionAlias) + " ON " + condition1;
return " LEFT JOIN " + junctionTable + " " + this.escapeAlias(junctionAlias) + " ON " + condition1;
}).join(" ");
}*/
@ -1764,14 +1825,14 @@ export class QueryBuilder<Entity> {
const selectString = Object.keys(orderBys)
.map(columnName => {
const [alias, column, ...embeddedProperties] = columnName.split(".");
return this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(alias + "_" + column + embeddedProperties.join("_"));
return this.escapeAlias(parentAlias) + "." + this.escapeColumn(alias + "_" + column + embeddedProperties.join("_"));
})
.join(", ");
const orderByString = Object.keys(orderBys)
.map(columnName => {
const [alias, column, ...embeddedProperties] = columnName.split(".");
return this.connection.driver.escapeAliasName(parentAlias) + "." + this.connection.driver.escapeColumnName(alias + "_" + column + embeddedProperties.join("_")) + " " + this.orderBys[columnName];
return this.escapeAlias(parentAlias) + "." + this.escapeColumn(alias + "_" + column + embeddedProperties.join("_")) + " " + this.orderBys[columnName];
})
.join(", ");
@ -1882,14 +1943,30 @@ export class QueryBuilder<Entity> {
const aliasObj = new Alias(alias);
this.aliasMap.addAlias(aliasObj);
if (entityOrProperty instanceof Function) {
aliasObj.target = entityOrProperty;
aliasObj.metadata = this.connection.getMetadata(entityOrProperty);
} else if (this.isPropertyAlias(entityOrProperty)) {
[aliasObj.parentAliasName, aliasObj.parentPropertyName] = entityOrProperty.split(".");
const parentAlias = this.aliasMap.findAliasByName(aliasObj.parentAliasName);
// todo: throw exception if parentAlias not found
// todo: throw exception if parentAlias.metadata not found
// todo: throw exception if parentAlias not found
// todo: throw exception if relation not found?
const relation = parentAlias!.metadata.findRelationWithPropertyName(aliasObj.parentPropertyName);
aliasObj.metadata = relation.inverseEntityMetadata;
} else if (typeof entityOrProperty === "string") {
tableName = entityOrProperty;
if (!mapToProperty)
// check if we have entity with such table name, and use its metadata if found
const metadata = this.connection.entityMetadatas.find(metadata => metadata.table.name === entityOrProperty);
if (metadata) {
aliasObj.metadata = metadata;
} else {
tableName = entityOrProperty;
}
if (!mapToProperty) // todo: comment why its needed
mapToProperty = entityOrProperty;
}
@ -1929,10 +2006,6 @@ export class QueryBuilder<Entity> {
return this;
}
private isValueSimpleString(str: any) {
return /^[A-Za-z0-9_-]+$/.test(str);
}
private isPropertyAlias(str: any): str is string {
if (!(typeof str === "string"))
return false;
@ -1960,8 +2033,8 @@ export class QueryBuilder<Entity> {
const metadata = this.connection.getMetadata(this.aliasMap.mainAlias.target);
// create shortcuts for better readability
const escapeAlias = (alias: string) => this.connection.driver.escapeAliasName(alias);
const escapeColumn = (column: string) => this.connection.driver.escapeColumnName(column);
const escapeAlias = (alias: string) => this.escapeAlias(alias);
const escapeColumn = (column: string) => this.escapeColumn(column);
const alias = this.aliasMap.mainAlias.name;
const parameters: ObjectLiteral = {};

View File

@ -1,9 +1,10 @@
import {EntityMetadata} from "../../metadata/EntityMetadata";
/**
*/
export class Alias {
isMain: boolean;
name: string;
target: Function|string;
metadata: EntityMetadata; // TODO can be undefined
parentPropertyName: string;
parentAliasName: string;
@ -15,4 +16,8 @@ export class Alias {
return this.parentAliasName + "." + this.parentPropertyName;
}
get target(): Function|string {
return this.metadata.target;
}
}

View File

@ -57,10 +57,13 @@ export class AliasMap {
});
}
/**
* @deprecated
*/
getEntityMetadataByAlias(alias: Alias): EntityMetadata|undefined {
if (alias.target) {
if (alias.metadata) {
// todo: use connection.getMetadata instead?
return this.connection.getMetadata(alias.target);
return alias.metadata;
} else if (alias.parentAliasName && alias.parentPropertyName) {

View File

@ -17,7 +17,7 @@ import {SqliteDriver} from "../../../../src/driver/sqlite/SqliteDriver";
import {OracleDriver} from "../../../../src/driver/oracle/OracleDriver";
import {LockNotSupportedOnGivenDriverError} from "../../../../src/query-builder/error/LockNotSupportedOnGivenDriverError";
describe.only("query builder > locking", () => {
describe("query builder > locking", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({

View File

@ -0,0 +1,21 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../../src/decorator/columns/Column";
import {VersionColumn} from "../../../../../src/decorator/columns/VersionColumn";
@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@VersionColumn()
version: string;
}

View File

@ -0,0 +1,29 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Column} from "../../../../../src/decorator/columns/Column";
import {VersionColumn} from "../../../../../src/decorator/columns/VersionColumn";
import {Category} from "./Category";
import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
description: string;
@Column()
rating: number;
@VersionColumn()
version: string;
@ManyToOne(type => Category)
category: Category;
}

View File

@ -0,0 +1,98 @@
import "reflect-metadata";
import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Connection} from "../../../../src/connection/Connection";
import {Post} from "./entity/Post";
import {expect} from "chai";
describe("query builder > select", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchemaOnConnection: true,
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("should append all entity mapped columns from main selection to select statement", () => Promise.all(connections.map(async connection => {
const sql = connection.entityManager.createQueryBuilder(Post, "post")
.disableQuoting()
.getSql();
expect(sql).to.equal("SELECT post.id AS post_id, " +
"post.title AS post_title, " +
"post.description AS post_description, " +
"post.rating AS post_rating, post.version AS post_version, post.category AS post_category " +
"FROM post post");
})));
it("should append all entity mapped columns from both main selection and join selections to select statement", () => Promise.all(connections.map(async connection => {
const sql = connection.entityManager.createQueryBuilder(Post, "post")
.leftJoinAndSelect("category", "category")
.disableQuoting()
.getSql();
expect(sql).to.equal("SELECT post.id AS post_id, " +
"post.title AS post_title, " +
"post.description AS post_description, " +
"post.rating AS post_rating, " +
"post.version AS post_version, " +
"post.category AS post_category, " +
"category.id AS category_id, " +
"category.name AS category_name," +
" category.description AS category_description, " +
"category.version AS category_version " +
"FROM post post LEFT JOIN category category");
})));
it("should append entity mapped columns from both main alias and join aliases to select statement", () => Promise.all(connections.map(async connection => {
const sql = connection.entityManager.createQueryBuilder(Post, "post")
.select("post.id")
.addSelect("category.name")
.leftJoin("category", "category")
.disableQuoting()
.getSql();
expect(sql).to.equal("SELECT post.id AS post_id, " +
"category.name AS category_name " +
"FROM post post LEFT JOIN category category");
})));
it("should append entity mapped columns to select statement, if they passed as array", () => Promise.all(connections.map(async connection => {
const sql = connection.entityManager.createQueryBuilder(Post, "post")
.select(["post.id", "post.title"])
.disableQuoting()
.getSql();
expect(sql).to.equal("SELECT post.id AS post_id, post.title AS post_title FROM post post");
})));
it("should append raw sql to select statement", () => Promise.all(connections.map(async connection => {
const sql = connection.entityManager.createQueryBuilder(Post, "post")
.select("COUNT(*) as cnt")
.disableQuoting()
.getSql();
expect(sql).to.equal("SELECT COUNT(*) as cnt FROM post post");
})));
it("should append raw sql and entity mapped column to select statement", () => Promise.all(connections.map(async connection => {
const sql = connection.entityManager.createQueryBuilder(Post, "post")
.select(["COUNT(*) as cnt", "post.title"])
.disableQuoting()
.getSql();
expect(sql).to.equal("SELECT post.title AS post_title, COUNT(*) as cnt FROM post post");
})));
it("should not create alias for selection, which is not entity mapped column", () => Promise.all(connections.map(async connection => {
const sql = connection.entityManager.createQueryBuilder(Post, "post")
.select("post.name")
.disableQuoting()
.getSql();
expect(sql).to.equal("SELECT post.name FROM post post");
})));
});