package io.ebeaninternal.server.query; import io.ebean.CountDistinctOrder; import io.ebean.OrderBy; import io.ebean.Query; import io.ebean.RawSql; import io.ebean.RawSqlBuilder; import io.ebean.annotation.Platform; import io.ebean.config.dbplatform.DatabasePlatform; import io.ebean.config.dbplatform.SqlLimitRequest; import io.ebean.config.dbplatform.SqlLimitResponse; import io.ebean.config.dbplatform.SqlLimiter; import io.ebean.event.readaudit.ReadAuditQueryPlan; import io.ebean.text.PathProperties; import io.ebean.util.SplitName; import io.ebean.util.StringHelper; import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; import io.ebeaninternal.server.el.ElPropertyValue; import io.ebeaninternal.server.persist.Binder; import io.ebeaninternal.server.querydefn.OrmQueryDetail; import io.ebeaninternal.server.querydefn.OrmQueryLimitRequest; import io.ebeaninternal.server.rawsql.SpiRawSql; import io.ebeaninternal.server.rawsql.SpiRawSql.ColumnMapping; import io.ebeaninternal.server.rawsql.SpiRawSql.ColumnMapping.Column; import javax.persistence.PersistenceException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Generates the SQL SELECT statements taking into account the physical * deployment properties. */ class CQueryBuilder { private 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; private final boolean selectCountWithColumnAlias; /** * Create the SqlGenSelect. */ CQueryBuilder(DatabasePlatform dbPlatform, Binder binder, CQueryHistorySupport historySupport, CQueryDraftSupport draftSupport) { this.dbPlatform = dbPlatform; this.binder = binder; this.draftSupport = draftSupport; this.historySupport = historySupport; this.columnAliasPrefix = dbPlatform.getColumnAliasPrefix(); this.sqlLimiter = dbPlatform.getSqlLimiter(); this.rawSqlHandler = new CQueryBuilderRawSql(sqlLimiter, dbPlatform); this.selectCountWithAlias = dbPlatform.isSelectCountWithAlias(); this.selectCountWithColumnAlias = dbPlatform.isSelectCountWithColumnAlias(); } /** * split the order by claus on the field delimiter and prefix each field with * the relation name */ static String prefixOrderByFields(String name, String orderBy) { StringBuilder sb = new StringBuilder(); for (String token : orderBy.split(",")) { if (sb.length() > 0) { sb.append(", "); } sb.append(name); sb.append("."); sb.append(token.trim()); } return sb.toString(); } /** * Build the delete query. */ CQueryUpdate buildUpdateQuery(boolean deleteRequest, OrmQueryRequest request) { SpiQuery query = request.getQuery(); String rootTableAlias = query.getAlias(); query.setupForDeleteOrUpdate(deleteRequest); CQueryPredicates predicates = new CQueryPredicates(binder, request); CQueryPlan queryPlan = request.getQueryPlan(); if (queryPlan != null) { // skip building the SqlTree and Sql string predicates.prepare(false); return new CQueryUpdate(request, predicates, queryPlan); } predicates.prepare(true); SqlTree sqlTree = createSqlTree(request, predicates); String sql; if (deleteRequest) { sql = buildDeleteSql(request, rootTableAlias, predicates, sqlTree); } else { sql = buildUpdateSql(request, rootTableAlias, predicates, sqlTree); } // cache the query plan queryPlan = new CQueryPlan(request, sql, sqlTree, false, predicates.getLogWhereSql()); request.putQueryPlan(queryPlan); return new CQueryUpdate(request, predicates, queryPlan); } private String buildDeleteSql(OrmQueryRequest request, String rootTableAlias, CQueryPredicates predicates, SqlTree sqlTree) { String alias = alias(rootTableAlias); if (sqlTree.noJoins()) { if (dbPlatform.isSupportsDeleteTableAlias()) { // delete from table ... return aliasReplace(buildSqlDelete("delete", request, predicates, sqlTree).getSql(), alias); } else if (isMySql(dbPlatform.getPlatform())) { return aliasReplace(buildSqlDelete("delete " + alias, request, predicates, sqlTree).getSql(), alias); } else { // simple - delete from table ... return aliasStrip(buildSqlDelete("delete", request, predicates, sqlTree).getSql()); } } // wrap as - delete from table where id in (select id ...) String sql = buildSqlDelete(null, request, predicates, sqlTree).getSql(); sql = request.getBeanDescriptor().getDeleteByIdInSql() + "in (" + sql + ")"; sql = aliasReplace(sql, alias); return sql; } private boolean isMySql(Platform platform) { return platform.base() == Platform.MYSQL; } private String alias(String rootTableAlias) { return (rootTableAlias == null) ? "t0" : rootTableAlias; } private String buildUpdateSql(OrmQueryRequest request, String rootTableAlias, CQueryPredicates predicates, SqlTree sqlTree) { StringBuilder sb = new StringBuilder(200); sb.append("update ").append(request.getBeanDescriptor().getBaseTable()); if (rootTableAlias != null) { sb.append(" ").append(rootTableAlias); } sb.append(" set ").append(predicates.getDbUpdateClause()); String updateClause = sb.toString(); if (sqlTree.noJoins() && request.isInlineSqlUpdateLimit()) { // simple - update table set ... where ... return aliasStrip(buildSqlUpdate(updateClause, request, predicates, sqlTree).getSql()); } // wrap as - update table set ... where id in (select id ...) String sql = buildSqlUpdate(null, request, predicates, sqlTree).getSql(); sql = updateClause + " " + request.getBeanDescriptor().getWhereIdInSql() + "in (" + sql + ")"; sql = aliasReplace(sql, alias(rootTableAlias)); return sql; } /** * Strip the root table alias. */ private String aliasStrip(String sql) { sql = StringHelper.replaceString(sql, "${RTA}.", ""); return StringHelper.replaceString(sql, " ${RTA}", ""); } /** * Replace the root table alias. */ private String aliasReplace(String sql, String replaceWith) { sql = StringHelper.replaceString(sql, "${RTA}.", replaceWith + "."); return StringHelper.replaceString(sql, "${RTA}", replaceWith); } CQueryFetchSingleAttribute buildFetchAttributeQuery(OrmQueryRequest request) { SpiQuery query = request.getQuery(); query.setSingleAttribute(); CQueryPredicates predicates = new CQueryPredicates(binder, request); CQueryPlan queryPlan = request.getQueryPlan(); if (queryPlan != null) { predicates.prepare(false); return new CQueryFetchSingleAttribute(request, predicates, queryPlan, query.isCountDistinct()); } // use RawSql or generated Sql predicates.prepare(true); SqlTree sqlTree = createSqlTree(request, predicates); SqlLimitResponse s = buildSql(null, request, predicates, sqlTree); queryPlan = new CQueryPlan(request, s.getSql(), sqlTree, s.isIncludesRowNumberColumn(), predicates.getLogWhereSql()); request.putQueryPlan(queryPlan); return new CQueryFetchSingleAttribute(request, predicates, queryPlan, query.isCountDistinct()); } /** * Build the find ids query. */ CQueryFetchSingleAttribute buildFetchIdsQuery(OrmQueryRequest request) { SpiQuery query = request.getQuery(); query.setSelectId(); BeanDescriptor desc = request.getBeanDescriptor(); if (!query.isIncludeSoftDeletes() && desc.isSoftDelete()) { query.addSoftDeletePredicate(desc.getSoftDeletePredicate(alias(query.getAlias()))); } return buildFetchAttributeQuery(request); } /** * Return the history support if this query needs it (is a 'as of' type query). */ CQueryHistorySupport getHistorySupport(SpiQuery query) { return query.getTemporalMode().isHistory() ? historySupport : null; } /** * Return the draft support (or null) for a 'asDraft' query. */ CQueryDraftSupport getDraftSupport(SpiQuery query) { return query.getTemporalMode() == SpiQuery.TemporalMode.DRAFT ? draftSupport : null; } /** * Build the row count query. */ CQueryRowCount buildRowCountQuery(OrmQueryRequest request) { SpiQuery query = request.getQuery(); // always set the order by to null for row count query query.setOrder(null); query.setFirstRow(0); query.setMaxRows(0); boolean countDistinct = query.isDistinct(); boolean withAgg = false; if (!countDistinct) { withAgg = includesAggregation(request, query); if (!withAgg) { // minimise select clause for standard count query.setSelectId(); } } CQueryPredicates predicates = new CQueryPredicates(binder, request); CQueryPlan queryPlan = request.getQueryPlan(); if (queryPlan != null) { // skip building the SqlTree and Sql string predicates.prepare(false); return new CQueryRowCount(queryPlan, request, predicates); } predicates.prepare(true); SqlTree sqlTree = createSqlTree(request, predicates, selectCountWithColumnAlias && withAgg); if (SpiQuery.TemporalMode.CURRENT == query.getTemporalMode()) { sqlTree.addSoftDeletePredicate(query); } boolean wrap = sqlTree.hasMany() || withAgg; String sqlSelect = null; if (countDistinct) { if (sqlTree.isSingleProperty()) { request.setInlineCountDistinct(); } } else if (!wrap) { sqlSelect = "select count(*)"; } SqlLimitResponse s = buildSql(sqlSelect, request, predicates, sqlTree); String sql = s.getSql(); if (!request.isInlineCountDistinct()) { if (countDistinct) { sql = wrapSelectCount(sql); } else if (wrap || query.isRawSql()) { // remove order by - mssql does not accept order by in subqueries int pos = sql.lastIndexOf(" order by "); if (pos != -1) { sql = sql.substring(0, pos); } sql = wrapSelectCount(sql); } } // cache the query plan queryPlan = new CQueryPlan(request, sql, sqlTree, s.isIncludesRowNumberColumn(), predicates.getLogWhereSql()); request.putQueryPlan(queryPlan); return new CQueryRowCount(queryPlan, request, predicates); } /** * Return true if the query includes an aggregation property. */ private boolean includesAggregation(OrmQueryRequest request, SpiQuery query) { return request.getBeanDescriptor().includesAggregation(query.getDetail()); } private String wrapSelectCount(String sql) { sql = "select count(*) from ( " + sql + ")"; if (selectCountWithAlias) { sql += " as c"; } return sql; } /** * Return the SQL Select statement as a String. Converts logical property * names to physical deployment column names. */ CQuery buildQuery(OrmQueryRequest request) { CQueryPredicates predicates = new CQueryPredicates(binder, request); CQueryPlan queryPlan = request.getQueryPlan(); if (queryPlan != null) { // Reuse the query plan so skip generating SqlTree and SQL. // We do prepare and bind the new parameters predicates.prepare(false); return new CQuery<>(request, predicates, queryPlan); } // RawSql or Generated Sql query // Prepare the where, having and order by clauses. // This also parses them from logical property names to // database columns and determines 'includes'. // We need to check these 'includes' for extra joins // that are not included via select predicates.prepare(true); // Build the tree structure that represents the query. SpiQuery query = request.getQuery(); SqlTree sqlTree = createSqlTree(request, predicates); if (query.isAsOfQuery()) { sqlTree.addAsOfTableAlias(query); } else if (SpiQuery.TemporalMode.CURRENT == query.getTemporalMode()) { sqlTree.addSoftDeletePredicate(query); } SqlLimitResponse res = buildSql(null, request, predicates, sqlTree); boolean rawSql = request.isRawSql(); if (rawSql) { queryPlan = new CQueryPlanRawSql(request, res, sqlTree, predicates.getLogWhereSql()); } else { queryPlan = new CQueryPlan(request, res, sqlTree, false, predicates.getLogWhereSql()); } BeanDescriptor desc = request.getBeanDescriptor(); if (desc.isReadAuditing()) { // log the query plan based bean type (i.e. ignoring query disabling for logging the sql/plan) desc.getReadAuditLogger().queryPlan(new ReadAuditQueryPlan(desc.getFullName(), queryPlan.getAuditQueryKey(), queryPlan.getSql())); } // cache the query plan because we can reuse it and also // gather query performance statistics based on it. request.putQueryPlan(queryPlan); return new CQuery<>(request, predicates, queryPlan); } /** * Build the SqlTree. *

* The SqlTree is immutable after construction and so is safe to use by * concurrent threads. *

*

* The predicates is used to add additional joins that come from the where or * order by clauses that are not already included for the select clause. *

*/ private SqlTree createSqlTree(OrmQueryRequest request, CQueryPredicates predicates) { return createSqlTree(request, predicates, false); } private SqlTree createSqlTree(OrmQueryRequest request, CQueryPredicates predicates, boolean forceColumnAlias) { if (request.isNativeSql()) { return createNativeSqlTree(request, predicates); } if (request.isRawSql()) { return createRawSqlSqlTree(request, predicates); } String colAliasPrefix = forceColumnAlias ? "c" : columnAliasPrefix; return new SqlTreeBuilder(colAliasPrefix, this, request, predicates).build(); } private String nativeQueryPaging(SpiQuery query, String sql) { return dbPlatform.getBasicSqlLimiter().limit(sql, query.getFirstRow(), query.getMaxRows()); } /** * Create the SqlTree by reading the ResultSetMetaData and mapping table/columns to bean property paths. */ private SqlTree createNativeSqlTree(OrmQueryRequest request, CQueryPredicates predicates) { SpiQuery query = request.getQuery(); // parse named parameters returning the final sql to execute String sql = predicates.parseBindParams(query.getNativeSql()); if (query.hasMaxRowsOrFirstRow()) { sql = nativeQueryPaging(query, sql); } query.setGeneratedSql(sql); Connection connection = request.getTransaction().getConnection(); BeanDescriptor desc = request.getBeanDescriptor(); try { // For SqlServer we need either "selectMethod=cursor" in the connection string or fetch explicitly a cursorable // statement here by specifying ResultSet.CONCUR_UPDATABLE PreparedStatement statement = connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, dbPlatform.isSupportsResultSetConcurrencyModeUpdatable() ? ResultSet.CONCUR_UPDATABLE : ResultSet.CONCUR_READ_ONLY); predicates.bind(statement, connection); ResultSet resultSet = statement.executeQuery(); ResultSetMetaData metaData = resultSet.getMetaData(); int cols = 1 + metaData.getColumnCount(); List propertyNames = new ArrayList<>(cols - 1); for (int i = 1; i < cols; i++) { String tableName = metaData.getTableName(i).toLowerCase(); String columnName = metaData.getColumnName(i).toLowerCase(); String path = desc.findBeanPath(tableName, columnName); if (path != null) { propertyNames.add(path); } else { propertyNames.add(SpiRawSql.IGNORE_COLUMN); } } RawSql rawSql = RawSqlBuilder.resultSet(resultSet, propertyNames.toArray(new String[0])); query.setRawSql(rawSql); return createRawSqlSqlTree(request, predicates); } catch (SQLException e) { throw new RuntimeException(e); } } private SqlTree createRawSqlSqlTree(OrmQueryRequest request, CQueryPredicates predicates) { BeanDescriptor descriptor = request.getBeanDescriptor(); ColumnMapping columnMapping = request.getQuery().getRawSql().getColumnMapping(); PathProperties pathProps = new PathProperties(); // convert list of columns into (tree like) PathProperties Iterator it = columnMapping.getColumns(); while (it.hasNext()) { SpiRawSql.ColumnMapping.Column column = it.next(); String propertyName = column.getPropertyName(); if (!SpiRawSql.IGNORE_COLUMN.equals(propertyName)) { ElPropertyValue el = descriptor.getElGetValue(propertyName); if (el == null && propertyName.endsWith("Id")) { // try default naming convention for foreign key columns String foreignIdPath = assocOneIdPath(propertyName); el = descriptor.getElGetValue(foreignIdPath); if (el != null) { propertyName = foreignIdPath; } } if (el == null) { throw new PersistenceException("Property [" + propertyName + "] not found on " + descriptor.getFullName()); } addRawColumnMapping(pathProps, column, propertyName, el); } } OrmQueryDetail detail = new OrmQueryDetail(); // transfer PathProperties into OrmQueryDetail for (PathProperties.Props props : pathProps.getPathProps()) { detail.fetch(props.getPath(), props.getProperties()); } // check if @Id property included in RawSql boolean rawNoId = true; BeanProperty idProperty = descriptor.getIdProperty(); if (idProperty != null && columnMapping.contains(idProperty.getName())) { // contains the @Id property for the root level bean rawNoId = false; } // build SqlTree based on OrmQueryDetail of the RawSql return new SqlTreeBuilder(request, predicates, detail, rawNoId).build(); } private void addRawColumnMapping(PathProperties pathProps, Column column, String propertyName, ElPropertyValue el) { BeanProperty beanProperty = el.getBeanProperty(); if (beanProperty.isId()) { if (propertyName.contains(".")) { // For @Id properties we chop off the last part of the path propertyName = SplitName.parent(propertyName); } } else if (beanProperty.isDiscriminator()) { propertyName = SplitName.parent(propertyName); } else if (beanProperty instanceof BeanPropertyAssocOne) { String msg = "Column [" + column.getDbColumn() + "] mapped to complex Property[" + propertyName + "]"; msg += ". It should be mapped to a simple property (probably the Id property). "; throw new PersistenceException(msg); } if (propertyName != null) { boolean assocProperty = el.isAssocProperty(); if (!assocProperty) { pathProps.addToPath(null, propertyName); } else { String[] pathProp = SplitName.split(propertyName); pathProps.addToPath(pathProp[0], pathProp[1]); } } } /** * Return a path for a foreign key property using the default naming convention. */ private String assocOneIdPath(String propertyName) { return propertyName.substring(0, propertyName.length() - 2) + ".id"; } /** * Return the SQL response with row limiting (when not an update statement). */ private SqlLimitResponse buildSql(String selectClause, OrmQueryRequest request, CQueryPredicates predicates, SqlTree select) { 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(); } private SqlLimitResponse buildSqlDelete(String selectClause, OrmQueryRequest request, CQueryPredicates predicates, SqlTree select) { return new BuildReq(selectClause, request, predicates, select).buildSql(); } private SqlLimitResponse buildSqlUpdate(String selectClause, OrmQueryRequest request, CQueryPredicates predicates, SqlTree select) { return new BuildReq(selectClause, request, predicates, select, true).buildSql(); } 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; 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)); } } } } 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(") "); } } } private void appendFrom() { if (selectClause == null || !selectClause.startsWith("update")) { sb.append(" from "); sb.append(select.getFromSql()); } } private void appendAndOrWhere() { if (hasWhere) { sb.append(" and "); } else { sb.append(" where "); hasWhere = true; } } 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 softDeletePredicates = query.getSoftDeletePredicates(); if (softDeletePredicates != null) { appendAndOrWhere(); for (int i = 0; i < softDeletePredicates.size(); i++) { if (i > 0) { sb.append(" and "); } sb.append(softDeletePredicates.get(i)); } } } 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); } } 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); } } private boolean hasValue(String s) { return s != null && !s.isEmpty(); } } boolean isPlatformDistinctOn() { return dbPlatform.isPlatform(Platform.POSTGRES); } /** * Return the 'for update' FROM hint (sql server). */ String fromForUpdate(SpiQuery query) { Query.ForUpdate mode = query.getForUpdateMode(); if (mode == null) { return null; } else { return dbPlatform.fromForUpdate(mode); } } }