#1831 - Update query with maxRows does not include the limit in the generated SQL

This commit is contained in:
rob bygrave
2019-10-11 17:01:30 +13:00
parent 4e74c68603
commit b665d8375c
10 changed files with 280 additions and 191 deletions
@@ -75,6 +75,11 @@ public class DatabasePlatform {
protected boolean caseSensitiveCollation = true;
/**
* Set true if the Database support LIMIT clause on sql update.
*/
protected boolean inlineSqlUpdateLimit;
/**
* For limit/offset, row_number etc limiting of SQL queries.
*/
@@ -171,7 +176,7 @@ public class DatabasePlatform {
/**
* Characters escaped for startsWith, endsWith and contains.
*/
protected char[] likeSpecialCharacters = { '%', '_', '|' };
protected char[] likeSpecialCharacters = {'%', '_', '|'};
protected DbEncrypt dbEncrypt;
@@ -187,7 +192,7 @@ public class DatabasePlatform {
/**
* If set then use the CONCUR_UPDATABLE hint when creating ResultSets.
*
* <p>
* This is {@code false} for HANA
*/
protected boolean supportsResultSetConcurrencyModeUpdatable = true;
@@ -339,6 +344,13 @@ public class DatabasePlatform {
return supportsSavepointId;
}
/**
* Return true if the platform supports LIMIT with sql update.
*/
public boolean isInlineSqlUpdateLimit() {
return inlineSqlUpdateLimit;
}
/**
* Return the maximum table name length.
* <p>
@@ -370,11 +382,11 @@ public class DatabasePlatform {
/**
* Return a DB Sequence based IdGenerator.
*
* @param be the BackgroundExecutor that can be used to load the sequence if
* desired
* @param ds the DataSource
* @param stepSize the sequence allocation size as defined by mapping (defaults to 50)
* @param seqName the name of the sequence
* @param be the BackgroundExecutor that can be used to load the sequence if
* desired
* @param ds the DataSource
* @param stepSize the sequence allocation size as defined by mapping (defaults to 50)
* @param seqName the name of the sequence
*/
public PlatformIdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, int stepSize, String seqName) {
return null;
@@ -659,7 +671,7 @@ public class DatabasePlatform {
if (dbName != null && !dbName.isEmpty()) {
if (dbName.startsWith(openQuote)) {
// trim off the open and close quotes
return dbName.substring(1, dbName.length()-1);
return dbName.substring(1, dbName.length() - 1);
}
}
return dbName;
@@ -786,7 +798,7 @@ public class DatabasePlatform {
for (int i = 0; i < value.length(); i++) {
char ch = value.charAt(i);
boolean escaped = false;
for (char escapeChar: likeSpecialCharacters) {
for (char escapeChar : likeSpecialCharacters) {
if (ch == escapeChar) {
if (sb == null) {
sb = new StringBuilder(value.substring(0, i));
@@ -24,6 +24,7 @@ public class H2Platform extends DatabasePlatform {
this.historySupport = new H2HistorySupport();
this.nativeUuidType = true;
this.supportsDeleteTableAlias = true;
this.inlineSqlUpdateLimit = true;
this.dbDefaultValue.setNow("now()");
this.columnAliasPrefix = null;
this.exceptionTranslator =
@@ -28,6 +28,7 @@ public class MySqlPlatform extends DatabasePlatform {
this.useExtraTransactionOnIterateSecondaryQueries = true;
this.selectCountWithAlias = true;
this.supportsSavepointId = false;
this.inlineSqlUpdateLimit = true;
this.dbEncrypt = new MySqlDbEncrypt();
this.historySupport = new MySqlHistorySupport();
this.columnAliasPrefix = null;
@@ -458,7 +458,7 @@ public interface SpiQuery<T> extends Query<T>, TxnProfileEventCodes {
/**
* Set the query to be a delete query.
*/
void setDelete();
void setupForDeleteOrUpdate(boolean deleteRequest);
/**
* Set the query to be delete by ids due to cascading delete.
@@ -795,4 +795,11 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
dependentTables.addAll(tables);
}
}
/**
* Return true if no MaxRows or use LIMIT in SQL update.
*/
public boolean isInlineSqlUpdateLimit() {
return query.getMaxRows() < 1 || ebeanServer.getDatabasePlatform().isInlineSqlUpdateLimit();
}
}
@@ -47,24 +47,19 @@ class CQueryBuilder {
final String columnAliasPrefix;
private final SqlLimiter sqlLimiter;
private final CQueryBuilderRawSql rawSqlHandler;
private final Binder binder;
private final boolean selectCountWithAlias;
private final CQueryHistorySupport historySupport;
private final CQueryDraftSupport draftSupport;
private final DatabasePlatform dbPlatform;
/**
* Create the SqlGenSelect.
*/
CQueryBuilder(DatabasePlatform dbPlatform, Binder binder, CQueryHistorySupport historySupport, CQueryDraftSupport draftSupport) {
this.dbPlatform = dbPlatform;
this.binder = binder;
this.draftSupport = draftSupport;
@@ -102,7 +97,7 @@ class CQueryBuilder {
SpiQuery<T> query = request.getQuery();
String rootTableAlias = query.getAlias();
query.setDelete();
query.setupForDeleteOrUpdate(deleteRequest);
CQueryPredicates predicates = new CQueryPredicates(binder, request);
CQueryPlan queryPlan = request.getQueryPlan();
@@ -135,16 +130,16 @@ class CQueryBuilder {
if (sqlTree.noJoins()) {
if (dbPlatform.isSupportsDeleteTableAlias()) {
// delete from table <alias> ...
return aliasReplace(buildSql("delete", request, predicates, sqlTree).getSql(), alias);
return aliasReplace(buildSqlDelete("delete", request, predicates, sqlTree).getSql(), alias);
} else if (isMySql(dbPlatform.getPlatform())) {
return aliasReplace(buildSql("delete " + alias, request, predicates, sqlTree).getSql(), alias);
return aliasReplace(buildSqlDelete("delete " + alias, request, predicates, sqlTree).getSql(), alias);
} else {
// simple - delete from table ...
return aliasStrip(buildSql("delete", request, predicates, sqlTree).getSql());
return aliasStrip(buildSqlDelete("delete", request, predicates, sqlTree).getSql());
}
}
// wrap as - delete from table where id in (select id ...)
String sql = buildSql(null, request, predicates, sqlTree).getSql();
String sql = buildSqlDelete(null, request, predicates, sqlTree).getSql();
sql = request.getBeanDescriptor().getDeleteByIdInSql() + "in (" + sql + ")";
sql = aliasReplace(sql, alias);
return sql;
@@ -168,7 +163,7 @@ class CQueryBuilder {
sb.append(" set ").append(predicates.getDbUpdateClause());
String updateClause = sb.toString();
if (sqlTree.noJoins()) {
if (sqlTree.noJoins() && request.isInlineSqlUpdateLimit()) {
// simple - update table set ... where ...
return aliasStrip(buildSqlUpdate(updateClause, request, predicates, sqlTree).getSql());
}
@@ -547,137 +542,156 @@ class CQueryBuilder {
* Return the SQL response with row limiting (when not an update statement).
*/
private SqlLimitResponse buildSql(String selectClause, OrmQueryRequest<?> request, CQueryPredicates predicates, SqlTree select) {
return buildSql(selectClause, request, predicates, select, false);
}
/**
* Return the SQL response for update statement (stripping table alias for find by id expression).
*/
private SqlLimitResponse buildSqlUpdate(String selectClause, OrmQueryRequest<?> request, CQueryPredicates predicates, SqlTree select) {
return buildSql(selectClause, request, predicates, select, true);
}
private SqlLimitResponse buildSql(String selectClause, OrmQueryRequest<?> request, CQueryPredicates predicates, SqlTree select, boolean stripAlias) {
SpiQuery<?> query = request.getQuery();
if (query.isNativeSql()) {
return new SqlLimitResponse(query.getGeneratedSql(), false);
}
if (query.isRawSql()) {
return rawSqlHandler.buildSql(request, predicates, query.getRawSql().getSql());
}
return new BuildReq(selectClause, request, predicates, select).buildSql();
}
boolean distinct = query.isDistinct() || select.isSqlDistinct();
boolean useSqlLimiter = false;
StringBuilder sb = new StringBuilder(500);
String dbOrderBy = predicates.getDbOrderBy();
private SqlLimitResponse buildSqlDelete(String selectClause, OrmQueryRequest<?> request, CQueryPredicates predicates, SqlTree select) {
return new BuildReq(selectClause, request, predicates, select).buildSql();
}
if (selectClause != null) {
sb.append(selectClause);
private SqlLimitResponse buildSqlUpdate(String selectClause, OrmQueryRequest<?> request, CQueryPredicates predicates, SqlTree select) {
return new BuildReq(selectClause, request, predicates, select, true).buildSql();
}
} else {
useSqlLimiter = (query.hasMaxRowsOrFirstRow() && select.getManyProperty() == null);
private class BuildReq {
private final StringBuilder sb = new StringBuilder(500);
private final String selectClause;
private final OrmQueryRequest<?> request;
private final SpiQuery<?> query;
private final CQueryPredicates predicates;
private final SqlTree select;
private final boolean updateStatement;
if (!useSqlLimiter) {
sb.append("select ");
if (distinct) {
if (request.isInlineCountDistinct()) {
sb.append("count(");
}
sb.append("distinct ");
String distinctOn = select.getDistinctOn();
if (distinctOn != null) {
sb.append("on (");
sb.append(distinctOn).append(") ");
private final boolean distinct;
private final String dbOrderBy;
private boolean useSqlLimiter;
private boolean hasWhere;
private BuildReq(String selectClause, OrmQueryRequest<?> request, CQueryPredicates predicates, SqlTree select) {
this(selectClause, request, predicates, select, false);
}
private BuildReq(String selectClause, OrmQueryRequest<?> request, CQueryPredicates predicates, SqlTree select, boolean updateStatement) {
this.selectClause = selectClause;
this.request = request;
this.query = request.getQuery();
this.predicates = predicates;
this.select = select;
this.updateStatement = updateStatement;
this.distinct = query.isDistinct() || select.isSqlDistinct();
this.dbOrderBy = predicates.getDbOrderBy();
}
private void appendSelect() {
if (selectClause != null) {
sb.append(selectClause);
} else {
useSqlLimiter = (query.hasMaxRowsOrFirstRow() && select.getManyProperty() == null);
if (!useSqlLimiter) {
appendSelectDistinct();
}
if (query.isCountDistinct() && query.isSingleAttribute()) {
sb.append("r1.attribute_, count(*) from (select ").append(select.getSelectSql()).append(" as attribute_");
} else {
sb.append(select.getSelectSql());
}
if (request.isInlineCountDistinct()) {
sb.append(")");
}
if (distinct && dbOrderBy != null && !query.isSingleAttribute()) {
// add the orderBy columns to the select clause (due to distinct)
final OrderBy<?> orderBy = query.getOrderBy();
if (orderBy != null && orderBy.supportsSelect()) {
sb.append(", ").append(DbOrderByTrim.trim(dbOrderBy));
}
}
}
if (query.isCountDistinct() && query.isSingleAttribute()) {
sb.append("r1.attribute_, count(*) from (select ");
sb.append(select.getSelectSql());
sb.append(" as attribute_");
} else {
sb.append(select.getSelectSql());
}
if (request.isInlineCountDistinct()) {
sb.append(")");
}
if (distinct && dbOrderBy != null && !query.isSingleAttribute()) {
// add the orderBy columns to the select clause (due to distinct)
final OrderBy<?> orderBy = query.getOrderBy();
if (orderBy != null && orderBy.supportsSelect()) {
sb.append(", ").append(DbOrderByTrim.trim(dbOrderBy));
}
private void appendSelectDistinct() {
sb.append("select ");
if (distinct) {
if (request.isInlineCountDistinct()) {
sb.append("count(");
}
sb.append("distinct ");
String distinctOn = select.getDistinctOn();
if (distinctOn != null) {
sb.append("on (").append(distinctOn).append(") ");
}
}
}
if (selectClause == null || !selectClause.startsWith("update")) {
sb.append(" from ");
sb.append(select.getFromSql());
}
String inheritanceWhere = select.getInheritanceWhereSql();
boolean hasWhere = false;
if (!inheritanceWhere.isEmpty()) {
sb.append(" where");
sb.append(inheritanceWhere);
hasWhere = true;
}
if (query.isAsOfBaseTable() && !historySupport.isStandardsBased()) {
hasWhere = appendWhere(hasWhere, sb);
sb.append(historySupport.getAsOfPredicate(request.getBaseTableAlias()));
}
if (request.isFindById() || query.getId() != null) {
appendWhere(hasWhere, sb);
BeanDescriptor<?> desc = request.getBeanDescriptor();
String idSql = desc.getIdBinderIdSql(query.getAlias());
if (idSql.isEmpty()) {
throw new IllegalStateException("Executing FindById query on entity bean " + desc.getName()
+ " that doesn't have an @Id property??");
private void appendFrom() {
if (selectClause == null || !selectClause.startsWith("update")) {
sb.append(" from ");
sb.append(select.getFromSql());
}
if (stripAlias) {
// strip the table alias for use in update statement
idSql = StringHelper.replaceString(idSql, "t0.", "");
}
sb.append(idSql).append(" ");
hasWhere = true;
}
String dbWhere = predicates.getDbWhere();
if (hasValue(dbWhere)) {
if (!hasWhere) {
hasWhere = true;
sb.append(" where ");
} else {
private void appendAndOrWhere() {
if (hasWhere) {
sb.append(" and ");
}
sb.append(dbWhere);
}
String dbFilterMany = predicates.getDbFilterMany();
if (hasValue(dbFilterMany)) {
if (!hasWhere) {
hasWhere = true;
sb.append(" where ");
} else {
sb.append("and ");
sb.append(" where ");
hasWhere = true;
}
sb.append(dbFilterMany);
}
if (!query.isIncludeSoftDeletes()) {
private void appendInheritanceWhere() {
String inheritanceWhere = select.getInheritanceWhereSql();
if (!inheritanceWhere.isEmpty()) {
sb.append(" where");
sb.append(inheritanceWhere);
hasWhere = true;
}
}
private void appendHistoryAsOfPredicate() {
if (query.isAsOfBaseTable() && !historySupport.isStandardsBased()) {
appendAndOrWhere();
sb.append(historySupport.getAsOfPredicate(request.getBaseTableAlias()));
}
}
private void appendFindId() {
if (request.isFindById() || query.getId() != null) {
appendAndOrWhere();
BeanDescriptor<?> desc = request.getBeanDescriptor();
String idSql = desc.getIdBinderIdSql(query.getAlias());
if (idSql.isEmpty()) {
throw new IllegalStateException("Executing FindById query on entity bean " + desc.getName()
+ " that doesn't have an @Id property??");
}
if (updateStatement) {
// strip the table alias for use in update statement
idSql = StringHelper.replaceString(idSql, "t0.", "");
}
sb.append(idSql).append(" ");
hasWhere = true;
}
}
private void appendToWhere(String predicate) {
if (hasValue(predicate)) {
appendAndOrWhere();
sb.append(predicate);
}
}
private void appendSoftDelete() {
List<String> softDeletePredicates = query.getSoftDeletePredicates();
if (softDeletePredicates != null) {
if (!hasWhere) {
sb.append(" where ");
} else {
sb.append(" and ");
}
appendAndOrWhere();
for (int i = 0; i < softDeletePredicates.size(); i++) {
if (i > 0) {
sb.append(" and ");
@@ -687,69 +701,75 @@ class CQueryBuilder {
}
}
String groupBy = select.getGroupBy();
if (groupBy != null) {
sb.append(" group by ").append(groupBy);
private SqlLimitResponse buildSql() {
appendSelect();
appendFrom();
appendInheritanceWhere();
appendHistoryAsOfPredicate();
appendFindId();
appendToWhere(predicates.getDbWhere());
appendToWhere(predicates.getDbFilterMany());
if (!query.isIncludeSoftDeletes()) {
appendSoftDelete();
}
String groupBy = select.getGroupBy();
if (groupBy != null) {
sb.append(" group by ").append(groupBy);
}
String dbHaving = predicates.getDbHaving();
if (hasValue(dbHaving)) {
sb.append(" having ").append(dbHaving);
}
if (dbOrderBy != null && !query.isCountDistinct()) {
sb.append(" order by ").append(dbOrderBy);
}
if (query.isCountDistinct() && query.isSingleAttribute()) {
sb.append(") r1 group by r1.attribute_");
sb.append(toSql(query.getCountDistinctOrder()));
}
if (useSqlLimiter) {
// use LIMIT/OFFSET, ROW_NUMBER() or rownum type SQL query limitation
SqlLimitRequest r = new OrmQueryLimitRequest(sb.toString(), dbOrderBy, query, dbPlatform, distinct);
return sqlLimiter.limit(r);
} else {
if (updateStatement) {
final int maxRows = query.getMaxRows();
if (maxRows > 0) {
// limit on update statement only support on platforms with supportsMaxRowsOnUpdate
sb.append(" limit ").append(maxRows);
}
}
return new SqlLimitResponse(dbPlatform.completeSql(sb.toString(), query), false);
}
}
String dbHaving = predicates.getDbHaving();
if (hasValue(dbHaving)) {
sb.append(" having ").append(dbHaving);
private String toSql(CountDistinctOrder orderBy) {
switch (orderBy) {
case ATTR_ASC:
return " order by r1.attribute_";
case ATTR_DESC:
return " order by r1.attribute_ desc";
case COUNT_ASC_ATTR_ASC:
return " order by count(*), r1.attribute_";
case COUNT_ASC_ATTR_DESC:
return " order by count(*), r1.attribute_ desc";
case COUNT_DESC_ATTR_ASC:
return " order by count(*) desc, r1.attribute_";
case COUNT_DESC_ATTR_DESC:
return " order by count(*) desc, r1.attribute_ desc";
default:
throw new IllegalArgumentException("Illegal enum: " + orderBy);
}
}
if (dbOrderBy != null && !query.isCountDistinct()) {
sb.append(" order by ").append(dbOrderBy);
private boolean hasValue(String s) {
return s != null && !s.isEmpty();
}
if (query.isCountDistinct() && query.isSingleAttribute()) {
sb.append(") r1 group by r1.attribute_");
sb.append(toSql(query.getCountDistinctOrder()));
}
if (useSqlLimiter) {
// use LIMIT/OFFSET, ROW_NUMBER() or rownum type SQL query limitation
SqlLimitRequest r = new OrmQueryLimitRequest(sb.toString(), dbOrderBy, query, dbPlatform, distinct);
return sqlLimiter.limit(r);
} else {
return new SqlLimitResponse(dbPlatform.completeSql(sb.toString(), query), false);
}
}
private String toSql(CountDistinctOrder orderBy) {
switch (orderBy) {
case ATTR_ASC:
return " order by r1.attribute_";
case ATTR_DESC:
return " order by r1.attribute_ desc";
case COUNT_ASC_ATTR_ASC:
return " order by count(*), r1.attribute_";
case COUNT_ASC_ATTR_DESC:
return " order by count(*), r1.attribute_ desc";
case COUNT_DESC_ATTR_ASC:
return " order by count(*) desc, r1.attribute_";
case COUNT_DESC_ATTR_DESC:
return " order by count(*) desc, r1.attribute_ desc";
default:
throw new IllegalArgumentException("Illegal enum: " + orderBy);
}
}
/**
* Append where or and based on the hasWhere flag.
*/
private boolean appendWhere(boolean hasWhere, StringBuilder sb) {
if (hasWhere) {
sb.append(" and ");
} else {
sb.append(" where ");
}
return true;
}
private boolean hasValue(String s) {
return s != null && !s.isEmpty();
}
boolean isPlatformDistinctOn() {
@@ -759,7 +779,7 @@ class CQueryBuilder {
/**
* Return the 'for update' FROM hint (sql server).
*/
public String fromForUpdate(SpiQuery<?> query) {
String fromForUpdate(SpiQuery<?> query) {
Query.ForUpdate mode = query.getForUpdateMode();
if (mode == null) {
return null;
@@ -692,13 +692,15 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
}
/**
* Setup to be a delete query.
* Setup to be a delete or update query.
*/
@Override
public void setDelete() {
public void setupForDeleteOrUpdate(boolean deleteRequest) {
// unset any paging and select on the id in the case where the query
// includes joins and we use - delete ... where id in (...)
maxRows = 0;
if (deleteRequest) {
maxRows = 0;
}
firstRow = 0;
forUpdate = null;
rootTableAlias = "${RTA}"; // alias we remove later