diff --git a/src/driver/Driver.ts b/src/driver/Driver.ts index 079b19819..b660a1490 100644 --- a/src/driver/Driver.ts +++ b/src/driver/Driver.ts @@ -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. diff --git a/src/driver/mongodb/MongoDriver.ts b/src/driver/mongodb/MongoDriver.ts index eee221d83..3d8c56915 100644 --- a/src/driver/mongodb/MongoDriver.ts +++ b/src/driver/mongodb/MongoDriver.ts @@ -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. */ diff --git a/src/driver/mysql/MysqlDriver.ts b/src/driver/mysql/MysqlDriver.ts index 9efcfe737..8b08d08c4 100644 --- a/src/driver/mysql/MysqlDriver.ts +++ b/src/driver/mysql/MysqlDriver.ts @@ -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. */ diff --git a/src/driver/oracle/OracleDriver.ts b/src/driver/oracle/OracleDriver.ts index 2c9bc5a56..b4b7bcf63 100644 --- a/src/driver/oracle/OracleDriver.ts +++ b/src/driver/oracle/OracleDriver.ts @@ -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. */ diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 06f954a47..706764b1c 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -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. */ diff --git a/src/driver/sqlite/SqliteDriver.ts b/src/driver/sqlite/SqliteDriver.ts index ec4e38db9..cde89c633 100644 --- a/src/driver/sqlite/SqliteDriver.ts +++ b/src/driver/sqlite/SqliteDriver.ts @@ -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. */ diff --git a/src/driver/sqlserver/SqlServerDriver.ts b/src/driver/sqlserver/SqlServerDriver.ts index 1110519ad..c44c366c4 100644 --- a/src/driver/sqlserver/SqlServerDriver.ts +++ b/src/driver/sqlserver/SqlServerDriver.ts @@ -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. */ diff --git a/src/driver/websql/WebsqlDriver.ts b/src/driver/websql/WebsqlDriver.ts index f6364e36d..59a4fb554 100644 --- a/src/driver/websql/WebsqlDriver.ts +++ b/src/driver/websql/WebsqlDriver.ts @@ -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. */ diff --git a/src/persistence/SubjectBuilder.ts b/src/persistence/SubjectBuilder.ts index 44c95ecb8..28c9f2d95 100644 --- a/src/persistence/SubjectBuilder.ts +++ b/src/persistence/SubjectBuilder.ts @@ -551,8 +551,7 @@ export class SubjectBuilder { 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 { // 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 { // 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}`; diff --git a/src/query-builder/DeleteQueryBuilder.ts b/src/query-builder/DeleteQueryBuilder.ts index eb5307418..3383e24da 100644 --- a/src/query-builder/DeleteQueryBuilder.ts +++ b/src/query-builder/DeleteQueryBuilder.ts @@ -71,7 +71,7 @@ export class DeleteQueryBuilder extends QueryBuilder { * 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? } diff --git a/src/query-builder/InsertQueryBuilder.ts b/src/query-builder/InsertQueryBuilder.ts index d057d130d..03ef16624 100644 --- a/src/query-builder/InsertQueryBuilder.ts +++ b/src/query-builder/InsertQueryBuilder.ts @@ -76,8 +76,8 @@ export class InsertQueryBuilder extends QueryBuilder { }).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}`; diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index 8cff99e8c..cb821ad62 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -283,6 +283,14 @@ export abstract class QueryBuilder { 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 { 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 { } /** - * 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 { 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 { 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 { 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 { 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!; - } - } diff --git a/src/query-builder/SelectQueryBuilder.ts b/src/query-builder/SelectQueryBuilder.ts index 469962afa..f7a89b3f3 100644 --- a/src/query-builder/SelectQueryBuilder.ts +++ b/src/query-builder/SelectQueryBuilder.ts @@ -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 extends QueryBuilder { 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 extends QueryBuilder { 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 extends QueryBuilder { } } + 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); + }); + } + } diff --git a/src/query-builder/UpdateQueryBuilder.ts b/src/query-builder/UpdateQueryBuilder.ts index 1632db9ba..06b8e567c 100644 --- a/src/query-builder/UpdateQueryBuilder.ts +++ b/src/query-builder/UpdateQueryBuilder.ts @@ -78,12 +78,12 @@ export class UpdateQueryBuilder extends QueryBuilder { 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? diff --git a/src/query-builder/relation-count/RelationCountLoader.ts b/src/query-builder/relation-count/RelationCountLoader.ts index 959263b9c..4b75cdc00 100644 --- a/src/query-builder/relation-count/RelationCountLoader.ts +++ b/src/query-builder/relation-count/RelationCountLoader.ts @@ -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); diff --git a/src/repository/TreeRepository.ts b/src/repository/TreeRepository.ts index aca7fc96d..c4ac0da79 100644 --- a/src/repository/TreeRepository.ts +++ b/src/repository/TreeRepository.ts @@ -45,8 +45,8 @@ export class TreeRepository extends Repository { createDescendantsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder { // 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 extends Repository { createAncestorsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder { // 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) diff --git a/test/github-issues/512/issue-512.ts b/test/github-issues/512/issue-512.ts index 4d6e3d70b..9000fce9f 100644 --- a/test/github-issues/512/issue-512.ts +++ b/test/github-issues/512/issue-512.ts @@ -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")); }))); });