mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
refactoring query builder; removed useless escape methods
This commit is contained in:
parent
77534b4dcc
commit
67e50063c2
@ -49,19 +49,9 @@ export interface Driver {
|
||||
escapeQueryWithParameters(sql: string, parameters: ObjectLiteral): [string, any[]];
|
||||
|
||||
/**
|
||||
* Escapes a column name.
|
||||
* Escapes a table name, column name or an alias.
|
||||
*/
|
||||
escapeColumn(columnName: string): string;
|
||||
|
||||
/**
|
||||
* Escapes an alias.
|
||||
*/
|
||||
escapeAlias(aliasName: string): string;
|
||||
|
||||
/**
|
||||
* Escapes a table name.
|
||||
*/
|
||||
escapeTable(tableName: string): string;
|
||||
escape(tableName: string): string;
|
||||
|
||||
/**
|
||||
* Prepares given value to a value to be persisted, based on its column type and metadata.
|
||||
|
||||
@ -136,24 +136,10 @@ export class MongoDriver implements Driver {
|
||||
/**
|
||||
* Escapes a column name.
|
||||
*/
|
||||
escapeColumn(columnName: string): string {
|
||||
escape(columnName: string): string {
|
||||
return columnName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes an alias.
|
||||
*/
|
||||
escapeAlias(aliasName: string): string {
|
||||
return aliasName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a table name.
|
||||
*/
|
||||
escapeTable(tableName: string): string {
|
||||
return tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares given value to a value to be persisted, based on its column type and metadata.
|
||||
*/
|
||||
|
||||
@ -187,24 +187,10 @@ export class MysqlDriver implements Driver {
|
||||
/**
|
||||
* Escapes a column name.
|
||||
*/
|
||||
escapeColumn(columnName: string): string {
|
||||
escape(columnName: string): string {
|
||||
return "`" + columnName + "`";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes an alias.
|
||||
*/
|
||||
escapeAlias(aliasName: string): string {
|
||||
return "`" + aliasName + "`";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a table name.
|
||||
*/
|
||||
escapeTable(tableName: string): string {
|
||||
return "`" + tableName + "`";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares given value to a value to be persisted, based on its column type and metadata.
|
||||
*/
|
||||
|
||||
@ -206,24 +206,10 @@ export class OracleDriver implements Driver {
|
||||
/**
|
||||
* Escapes a column name.
|
||||
*/
|
||||
escapeColumn(columnName: string): string {
|
||||
escape(columnName: string): string {
|
||||
return `"${columnName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes an alias.
|
||||
*/
|
||||
escapeAlias(aliasName: string): string {
|
||||
return `"${aliasName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a table name.
|
||||
*/
|
||||
escapeTable(tableName: string): string {
|
||||
return `"${tableName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares given value to a value to be persisted, based on its column type and metadata.
|
||||
*/
|
||||
|
||||
@ -283,24 +283,10 @@ export class PostgresDriver implements Driver {
|
||||
/**
|
||||
* Escapes a column name.
|
||||
*/
|
||||
escapeColumn(columnName: string): string {
|
||||
escape(columnName: string): string {
|
||||
return "\"" + columnName + "\"";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes an alias.
|
||||
*/
|
||||
escapeAlias(aliasName: string): string {
|
||||
return "\"" + aliasName + "\"";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a table name.
|
||||
*/
|
||||
escapeTable(tableName: string): string {
|
||||
return "\"" + tableName + "\"";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database type from a given column metadata.
|
||||
*/
|
||||
|
||||
@ -241,24 +241,10 @@ export class SqliteDriver implements Driver {
|
||||
/**
|
||||
* Escapes a column name.
|
||||
*/
|
||||
escapeColumn(columnName: string): string {
|
||||
escape(columnName: string): string {
|
||||
return "\"" + columnName + "\"";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes an alias.
|
||||
*/
|
||||
escapeAlias(aliasName: string): string {
|
||||
return "\"" + aliasName + "\"";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a table name.
|
||||
*/
|
||||
escapeTable(tableName: string): string {
|
||||
return "\"" + tableName + "\"";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database type from a given column metadata.
|
||||
*/
|
||||
|
||||
@ -211,24 +211,10 @@ export class SqlServerDriver implements Driver {
|
||||
/**
|
||||
* Escapes a column name.
|
||||
*/
|
||||
escapeColumn(columnName: string): string {
|
||||
escape(columnName: string): string {
|
||||
return `"${columnName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes an alias.
|
||||
*/
|
||||
escapeAlias(aliasName: string): string {
|
||||
return `"${aliasName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a table name.
|
||||
*/
|
||||
escapeTable(tableName: string): string {
|
||||
return `"${tableName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares given value to a value to be persisted, based on its column type and metadata.
|
||||
*/
|
||||
|
||||
@ -167,24 +167,10 @@ export class WebsqlDriver implements Driver {
|
||||
/**
|
||||
* Escapes a column name.
|
||||
*/
|
||||
escapeColumn(columnName: string): string {
|
||||
escape(columnName: string): string {
|
||||
return columnName; // "`" + columnName + "`";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes an alias.
|
||||
*/
|
||||
escapeAlias(aliasName: string): string {
|
||||
return aliasName; // "`" + aliasName + "`";
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a table name.
|
||||
*/
|
||||
escapeTable(tableName: string): string {
|
||||
return tableName; // "`" + tableName + "`";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares given value to a value to be persisted, based on its column type and metadata.
|
||||
*/
|
||||
|
||||
@ -551,8 +551,7 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
|
||||
let databaseEntities: ObjectLiteral[] = [];
|
||||
|
||||
// create shortcuts for better readability
|
||||
const ea = (alias: string) => this.connection.driver.escapeAlias(alias);
|
||||
const ec = (column: string) => this.connection.driver.escapeColumn(column);
|
||||
const escape = (name: string) => this.connection.driver.escape(name);
|
||||
|
||||
if (relation.isManyToManyOwner) {
|
||||
|
||||
@ -560,13 +559,13 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
|
||||
// because remove by cascades is the only reason we need relational entities here
|
||||
if (!relation.isCascadeRemove) return;
|
||||
|
||||
const joinAlias = ea("persistenceJoinedRelation");
|
||||
const joinAlias = escape("persistenceJoinedRelation");
|
||||
|
||||
const joinColumnConditions = relation.joinColumns.map(joinColumn => {
|
||||
return `${joinAlias}.${joinColumn.propertyName} = :${joinColumn.propertyName}`;
|
||||
});
|
||||
const inverseJoinColumnConditions = relation.inverseJoinColumns.map(inverseJoinColumn => {
|
||||
return `${joinAlias}.${inverseJoinColumn.propertyName} = ${ea(qbAlias)}.${ec(inverseJoinColumn.referencedColumn!.propertyName)}`;
|
||||
return `${joinAlias}.${inverseJoinColumn.propertyName} = ${escape(qbAlias)}.${escape(inverseJoinColumn.referencedColumn!.propertyName)}`;
|
||||
});
|
||||
|
||||
const conditions = joinColumnConditions.concat(inverseJoinColumnConditions).join(" AND ");
|
||||
@ -591,10 +590,10 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
|
||||
// because remove by cascades is the only reason we need relational entities here
|
||||
if (!relation.isCascadeRemove) return;
|
||||
|
||||
const joinAlias = ea("persistenceJoinedRelation");
|
||||
const joinAlias = escape("persistenceJoinedRelation");
|
||||
|
||||
const joinColumnConditions = relation.joinColumns.map(joinColumn => {
|
||||
return `${joinAlias}.${joinColumn.propertyName} = ${ea(qbAlias)}.${ec(joinColumn.referencedColumn!.propertyName)}`;
|
||||
return `${joinAlias}.${joinColumn.propertyName} = ${escape(qbAlias)}.${escape(joinColumn.referencedColumn!.propertyName)}`;
|
||||
});
|
||||
const inverseJoinColumnConditions = relation.inverseJoinColumns.map(inverseJoinColumn => {
|
||||
return `${joinAlias}.${inverseJoinColumn.propertyName} = :${inverseJoinColumn.propertyName}`;
|
||||
|
||||
@ -71,7 +71,7 @@ export class DeleteQueryBuilder<Entity> extends QueryBuilder<Entity> {
|
||||
* Creates DELETE express used to perform insert query.
|
||||
*/
|
||||
protected createDeleteExpression() {
|
||||
const tableName = this.escapeTable(this.getTableName());
|
||||
const tableName = this.escape(this.getTableName());
|
||||
return `DELETE FROM ${tableName}`; // todo: how do we replace aliases in where to nothing?
|
||||
}
|
||||
|
||||
|
||||
@ -76,8 +76,8 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
|
||||
}).join(", ");
|
||||
|
||||
// get a table name and all column database names
|
||||
const tableName = this.escapeTable(this.getTableName());
|
||||
const columnNames = insertColumns.map(column => this.escapeColumn(column.databaseName)).join(", ");
|
||||
const tableName = this.escape(this.getTableName());
|
||||
const columnNames = insertColumns.map(column => this.escape(column.databaseName)).join(", ");
|
||||
|
||||
// generate sql query
|
||||
return `INSERT INTO ${tableName}(${columnNames}) VALUES ${values}`;
|
||||
|
||||
@ -283,6 +283,14 @@ export abstract class QueryBuilder<Entity> {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets generated sql that will be executed.
|
||||
* Parameters in the query are escaped for the currently used driver.
|
||||
*/
|
||||
getSql(): string {
|
||||
return this.connection.driver.escapeQueryWithParameters(this.getQuery(), this.getParameters())[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints sql to stdout using console.log.
|
||||
*/
|
||||
@ -291,14 +299,6 @@ export abstract class QueryBuilder<Entity> {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets generated sql that will be executed.
|
||||
* Parameters in the query are escaped for the currently used driver.
|
||||
*/
|
||||
getSql(): string {
|
||||
return this.connection.driver.escapeQueryWithParameters(this.getQuery(), this.expressionMap.parameters)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets sql to be executed with all parameters used in it.
|
||||
*/
|
||||
@ -348,36 +348,31 @@ export abstract class QueryBuilder<Entity> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes alias name using current database's escaping character.
|
||||
* Escapes table name, column name or alias name using current database's escaping character.
|
||||
*/
|
||||
escapeAlias(name: string) {
|
||||
escape(name: string): string {
|
||||
if (!this.expressionMap.disableEscaping)
|
||||
return name;
|
||||
return this.connection.driver.escapeAlias(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes column name using current database's escaping character.
|
||||
*/
|
||||
escapeColumn(name: string) {
|
||||
if (!this.expressionMap.disableEscaping)
|
||||
return name;
|
||||
return this.connection.driver.escapeColumn(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes table name using current database's escaping character.
|
||||
*/
|
||||
escapeTable(name: string) {
|
||||
if (!this.expressionMap.disableEscaping)
|
||||
return name;
|
||||
return this.connection.driver.escapeTable(name);
|
||||
return this.connection.driver.escape(name);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets name of the table where insert should be performed.
|
||||
*/
|
||||
protected getTableName(): string {
|
||||
if (!this.expressionMap.mainAlias)
|
||||
throw new Error(`Entity where values should be inserted is not specified. Call "qb.into(entity)" method to specify it.`);
|
||||
|
||||
if (this.expressionMap.mainAlias.hasMetadata)
|
||||
return this.expressionMap.mainAlias.metadata.tableName;
|
||||
|
||||
return this.expressionMap.mainAlias.tableName!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies FROM which entity's table select/update/delete will be executed.
|
||||
* Also sets a main string alias of the selection data.
|
||||
@ -401,160 +396,35 @@ export abstract class QueryBuilder<Entity> {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected buildEscapedEntityColumnSelects(aliasName: string, metadata: EntityMetadata): SelectQuery[] {
|
||||
const hasMainAlias = this.expressionMap.selects.some(select => select.selection === aliasName);
|
||||
|
||||
const columns: ColumnMetadata[] = hasMainAlias ? metadata.columns : metadata.columns.filter(column => {
|
||||
return this.expressionMap.selects.some(select => select.selection === aliasName + "." + column.propertyName);
|
||||
});
|
||||
|
||||
return columns.map(column => {
|
||||
const selection = this.expressionMap.selects.find(select => select.selection === aliasName + "." + column.propertyName);
|
||||
return {
|
||||
selection: this.escapeAlias(aliasName) + "." + this.escapeColumn(column.databaseName),
|
||||
aliasName: selection && selection.aliasName ? selection.aliasName : aliasName + "_" + column.databaseName,
|
||||
// todo: need to keep in mind that custom selection.aliasName breaks hydrator. fix it later!
|
||||
};
|
||||
// return this.escapeAlias(aliasName) + "." + this.escapeColumn(column.fullName) +
|
||||
// " AS " + this.escapeAlias(aliasName + "_" + column.fullName);
|
||||
});
|
||||
}
|
||||
|
||||
protected findEntityColumnSelects(aliasName: string, metadata: EntityMetadata): SelectQuery[] {
|
||||
const mainSelect = this.expressionMap.selects.find(select => select.selection === aliasName);
|
||||
if (mainSelect)
|
||||
return [mainSelect];
|
||||
|
||||
return this.expressionMap.selects.filter(select => {
|
||||
return metadata.columns.some(column => select.selection === aliasName + "." + column.propertyName);
|
||||
});
|
||||
}
|
||||
|
||||
// todo: extract all create expression methods to separate class QueryExpressionBuilder
|
||||
|
||||
protected createSelectExpression() {
|
||||
|
||||
if (!this.expressionMap.mainAlias)
|
||||
throw new Error("Cannot build query because main alias is not set (call qb#from method)");
|
||||
|
||||
// separate escaping functions are used to reduce code size and complexity below
|
||||
const et = (aliasName: string) => this.escapeTable(aliasName);
|
||||
const ea = (aliasName: string) => this.escapeAlias(aliasName);
|
||||
const ec = (aliasName: string) => this.escapeColumn(aliasName);
|
||||
|
||||
// todo throw exception if selects or from is missing
|
||||
|
||||
let tableName: string;
|
||||
const allSelects: SelectQuery[] = [];
|
||||
const excludedSelects: SelectQuery[] = [];
|
||||
|
||||
const aliasName = this.expressionMap.mainAlias.name;
|
||||
|
||||
if (this.expressionMap.mainAlias.hasMetadata) {
|
||||
const metadata = this.expressionMap.mainAlias.metadata;
|
||||
tableName = metadata.tableName;
|
||||
|
||||
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!;
|
||||
}
|
||||
|
||||
// add selects from joins
|
||||
this.expressionMap.joinAttributes
|
||||
.forEach(join => {
|
||||
if (join.metadata) {
|
||||
allSelects.push(...this.buildEscapedEntityColumnSelects(join.alias.name!, join.metadata));
|
||||
excludedSelects.push(...this.findEntityColumnSelects(join.alias.name!, join.metadata));
|
||||
} else {
|
||||
const hasMainAlias = this.expressionMap.selects.some(select => select.selection === join.alias.name);
|
||||
if (hasMainAlias) {
|
||||
allSelects.push({ selection: ea(join.alias.name!) + ".*" });
|
||||
excludedSelects.push({ selection: ea(join.alias.name!) });
|
||||
}
|
||||
/**
|
||||
* Replaces all entity's propertyName to name in the given statement.
|
||||
*/
|
||||
protected replacePropertyNames(statement: string) {
|
||||
this.expressionMap.aliases.forEach(alias => {
|
||||
if (!alias.hasMetadata) return;
|
||||
alias.metadata.columns.forEach(column => {
|
||||
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + column.propertyPath + "([ =\)\,]|.{0}$)";
|
||||
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escape(alias.name) + "." + this.escape(column.databaseName) + "$2");
|
||||
const expression2 = "([ =\(]|^.{0})" + alias.name + "\\." + column.propertyName + "([ =\)\,]|.{0}$)";
|
||||
statement = statement.replace(new RegExp(expression2, "gm"), "$1" + this.escape(alias.name) + "." + this.escape(column.databaseName) + "$2");
|
||||
});
|
||||
alias.metadata.relations.forEach(relation => {
|
||||
[...relation.joinColumns, ...relation.inverseJoinColumns].forEach(joinColumn => {
|
||||
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + relation.propertyPath + "\\." + joinColumn.referencedColumn!.propertyPath + "([ =\)\,]|.{0}$)";
|
||||
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escape(alias.name) + "." + this.escape(joinColumn.databaseName) + "$2"); // todo: fix relation.joinColumns[0], what if multiple columns
|
||||
});
|
||||
if (relation.joinColumns.length > 0) {
|
||||
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + relation.propertyPath + "([ =\)\,]|.{0}$)";
|
||||
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escape(alias.name) + "." + this.escape(relation.joinColumns[0].databaseName) + "$2"); // todo: fix relation.joinColumns[0], what if multiple columns
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.expressionMap.ignoreParentTablesJoins && this.expressionMap.mainAlias.hasMetadata) {
|
||||
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) + "." + ec(column.databaseName), aliasName: alias + "_" + column.databaseName });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// add selects from relation id joins
|
||||
// this.relationIdAttributes.forEach(relationIdAttr => {
|
||||
// });
|
||||
|
||||
/*if (this.enableRelationIdValues) {
|
||||
const parentMetadata = this.aliasMap.getEntityMetadataByAlias(this.aliasMap.mainAlias);
|
||||
if (!parentMetadata)
|
||||
throw new Error("Cannot get entity metadata for the given alias " + this.aliasMap.mainAlias.name);
|
||||
|
||||
const metadata = this.connection.entityMetadatas.findByTarget(this.aliasMap.mainAlias.target);
|
||||
metadata.manyToManyRelations.forEach(relation => {
|
||||
|
||||
const junctionMetadata = relation.junctionEntityMetadata;
|
||||
junctionMetadata.columns.forEach(column => {
|
||||
const select = ea(this.aliasMap.mainAlias.name + "_" + junctionMetadata.table.name + "_ids") + "." +
|
||||
ec(column.name) + " AS " +
|
||||
ea(this.aliasMap.mainAlias.name + "_" + relation.name + "_ids_" + column.name);
|
||||
allSelects.push(select);
|
||||
});
|
||||
});
|
||||
}*/
|
||||
|
||||
// add all other selects
|
||||
this.expressionMap.selects
|
||||
.filter(select => excludedSelects.indexOf(select) === -1)
|
||||
.forEach(select => allSelects.push({ selection: this.replacePropertyNames(select.selection), aliasName: select.aliasName }));
|
||||
|
||||
// if still selection is empty, then simply set it to all (*)
|
||||
if (allSelects.length === 0)
|
||||
allSelects.push({ selection: "*" });
|
||||
|
||||
let lock: string = "";
|
||||
if (this.connection.driver instanceof SqlServerDriver) {
|
||||
switch (this.expressionMap.lockMode) {
|
||||
case "pessimistic_read":
|
||||
lock = " WITH (HOLDLOCK, ROWLOCK)";
|
||||
break;
|
||||
case "pessimistic_write":
|
||||
lock = " WITH (UPDLOCK, ROWLOCK)";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// create a selection query
|
||||
const selection = allSelects.map(select => select.selection + (select.aliasName ? " AS " + ea(select.aliasName) : "")).join(", ");
|
||||
if ((this.expressionMap.limit || this.expressionMap.offset) && this.connection.driver instanceof OracleDriver) {
|
||||
return "SELECT ROWNUM " + this.escapeAlias("RN") + "," + selection + " FROM " + this.escapeTable(tableName) + " " + ea(aliasName) + lock;
|
||||
}
|
||||
return "SELECT " + selection + " FROM " + this.escapeTable(tableName) + " " + ea(aliasName) + lock;
|
||||
}
|
||||
|
||||
protected createHavingExpression() {
|
||||
if (!this.expressionMap.havings || !this.expressionMap.havings.length) return "";
|
||||
const conditions = this.expressionMap.havings.map((having, index) => {
|
||||
switch (having.type) {
|
||||
case "and":
|
||||
return (index > 0 ? "AND " : "") + this.replacePropertyNames(having.condition);
|
||||
case "or":
|
||||
return (index > 0 ? "OR " : "") + this.replacePropertyNames(having.condition);
|
||||
default:
|
||||
return this.replacePropertyNames(having.condition);
|
||||
}
|
||||
}).join(" ");
|
||||
|
||||
if (!conditions.length) return "";
|
||||
return " HAVING " + conditions;
|
||||
});
|
||||
return statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates "WHERE" expression.
|
||||
*/
|
||||
protected createWhereExpression() {
|
||||
|
||||
const conditions = this.expressionMap.wheres.map((where, index) => {
|
||||
@ -585,254 +455,6 @@ export abstract class QueryBuilder<Entity> {
|
||||
return " WHERE " + conditions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all entity's propertyName to name in the given statement.
|
||||
*/
|
||||
protected replacePropertyNames(statement: string) {
|
||||
this.expressionMap.aliases.forEach(alias => {
|
||||
if (!alias.hasMetadata) return;
|
||||
alias.metadata.columns.forEach(column => {
|
||||
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + column.propertyPath + "([ =\)\,]|.{0}$)";
|
||||
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escapeAlias(alias.name) + "." + this.escapeColumn(column.databaseName) + "$2");
|
||||
const expression2 = "([ =\(]|^.{0})" + alias.name + "\\." + column.propertyName + "([ =\)\,]|.{0}$)";
|
||||
statement = statement.replace(new RegExp(expression2, "gm"), "$1" + this.escapeAlias(alias.name) + "." + this.escapeColumn(column.databaseName) + "$2");
|
||||
});
|
||||
alias.metadata.relations.forEach(relation => {
|
||||
[...relation.joinColumns, ...relation.inverseJoinColumns].forEach(joinColumn => {
|
||||
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + relation.propertyPath + "\\." + joinColumn.referencedColumn!.propertyPath + "([ =\)\,]|.{0}$)";
|
||||
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escapeAlias(alias.name) + "." + this.escapeColumn(joinColumn.databaseName) + "$2"); // todo: fix relation.joinColumns[0], what if multiple columns
|
||||
});
|
||||
if (relation.joinColumns.length > 0) {
|
||||
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + relation.propertyPath + "([ =\)\,]|.{0}$)";
|
||||
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escapeAlias(alias.name) + "." + this.escapeColumn(relation.joinColumns[0].databaseName) + "$2"); // todo: fix relation.joinColumns[0], what if multiple columns
|
||||
}
|
||||
});
|
||||
});
|
||||
return statement;
|
||||
}
|
||||
|
||||
protected createJoinExpression(): string {
|
||||
|
||||
// separate escaping functions are used to reduce code size and complexity below
|
||||
const et = (aliasName: string) => this.escapeTable(aliasName);
|
||||
const ea = (aliasName: string) => this.escapeAlias(aliasName);
|
||||
const ec = (aliasName: string) => this.escapeColumn(aliasName);
|
||||
|
||||
// examples:
|
||||
// select from owning side
|
||||
// qb.select("post")
|
||||
// .leftJoinAndSelect("post.category", "category");
|
||||
// select from non-owning side
|
||||
// qb.select("category")
|
||||
// .leftJoinAndSelect("category.post", "post");
|
||||
|
||||
const joins = this.expressionMap.joinAttributes.map(joinAttr => {
|
||||
|
||||
const relation = joinAttr.relation;
|
||||
const destinationTableName = joinAttr.tableName;
|
||||
const destinationTableAlias = joinAttr.alias.name;
|
||||
const appendedCondition = joinAttr.condition ? " AND (" + joinAttr.condition + ")" : "";
|
||||
const parentAlias = joinAttr.parentAlias;
|
||||
|
||||
// if join was build without relation (e.g. without "post.category") then it means that we have direct
|
||||
// table to join, without junction table involved. This means we simply join direct table.
|
||||
if (!parentAlias || !relation)
|
||||
return " " + joinAttr.direction + " JOIN " + et(destinationTableName) + " " + ea(destinationTableAlias) +
|
||||
(joinAttr.condition ? " ON " + this.replacePropertyNames(joinAttr.condition) : "");
|
||||
|
||||
// if real entity relation is involved
|
||||
if (relation.isManyToOne || relation.isOneToOneOwner) {
|
||||
|
||||
// JOIN `category` `category` ON `category`.`id` = `post`.`categoryId`
|
||||
const condition = relation.joinColumns.map(joinColumn => {
|
||||
return destinationTableAlias + "." + joinColumn.referencedColumn!.propertyPath + "=" +
|
||||
parentAlias + "." + relation.propertyPath + "." + joinColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
return " " + joinAttr.direction + " JOIN " + et(destinationTableName) + " " + ea(destinationTableAlias) + " ON " + this.replacePropertyNames(condition + appendedCondition);
|
||||
|
||||
} else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
|
||||
|
||||
// JOIN `post` `post` ON `post`.`categoryId` = `category`.`id`
|
||||
const condition = relation.inverseRelation!.joinColumns.map(joinColumn => {
|
||||
return destinationTableAlias + "." + relation.inverseRelation!.propertyPath + "." + joinColumn.referencedColumn!.propertyPath + "=" +
|
||||
parentAlias + "." + joinColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
return " " + joinAttr.direction + " JOIN " + et(destinationTableName) + " " + ea(destinationTableAlias) + " ON " + this.replacePropertyNames(condition + appendedCondition);
|
||||
|
||||
} else { // means many-to-many
|
||||
const junctionTableName = relation.junctionEntityMetadata!.tableName;
|
||||
|
||||
const junctionAlias = joinAttr.junctionAlias;
|
||||
let junctionCondition = "", destinationCondition = "";
|
||||
|
||||
if (relation.isOwning) {
|
||||
|
||||
junctionCondition = relation.joinColumns.map(joinColumn => {
|
||||
// `post_category`.`postId` = `post`.`id`
|
||||
return junctionAlias + "." + joinColumn.propertyPath + "=" + parentAlias + "." + joinColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
destinationCondition = relation.inverseJoinColumns.map(joinColumn => {
|
||||
// `category`.`id` = `post_category`.`categoryId`
|
||||
return destinationTableAlias + "." + joinColumn.referencedColumn!.propertyPath + "=" + junctionAlias + "." + joinColumn.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
} else {
|
||||
junctionCondition = relation.inverseRelation!.inverseJoinColumns.map(joinColumn => {
|
||||
// `post_category`.`categoryId` = `category`.`id`
|
||||
return junctionAlias + "." + joinColumn.propertyPath + "=" + parentAlias + "." + joinColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
destinationCondition = relation.inverseRelation!.joinColumns.map(joinColumn => {
|
||||
// `post`.`id` = `post_category`.`postId`
|
||||
return destinationTableAlias + "." + joinColumn.referencedColumn!.propertyPath + "=" + junctionAlias + "." + joinColumn.propertyPath;
|
||||
}).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);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.expressionMap.ignoreParentTablesJoins && this.expressionMap.mainAlias!.hasMetadata) {
|
||||
const metadata = this.expressionMap.mainAlias!.metadata;
|
||||
if (metadata.parentEntityMetadata && metadata.parentEntityMetadata.inheritanceType === "class-table" && metadata.parentIdColumns) {
|
||||
const alias = "parentIdColumn_" + metadata.parentEntityMetadata.tableName;
|
||||
const condition = metadata.parentIdColumns.map(parentIdColumn => {
|
||||
return this.expressionMap.mainAlias!.name + "." + parentIdColumn.propertyPath + " = " + alias + "." + parentIdColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
const join = " JOIN " + et(metadata.parentEntityMetadata.tableName) + " " + ea(alias) + " ON " + this.replacePropertyNames(condition);
|
||||
joins.push(join);
|
||||
}
|
||||
}
|
||||
|
||||
return joins.join(" ");
|
||||
}
|
||||
|
||||
protected createGroupByExpression() {
|
||||
if (!this.expressionMap.groupBys || !this.expressionMap.groupBys.length) return "";
|
||||
return " GROUP BY " + this.replacePropertyNames(this.expressionMap.groupBys.join(", "));
|
||||
}
|
||||
|
||||
protected createOrderByCombinedWithSelectExpression(parentAlias: string) {
|
||||
|
||||
// if table has a default order then apply it
|
||||
let orderBys = this.expressionMap.orderBys;
|
||||
if (!Object.keys(orderBys).length && this.expressionMap.mainAlias!.hasMetadata) {
|
||||
orderBys = this.expressionMap.mainAlias!.metadata.orderBy || {};
|
||||
}
|
||||
|
||||
const selectString = Object.keys(orderBys)
|
||||
.map(columnName => {
|
||||
const [alias, column, ...embeddedProperties] = columnName.split(".");
|
||||
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.escapeAlias(parentAlias) + "." + this.escapeColumn(alias + "_" + column + embeddedProperties.join("_")) + " " + this.expressionMap.orderBys[columnName];
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
return [selectString, orderByString];
|
||||
}
|
||||
|
||||
protected createOrderByExpression() {
|
||||
|
||||
let orderBys = this.expressionMap.orderBys;
|
||||
|
||||
// if table has a default order then apply it
|
||||
if (!Object.keys(orderBys).length && this.expressionMap.mainAlias!.hasMetadata) {
|
||||
orderBys = this.expressionMap.mainAlias!.metadata.orderBy || {};
|
||||
}
|
||||
|
||||
// if user specified a custom order then apply it
|
||||
if (Object.keys(orderBys).length > 0)
|
||||
return " ORDER BY " + Object.keys(orderBys)
|
||||
.map(columnName => {
|
||||
return this.replacePropertyNames(columnName) + " " + this.expressionMap.orderBys[columnName];
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
protected createLimitOffsetOracleSpecificExpression(sql: string): string {
|
||||
if ((this.expressionMap.offset || this.expressionMap.limit) && this.connection.driver instanceof OracleDriver) {
|
||||
sql = "SELECT * FROM (" + sql + ") WHERE ";
|
||||
if (this.expressionMap.offset) {
|
||||
sql += this.escapeAlias("RN") + " >= " + this.expressionMap.offset;
|
||||
}
|
||||
if (this.expressionMap.limit) {
|
||||
sql += (this.expressionMap.offset ? " AND " : "") + this.escapeAlias("RN") + " <= " + ((this.expressionMap.offset || 0) + this.expressionMap.limit);
|
||||
}
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected createLimitOffsetExpression(): string {
|
||||
if (this.connection.driver instanceof OracleDriver)
|
||||
return "";
|
||||
|
||||
if (this.connection.driver instanceof SqlServerDriver) {
|
||||
|
||||
if (this.expressionMap.limit && this.expressionMap.offset)
|
||||
return " OFFSET " + this.expressionMap.offset + " ROWS FETCH NEXT " + this.expressionMap.limit + " ROWS ONLY";
|
||||
if (this.expressionMap.limit)
|
||||
return " OFFSET 0 ROWS FETCH NEXT " + this.expressionMap.limit + " ROWS ONLY";
|
||||
if (this.expressionMap.offset)
|
||||
return " OFFSET " + this.expressionMap.offset + " ROWS";
|
||||
|
||||
} else {
|
||||
if (this.expressionMap.limit && this.expressionMap.offset)
|
||||
return " LIMIT " + this.expressionMap.limit + " OFFSET " + this.expressionMap.offset;
|
||||
if (this.expressionMap.limit)
|
||||
return " LIMIT " + this.expressionMap.limit;
|
||||
if (this.expressionMap.offset)
|
||||
return " OFFSET " + this.expressionMap.offset;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds lock expression to a query.
|
||||
*/
|
||||
protected createLockExpression(): string {
|
||||
switch (this.expressionMap.lockMode) {
|
||||
case "pessimistic_read":
|
||||
if (this.connection.driver instanceof MysqlDriver) {
|
||||
return " LOCK IN SHARE MODE";
|
||||
|
||||
} else if (this.connection.driver instanceof PostgresDriver) {
|
||||
return " FOR SHARE";
|
||||
|
||||
} else if (this.connection.driver instanceof SqlServerDriver) {
|
||||
return "";
|
||||
|
||||
} else {
|
||||
throw new LockNotSupportedOnGivenDriverError();
|
||||
}
|
||||
case "pessimistic_write":
|
||||
if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof PostgresDriver) {
|
||||
return " FOR UPDATE";
|
||||
|
||||
} else if (this.connection.driver instanceof SqlServerDriver) {
|
||||
return "";
|
||||
|
||||
} else {
|
||||
throw new LockNotSupportedOnGivenDriverError();
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates "WHERE" expression and variables for the given "ids".
|
||||
*/
|
||||
@ -840,32 +462,18 @@ export abstract class QueryBuilder<Entity> {
|
||||
const metadata = this.expressionMap.mainAlias!.metadata;
|
||||
|
||||
// create shortcuts for better readability
|
||||
const ea = (aliasName: string) => this.escapeAlias(aliasName);
|
||||
const ec = (columnName: string) => this.escapeColumn(columnName);
|
||||
|
||||
const alias = this.expressionMap.mainAlias!.name;
|
||||
const parameters: ObjectLiteral = {};
|
||||
const whereStrings = ids.map((id, index) => {
|
||||
const whereSubStrings: string[] = [];
|
||||
// if (metadata.hasMultiplePrimaryKeys) {
|
||||
metadata.primaryColumns.forEach((primaryColumn, secondIndex) => {
|
||||
whereSubStrings.push(ea(alias) + "." + ec(primaryColumn.databaseName) + "=:id_" + index + "_" + secondIndex);
|
||||
parameters["id_" + index + "_" + secondIndex] = primaryColumn.getEntityValue(id);
|
||||
});
|
||||
metadata.parentIdColumns.forEach((parentIdColumn, secondIndex) => {
|
||||
whereSubStrings.push(ea(alias) + "." + ec(parentIdColumn.databaseName) + "=:parentId_" + index + "_" + secondIndex);
|
||||
parameters["parentId_" + index + "_" + secondIndex] = parentIdColumn.getEntityValue(id);
|
||||
});
|
||||
// } else {
|
||||
// if (metadata.primaryColumns.length > 0) {
|
||||
// whereSubStrings.push(ea(alias) + "." + ec(metadata.firstPrimaryColumn.fullName) + "=:id_" + index);
|
||||
// parameters["id_" + index] = id;
|
||||
//
|
||||
// } else if (metadata.parentIdColumns.length > 0) {
|
||||
// whereSubStrings.push(ea(alias) + "." + ec(metadata.parentIdColumns[0].fullName) + "=:parentId_" + index);
|
||||
// parameters["parentId_" + index] = id;
|
||||
// }
|
||||
// }
|
||||
metadata.primaryColumns.forEach((primaryColumn, secondIndex) => {
|
||||
whereSubStrings.push(this.escape(alias) + "." + this.escape(primaryColumn.databaseName) + "=:id_" + index + "_" + secondIndex);
|
||||
parameters["id_" + index + "_" + secondIndex] = primaryColumn.getEntityValue(id);
|
||||
});
|
||||
metadata.parentIdColumns.forEach((parentIdColumn, secondIndex) => {
|
||||
whereSubStrings.push(this.escape(alias) + "." + this.escape(parentIdColumn.databaseName) + "=:parentId_" + index + "_" + secondIndex);
|
||||
parameters["parentId_" + index + "_" + secondIndex] = parentIdColumn.getEntityValue(id);
|
||||
});
|
||||
return whereSubStrings.join(" AND ");
|
||||
});
|
||||
|
||||
@ -873,17 +481,4 @@ export abstract class QueryBuilder<Entity> {
|
||||
return [whereString, parameters];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets name of the table where insert should be performed.
|
||||
*/
|
||||
protected getTableName(): string {
|
||||
if (!this.expressionMap.mainAlias)
|
||||
throw new Error(`Entity where values should be inserted is not specified. Call "qb.into(entity)" method to specify it.`);
|
||||
|
||||
if (this.expressionMap.mainAlias.hasMetadata)
|
||||
return this.expressionMap.mainAlias.metadata.tableName;
|
||||
|
||||
return this.expressionMap.mainAlias.tableName!;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -18,6 +18,13 @@ import {RelationCountMetadataToAttributeTransformer} from "./relation-count/Rela
|
||||
import {Broadcaster} from "../subscriber/Broadcaster";
|
||||
import {QueryBuilder} from "./QueryBuilder";
|
||||
import {ReadStream} from "fs";
|
||||
import {LockNotSupportedOnGivenDriverError} from "./error/LockNotSupportedOnGivenDriverError";
|
||||
import {MysqlDriver} from "../driver/mysql/MysqlDriver";
|
||||
import {PostgresDriver} from "../driver/postgres/PostgresDriver";
|
||||
import {OracleDriver} from "../driver/oracle/OracleDriver";
|
||||
import {SelectQuery} from "./SelectQuery";
|
||||
import {EntityMetadata} from "../metadata/EntityMetadata";
|
||||
import {ColumnMetadata} from "../metadata/ColumnMetadata";
|
||||
|
||||
/**
|
||||
* Allows to build complex sql queries in a fashion way and execute those queries.
|
||||
@ -840,9 +847,9 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> {
|
||||
const mainAlias = this.expressionMap.mainAlias!.name; // todo: will this work with "fromTableName"?
|
||||
const metadata = this.expressionMap.mainAlias!.metadata;
|
||||
|
||||
const distinctAlias = this.escapeAlias(mainAlias);
|
||||
const distinctAlias = this.escape(mainAlias);
|
||||
let countSql = `COUNT(` + metadata.primaryColumns.map((primaryColumn, index) => {
|
||||
const propertyName = this.escapeColumn(primaryColumn.databaseName);
|
||||
const propertyName = this.escape(primaryColumn.databaseName);
|
||||
if (index === 0) {
|
||||
return `DISTINCT(${distinctAlias}.${propertyName})`;
|
||||
} else {
|
||||
@ -905,11 +912,11 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> {
|
||||
const [sql, parameters] = this/*.clone().orderBy()*/.getSqlAndParameters();
|
||||
const [selects, orderBys] = this.createOrderByCombinedWithSelectExpression("distinctAlias");
|
||||
|
||||
const distinctAlias = this.escapeTable("distinctAlias");
|
||||
const distinctAlias = this.escape("distinctAlias");
|
||||
const metadata = this.expressionMap.mainAlias!.metadata;
|
||||
let idsQuery = `SELECT `;
|
||||
idsQuery += metadata.primaryColumns.map((primaryColumn, index) => {
|
||||
const propertyName = this.escapeAlias(mainAliasName + "_" + primaryColumn.databaseName);
|
||||
const propertyName = this.escape(mainAliasName + "_" + primaryColumn.databaseName);
|
||||
if (index === 0) {
|
||||
return `DISTINCT(${distinctAlias}.${propertyName}) as "ids_${primaryColumn.databaseName}"`;
|
||||
} else {
|
||||
@ -1008,4 +1015,368 @@ export class SelectQueryBuilder<Entity> extends QueryBuilder<Entity> {
|
||||
}
|
||||
}
|
||||
|
||||
protected createSelectExpression() {
|
||||
|
||||
if (!this.expressionMap.mainAlias)
|
||||
throw new Error("Cannot build query because main alias is not set (call qb#from method)");
|
||||
|
||||
// todo throw exception if selects or from is missing
|
||||
|
||||
let tableName: string;
|
||||
const allSelects: SelectQuery[] = [];
|
||||
const excludedSelects: SelectQuery[] = [];
|
||||
|
||||
const aliasName = this.expressionMap.mainAlias.name;
|
||||
|
||||
if (this.expressionMap.mainAlias.hasMetadata) {
|
||||
const metadata = this.expressionMap.mainAlias.metadata;
|
||||
tableName = metadata.tableName;
|
||||
|
||||
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!;
|
||||
}
|
||||
|
||||
// add selects from joins
|
||||
this.expressionMap.joinAttributes
|
||||
.forEach(join => {
|
||||
if (join.metadata) {
|
||||
allSelects.push(...this.buildEscapedEntityColumnSelects(join.alias.name!, join.metadata));
|
||||
excludedSelects.push(...this.findEntityColumnSelects(join.alias.name!, join.metadata));
|
||||
} else {
|
||||
const hasMainAlias = this.expressionMap.selects.some(select => select.selection === join.alias.name);
|
||||
if (hasMainAlias) {
|
||||
allSelects.push({ selection: this.escape(join.alias.name!) + ".*" });
|
||||
excludedSelects.push({ selection: this.escape(join.alias.name!) });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.expressionMap.ignoreParentTablesJoins && this.expressionMap.mainAlias.hasMetadata) {
|
||||
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: this.escape(alias) + "." + this.escape(column.databaseName), aliasName: alias + "_" + column.databaseName });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// add selects from relation id joins
|
||||
// this.relationIdAttributes.forEach(relationIdAttr => {
|
||||
// });
|
||||
|
||||
/*if (this.enableRelationIdValues) {
|
||||
const parentMetadata = this.aliasMap.getEntityMetadataByAlias(this.aliasMap.mainAlias);
|
||||
if (!parentMetadata)
|
||||
throw new Error("Cannot get entity metadata for the given alias " + this.aliasMap.mainAlias.name);
|
||||
|
||||
const metadata = this.connection.entityMetadatas.findByTarget(this.aliasMap.mainAlias.target);
|
||||
metadata.manyToManyRelations.forEach(relation => {
|
||||
|
||||
const junctionMetadata = relation.junctionEntityMetadata;
|
||||
junctionMetadata.columns.forEach(column => {
|
||||
const select = ea(this.aliasMap.mainAlias.name + "_" + junctionMetadata.table.name + "_ids") + "." +
|
||||
ec(column.name) + " AS " +
|
||||
ea(this.aliasMap.mainAlias.name + "_" + relation.name + "_ids_" + column.name);
|
||||
allSelects.push(select);
|
||||
});
|
||||
});
|
||||
}*/
|
||||
|
||||
// add all other selects
|
||||
this.expressionMap.selects
|
||||
.filter(select => excludedSelects.indexOf(select) === -1)
|
||||
.forEach(select => allSelects.push({ selection: this.replacePropertyNames(select.selection), aliasName: select.aliasName }));
|
||||
|
||||
// if still selection is empty, then simply set it to all (*)
|
||||
if (allSelects.length === 0)
|
||||
allSelects.push({ selection: "*" });
|
||||
|
||||
let lock: string = "";
|
||||
if (this.connection.driver instanceof SqlServerDriver) {
|
||||
switch (this.expressionMap.lockMode) {
|
||||
case "pessimistic_read":
|
||||
lock = " WITH (HOLDLOCK, ROWLOCK)";
|
||||
break;
|
||||
case "pessimistic_write":
|
||||
lock = " WITH (UPDLOCK, ROWLOCK)";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// create a selection query
|
||||
const selection = allSelects.map(select => select.selection + (select.aliasName ? " AS " + this.escape(select.aliasName) : "")).join(", ");
|
||||
if ((this.expressionMap.limit || this.expressionMap.offset) && this.connection.driver instanceof OracleDriver) {
|
||||
return "SELECT ROWNUM " + this.escape("RN") + "," + selection + " FROM " + this.escape(tableName) + " " + this.escape(aliasName) + lock;
|
||||
}
|
||||
return "SELECT " + selection + " FROM " + this.escape(tableName) + " " + this.escape(aliasName) + lock;
|
||||
}
|
||||
|
||||
protected createJoinExpression(): string {
|
||||
|
||||
// examples:
|
||||
// select from owning side
|
||||
// qb.select("post")
|
||||
// .leftJoinAndSelect("post.category", "category");
|
||||
// select from non-owning side
|
||||
// qb.select("category")
|
||||
// .leftJoinAndSelect("category.post", "post");
|
||||
|
||||
const joins = this.expressionMap.joinAttributes.map(joinAttr => {
|
||||
|
||||
const relation = joinAttr.relation;
|
||||
const destinationTableName = joinAttr.tableName;
|
||||
const destinationTableAlias = joinAttr.alias.name;
|
||||
const appendedCondition = joinAttr.condition ? " AND (" + joinAttr.condition + ")" : "";
|
||||
const parentAlias = joinAttr.parentAlias;
|
||||
|
||||
// if join was build without relation (e.g. without "post.category") then it means that we have direct
|
||||
// table to join, without junction table involved. This means we simply join direct table.
|
||||
if (!parentAlias || !relation)
|
||||
return " " + joinAttr.direction + " JOIN " + this.escape(destinationTableName) + " " + this.escape(destinationTableAlias) +
|
||||
(joinAttr.condition ? " ON " + this.replacePropertyNames(joinAttr.condition) : "");
|
||||
|
||||
// if real entity relation is involved
|
||||
if (relation.isManyToOne || relation.isOneToOneOwner) {
|
||||
|
||||
// JOIN `category` `category` ON `category`.`id` = `post`.`categoryId`
|
||||
const condition = relation.joinColumns.map(joinColumn => {
|
||||
return destinationTableAlias + "." + joinColumn.referencedColumn!.propertyPath + "=" +
|
||||
parentAlias + "." + relation.propertyPath + "." + joinColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
return " " + joinAttr.direction + " JOIN " + this.escape(destinationTableName) + " " + this.escape(destinationTableAlias) + " ON " + this.replacePropertyNames(condition + appendedCondition);
|
||||
|
||||
} else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
|
||||
|
||||
// JOIN `post` `post` ON `post`.`categoryId` = `category`.`id`
|
||||
const condition = relation.inverseRelation!.joinColumns.map(joinColumn => {
|
||||
return destinationTableAlias + "." + relation.inverseRelation!.propertyPath + "." + joinColumn.referencedColumn!.propertyPath + "=" +
|
||||
parentAlias + "." + joinColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
return " " + joinAttr.direction + " JOIN " + this.escape(destinationTableName) + " " + this.escape(destinationTableAlias) + " ON " + this.replacePropertyNames(condition + appendedCondition);
|
||||
|
||||
} else { // means many-to-many
|
||||
const junctionTableName = relation.junctionEntityMetadata!.tableName;
|
||||
|
||||
const junctionAlias = joinAttr.junctionAlias;
|
||||
let junctionCondition = "", destinationCondition = "";
|
||||
|
||||
if (relation.isOwning) {
|
||||
|
||||
junctionCondition = relation.joinColumns.map(joinColumn => {
|
||||
// `post_category`.`postId` = `post`.`id`
|
||||
return junctionAlias + "." + joinColumn.propertyPath + "=" + parentAlias + "." + joinColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
destinationCondition = relation.inverseJoinColumns.map(joinColumn => {
|
||||
// `category`.`id` = `post_category`.`categoryId`
|
||||
return destinationTableAlias + "." + joinColumn.referencedColumn!.propertyPath + "=" + junctionAlias + "." + joinColumn.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
} else {
|
||||
junctionCondition = relation.inverseRelation!.inverseJoinColumns.map(joinColumn => {
|
||||
// `post_category`.`categoryId` = `category`.`id`
|
||||
return junctionAlias + "." + joinColumn.propertyPath + "=" + parentAlias + "." + joinColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
|
||||
destinationCondition = relation.inverseRelation!.joinColumns.map(joinColumn => {
|
||||
// `post`.`id` = `post_category`.`postId`
|
||||
return destinationTableAlias + "." + joinColumn.referencedColumn!.propertyPath + "=" + junctionAlias + "." + joinColumn.propertyPath;
|
||||
}).join(" AND ");
|
||||
}
|
||||
|
||||
return " " + joinAttr.direction + " JOIN " + this.escape(junctionTableName) + " " + this.escape(junctionAlias) + " ON " + this.replacePropertyNames(junctionCondition) +
|
||||
" " + joinAttr.direction + " JOIN " + this.escape(destinationTableName) + " " + this.escape(destinationTableAlias) + " ON " + this.replacePropertyNames(destinationCondition + appendedCondition);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.expressionMap.ignoreParentTablesJoins && this.expressionMap.mainAlias!.hasMetadata) {
|
||||
const metadata = this.expressionMap.mainAlias!.metadata;
|
||||
if (metadata.parentEntityMetadata && metadata.parentEntityMetadata.inheritanceType === "class-table" && metadata.parentIdColumns) {
|
||||
const alias = "parentIdColumn_" + metadata.parentEntityMetadata.tableName;
|
||||
const condition = metadata.parentIdColumns.map(parentIdColumn => {
|
||||
return this.expressionMap.mainAlias!.name + "." + parentIdColumn.propertyPath + " = " + alias + "." + parentIdColumn.referencedColumn!.propertyPath;
|
||||
}).join(" AND ");
|
||||
const join = " JOIN " + this.escape(metadata.parentEntityMetadata.tableName) + " " + this.escape(alias) + " ON " + this.replacePropertyNames(condition);
|
||||
joins.push(join);
|
||||
}
|
||||
}
|
||||
|
||||
return joins.join(" ");
|
||||
}
|
||||
|
||||
protected createGroupByExpression() {
|
||||
if (!this.expressionMap.groupBys || !this.expressionMap.groupBys.length) return "";
|
||||
return " GROUP BY " + this.replacePropertyNames(this.expressionMap.groupBys.join(", "));
|
||||
}
|
||||
|
||||
protected createOrderByCombinedWithSelectExpression(parentAlias: string) {
|
||||
|
||||
// if table has a default order then apply it
|
||||
let orderBys = this.expressionMap.orderBys;
|
||||
if (!Object.keys(orderBys).length && this.expressionMap.mainAlias!.hasMetadata) {
|
||||
orderBys = this.expressionMap.mainAlias!.metadata.orderBy || {};
|
||||
}
|
||||
|
||||
const selectString = Object.keys(orderBys)
|
||||
.map(columnName => {
|
||||
const [alias, column, ...embeddedProperties] = columnName.split(".");
|
||||
return this.escape(parentAlias) + "." + this.escape(alias + "_" + column + embeddedProperties.join("_"));
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
const orderByString = Object.keys(orderBys)
|
||||
.map(columnName => {
|
||||
const [alias, column, ...embeddedProperties] = columnName.split(".");
|
||||
return this.escape(parentAlias) + "." + this.escape(alias + "_" + column + embeddedProperties.join("_")) + " " + this.expressionMap.orderBys[columnName];
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
return [selectString, orderByString];
|
||||
}
|
||||
|
||||
protected createOrderByExpression() {
|
||||
|
||||
let orderBys = this.expressionMap.orderBys;
|
||||
|
||||
// if table has a default order then apply it
|
||||
if (!Object.keys(orderBys).length && this.expressionMap.mainAlias!.hasMetadata) {
|
||||
orderBys = this.expressionMap.mainAlias!.metadata.orderBy || {};
|
||||
}
|
||||
|
||||
// if user specified a custom order then apply it
|
||||
if (Object.keys(orderBys).length > 0)
|
||||
return " ORDER BY " + Object.keys(orderBys)
|
||||
.map(columnName => {
|
||||
return this.replacePropertyNames(columnName) + " " + this.expressionMap.orderBys[columnName];
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
protected createLimitOffsetOracleSpecificExpression(sql: string): string {
|
||||
if ((this.expressionMap.offset || this.expressionMap.limit) && this.connection.driver instanceof OracleDriver) {
|
||||
sql = "SELECT * FROM (" + sql + ") WHERE ";
|
||||
if (this.expressionMap.offset) {
|
||||
sql += this.escape("RN") + " >= " + this.expressionMap.offset;
|
||||
}
|
||||
if (this.expressionMap.limit) {
|
||||
sql += (this.expressionMap.offset ? " AND " : "") + this.escape("RN") + " <= " + ((this.expressionMap.offset || 0) + this.expressionMap.limit);
|
||||
}
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected createLimitOffsetExpression(): string {
|
||||
if (this.connection.driver instanceof OracleDriver)
|
||||
return "";
|
||||
|
||||
if (this.connection.driver instanceof SqlServerDriver) {
|
||||
|
||||
if (this.expressionMap.limit && this.expressionMap.offset)
|
||||
return " OFFSET " + this.expressionMap.offset + " ROWS FETCH NEXT " + this.expressionMap.limit + " ROWS ONLY";
|
||||
if (this.expressionMap.limit)
|
||||
return " OFFSET 0 ROWS FETCH NEXT " + this.expressionMap.limit + " ROWS ONLY";
|
||||
if (this.expressionMap.offset)
|
||||
return " OFFSET " + this.expressionMap.offset + " ROWS";
|
||||
|
||||
} else {
|
||||
if (this.expressionMap.limit && this.expressionMap.offset)
|
||||
return " LIMIT " + this.expressionMap.limit + " OFFSET " + this.expressionMap.offset;
|
||||
if (this.expressionMap.limit)
|
||||
return " LIMIT " + this.expressionMap.limit;
|
||||
if (this.expressionMap.offset)
|
||||
return " OFFSET " + this.expressionMap.offset;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds lock expression to a query.
|
||||
*/
|
||||
protected createLockExpression(): string {
|
||||
switch (this.expressionMap.lockMode) {
|
||||
case "pessimistic_read":
|
||||
if (this.connection.driver instanceof MysqlDriver) {
|
||||
return " LOCK IN SHARE MODE";
|
||||
|
||||
} else if (this.connection.driver instanceof PostgresDriver) {
|
||||
return " FOR SHARE";
|
||||
|
||||
} else if (this.connection.driver instanceof SqlServerDriver) {
|
||||
return "";
|
||||
|
||||
} else {
|
||||
throw new LockNotSupportedOnGivenDriverError();
|
||||
}
|
||||
case "pessimistic_write":
|
||||
if (this.connection.driver instanceof MysqlDriver || this.connection.driver instanceof PostgresDriver) {
|
||||
return " FOR UPDATE";
|
||||
|
||||
} else if (this.connection.driver instanceof SqlServerDriver) {
|
||||
return "";
|
||||
|
||||
} else {
|
||||
throw new LockNotSupportedOnGivenDriverError();
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
protected createHavingExpression() {
|
||||
if (!this.expressionMap.havings || !this.expressionMap.havings.length) return "";
|
||||
const conditions = this.expressionMap.havings.map((having, index) => {
|
||||
switch (having.type) {
|
||||
case "and":
|
||||
return (index > 0 ? "AND " : "") + this.replacePropertyNames(having.condition);
|
||||
case "or":
|
||||
return (index > 0 ? "OR " : "") + this.replacePropertyNames(having.condition);
|
||||
default:
|
||||
return this.replacePropertyNames(having.condition);
|
||||
}
|
||||
}).join(" ");
|
||||
|
||||
if (!conditions.length) return "";
|
||||
return " HAVING " + conditions;
|
||||
}
|
||||
|
||||
protected buildEscapedEntityColumnSelects(aliasName: string, metadata: EntityMetadata): SelectQuery[] {
|
||||
const hasMainAlias = this.expressionMap.selects.some(select => select.selection === aliasName);
|
||||
|
||||
const columns: ColumnMetadata[] = hasMainAlias ? metadata.columns : metadata.columns.filter(column => {
|
||||
return this.expressionMap.selects.some(select => select.selection === aliasName + "." + column.propertyName);
|
||||
});
|
||||
|
||||
return columns.map(column => {
|
||||
const selection = this.expressionMap.selects.find(select => select.selection === aliasName + "." + column.propertyName);
|
||||
return {
|
||||
selection: this.escape(aliasName) + "." + this.escape(column.databaseName),
|
||||
aliasName: selection && selection.aliasName ? selection.aliasName : aliasName + "_" + column.databaseName,
|
||||
// todo: need to keep in mind that custom selection.aliasName breaks hydrator. fix it later!
|
||||
};
|
||||
// return this.escapeAlias(aliasName) + "." + this.escapeColumn(column.fullName) +
|
||||
// " AS " + this.escapeAlias(aliasName + "_" + column.fullName);
|
||||
});
|
||||
}
|
||||
|
||||
protected findEntityColumnSelects(aliasName: string, metadata: EntityMetadata): SelectQuery[] {
|
||||
const mainSelect = this.expressionMap.selects.find(select => select.selection === aliasName);
|
||||
if (mainSelect)
|
||||
return [mainSelect];
|
||||
|
||||
return this.expressionMap.selects.filter(select => {
|
||||
return metadata.columns.some(column => select.selection === aliasName + "." + column.propertyName);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -78,12 +78,12 @@ export class UpdateQueryBuilder<Entity> extends QueryBuilder<Entity> {
|
||||
if (column) {
|
||||
const paramName = "_updated_" + column.databaseName;
|
||||
this.setParameter(paramName, valuesSet[column.propertyName]);
|
||||
updateColumnAndValues.push(this.escapeAlias(column.databaseName) + "=:" + paramName);
|
||||
updateColumnAndValues.push(this.escape(column.databaseName) + "=:" + paramName);
|
||||
}
|
||||
});
|
||||
|
||||
// get a table name and all column database names
|
||||
const tableName = this.escapeTable(this.getTableName());
|
||||
const tableName = this.escape(this.getTableName());
|
||||
|
||||
// generate and return sql update query
|
||||
return `UPDATE ${tableName} SET ${updateColumnAndValues.join(", ")}`; // todo: how do we replace aliases in where to nothing?
|
||||
|
||||
@ -50,7 +50,7 @@ export class RelationCountLoader {
|
||||
// SELECT category.post as parentId, COUNT(category.id) AS cnt FROM category category WHERE category.post IN (1, 2) GROUP BY category.post
|
||||
const qb = this.connection.createQueryBuilder(this.queryRunner);
|
||||
qb.select(inverseSideTableAlias + "." + inverseSidePropertyName, "parentId")
|
||||
.addSelect("COUNT(" + qb.escapeAlias(inverseSideTableAlias) + "." + qb.escapeColumn(referenceColumnName) + ")", "cnt")
|
||||
.addSelect("COUNT(" + qb.escape(inverseSideTableAlias) + "." + qb.escape(referenceColumnName) + ")", "cnt")
|
||||
.from(inverseSideTable, inverseSideTableAlias)
|
||||
.where(inverseSideTableAlias + "." + inverseSidePropertyName + " IN (:ids)")
|
||||
.addGroupBy(inverseSideTableAlias + "." + inverseSidePropertyName)
|
||||
@ -107,7 +107,7 @@ export class RelationCountLoader {
|
||||
|
||||
const qb = this.connection.createQueryBuilder(this.queryRunner);
|
||||
qb.select(junctionAlias + "." + firstJunctionColumn.propertyName, "parentId")
|
||||
.addSelect("COUNT(" + qb.escapeAlias(inverseSideTableAlias) + "." + qb.escapeColumn(inverseJoinColumnName) + ")", "cnt")
|
||||
.addSelect("COUNT(" + qb.escape(inverseSideTableAlias) + "." + qb.escape(inverseJoinColumnName) + ")", "cnt")
|
||||
.from(inverseSideTableName, inverseSideTableAlias)
|
||||
.innerJoin(junctionTableName, junctionAlias, condition)
|
||||
.addGroupBy(junctionAlias + "." + firstJunctionColumn.propertyName);
|
||||
|
||||
@ -45,8 +45,8 @@ export class TreeRepository<Entity> extends Repository<Entity> {
|
||||
createDescendantsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder<Entity> {
|
||||
|
||||
// create shortcuts for better readability
|
||||
const escapeAlias = (alias: string) => this.manager.connection.driver.escapeAlias(alias);
|
||||
const escapeColumn = (column: string) => this.manager.connection.driver.escapeColumn(column);
|
||||
const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
|
||||
const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
|
||||
|
||||
const joinCondition = `${escapeAlias(alias)}.${escapeColumn(this.metadata.primaryColumns[0].databaseName)}=${escapeAlias(closureTableAlias)}.${escapeColumn("descendant")}`;
|
||||
return this.createQueryBuilder(alias)
|
||||
@ -93,8 +93,8 @@ export class TreeRepository<Entity> extends Repository<Entity> {
|
||||
createAncestorsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder<Entity> {
|
||||
|
||||
// create shortcuts for better readability
|
||||
const escapeAlias = (alias: string) => this.manager.connection.driver.escapeAlias(alias);
|
||||
const escapeColumn = (column: string) => this.manager.connection.driver.escapeColumn(column);
|
||||
const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias);
|
||||
const escapeColumn = (column: string) => this.manager.connection.driver.escape(column);
|
||||
|
||||
const joinCondition = `${escapeAlias(alias)}.${escapeColumn(this.metadata.primaryColumns[0].databaseName)}=${escapeAlias(closureTableAlias)}.${escapeColumn("ancestor")}`;
|
||||
return this.createQueryBuilder(alias)
|
||||
|
||||
@ -24,7 +24,7 @@ describe("github issues > #512 Table name escaping in UPDATE in QueryBuilder", (
|
||||
})
|
||||
.getSql();
|
||||
|
||||
return query.should.contain(driver.escapeTable("Posts"));
|
||||
return query.should.contain(driver.escape("Posts"));
|
||||
})));
|
||||
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user