io.ebeaninternal.server.query.SqlTreeBuilder Maven / Gradle / Ivy
package io.ebeaninternal.server.query;
import io.ebean.util.SplitName;
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.api.ManyWhereJoins;
import io.ebeaninternal.api.PropertyJoin;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.api.SpiQuery.Type;
import io.ebeaninternal.server.core.OrmQueryRequest;
import io.ebeaninternal.server.deploy.TableJoin;
import io.ebeaninternal.server.querydefn.OrmQueryDetail;
import io.ebeaninternal.server.querydefn.OrmQueryProperties;
import java.util.*;
import static java.lang.System.Logger.Level.ERROR;
/**
* Factory for SqlTree.
*/
public final class SqlTreeBuilder {
private static final System.Logger log = CoreLog.internal;
private final SpiQuery> query;
private final STreeType desc;
private final OrmQueryDetail queryDetail;
private final CQueryPredicates predicates;
private final boolean subQuery;
private final boolean distinctOnPlatform;
/**
* Property if resultSet contains master and detail rows.
*/
private STreePropertyAssocMany manyProperty;
private final SqlTreeAlias alias;
private final DefaultDbSqlContext ctx;
private final HashSet selectIncludes = new HashSet<>();
private final ManyWhereJoins manyWhereJoins;
private final TableJoin includeJoin;
private final boolean rawSql;
/**
* rawNoId true if the RawSql does not include the @Id property
*/
private final boolean rawNoId;
private final boolean disableLazyLoad;
private final boolean readOnly;
private final SpiQuery.TemporalMode temporalMode;
private SqlTreeNode rootNode;
private boolean sqlDistinct;
private final boolean platformDistinctNoLobs;
private final SqlTreeCommon common;
/**
* Construct for RawSql query.
*/
SqlTreeBuilder(OrmQueryRequest> request, CQueryPredicates predicates, OrmQueryDetail queryDetail, boolean rawNoId) {
this.rawSql = true;
this.desc = request.descriptor();
this.rawNoId = rawNoId;
this.disableLazyLoad = request.query().isDisableLazyLoading();
this.readOnly = Boolean.TRUE.equals(request.query().isReadOnly());
this.query = null;
this.subQuery = false;
this.distinctOnPlatform = false;
this.platformDistinctNoLobs = false;
this.queryDetail = queryDetail;
this.predicates = predicates;
this.temporalMode = SpiQuery.TemporalMode.CURRENT;
this.includeJoin = null;
this.manyWhereJoins = null;
this.alias = null;
this.ctx = null;
this.common = new SqlTreeCommon(temporalMode, disableLazyLoad, readOnly, null);
}
/**
* The predicates are used to determine if 'extra' joins are required to
* support the where and/or order by clause. If so these extra joins are added
* to the root node.
*/
SqlTreeBuilder(String columnAliasPrefix, CQueryBuilder builder, OrmQueryRequest> request, CQueryPredicates predicates) {
this.rawSql = false;
this.rawNoId = false;
this.desc = request.descriptor();
this.query = request.query();
this.temporalMode = SpiQuery.TemporalMode.of(query);
this.disableLazyLoad = query.isDisableLazyLoading();
this.readOnly = Boolean.TRUE.equals(query.isReadOnly());
this.subQuery = Type.SQ_EXISTS == query.type()
|| Type.SQ_EX == query.type()
|| Type.ID_LIST == query.type()
|| Type.DELETE == query.type()
|| query.isCountDistinct();
this.includeJoin = query.m2mIncludeJoin();
this.manyWhereJoins = query.manyWhereJoins();
this.queryDetail = query.detail();
this.predicates = predicates;
this.alias = new SqlTreeAlias(request.baseTableAlias(), temporalMode);
this.distinctOnPlatform = builder.isPlatformDistinctOn();
this.platformDistinctNoLobs = builder.isPlatformDistinctNoLobs();
String fromForUpdate = builder.fromForUpdate(query);
CQueryHistorySupport historySupport = builder.historySupport(query);
String colAlias = subQuery ? null : columnAliasPrefix;
this.ctx = new DefaultDbSqlContext(alias, colAlias, historySupport, fromForUpdate);
this.common = new SqlTreeCommon(temporalMode, disableLazyLoad, readOnly, includeJoin);
}
/**
* Build based on the includes and using the BeanJoinTree.
*/
public SqlTree build() {
// build the appropriate chain of SelectAdapter's
buildRoot(desc);
// build the actual String
String distinctOn = null;
String selectSql = null;
String fromSql = null;
String inheritanceWhereSql = null;
String groupBy = null;
STreeProperty[] encryptedProps = null;
if (!rawSql) {
selectSql = buildSelectClause();
fromSql = buildFromClause();
inheritanceWhereSql = buildWhereClause();
groupBy = buildGroupByClause();
distinctOn = buildDistinctOn();
encryptedProps = ctx.encryptedProps();
query.incrementAsOfTableCount(ctx.asOfTableCount());
}
boolean includeJoins = alias != null && alias.isIncludeJoins();
return new SqlTree(rootNode, distinctOn, selectSql, fromSql, groupBy, inheritanceWhereSql, encryptedProps, manyProperty, includeJoins);
}
private String buildSelectClause() {
if (rawSql) {
return "Not Used";
}
if (query.type() == Type.SQ_EXISTS) {
// effective query is "where exists (select 1 from ...)"
return "1";
}
rootNode.appendSelect(ctx, subQuery);
return trimComma(ctx.content());
}
private String buildGroupByClause() {
if (rawSql || (!rootNode.isAggregation() && query.havingExpressions() == null)) {
return null;
}
ctx.startGroupBy();
rootNode.appendGroupBy(ctx, subQuery);
return trimComma(ctx.content());
}
private String buildDistinctOn() {
String distinctOn = predicates.dbDistinctOn();
if (distinctOn != null) {
return distinctOn;
}
if (rawSql || !distinctOnPlatform || !sqlDistinct || Type.COUNT == query.type()) {
return null;
}
ctx.startGroupBy();
rootNode.appendDistinctOn(ctx, subQuery);
String idCols = trimComma(ctx.content());
return idCols == null ? null : mergeOnDistinct(idCols, predicates.dbOrderBy());
}
static String mergeOnDistinct(String idCols, String dbOrderBy) {
if (dbOrderBy == null) {
return idCols;
}
dbOrderBy = DbOrderByTrim.trim(dbOrderBy);
StringBuilder sb = new StringBuilder(dbOrderBy.length() + idCols.length() + 2);
sb.append(dbOrderBy);
String[] split = idCols.split(",");
for (String col : split) {
col = col.trim();
if (!DbOrderByTrim.contains(dbOrderBy, col)) {
sb.append(", ").append(col);
}
}
return sb.toString();
}
/**
* Trim the first comma.
*/
private String trimComma(String groupBy) {
if (groupBy.length() < SqlTreeNode.COMMA.length()) {
return null;
} else {
return groupBy.substring(SqlTreeNode.COMMA.length());
}
}
private String buildWhereClause() {
if (rawSql) {
return "Not Used";
}
rootNode.appendWhere(ctx);
return ctx.content();
}
private String buildFromClause() {
if (rawSql) {
return "Not Used";
}
rootNode.appendFrom(ctx, SqlJoinType.AUTO);
return ctx.content();
}
private void buildRoot(STreeType desc) {
rootNode = buildSelectChain(null, null, desc, null);
if (!rawSql) {
alias.addJoin(queryDetail.getFetchPaths(), desc);
alias.addJoin(predicates.predicateIncludes(), desc);
alias.addManyWhereJoins(manyWhereJoins.propertyNames());
// build set of table alias
alias.buildAlias();
predicates.parseTableAlias(alias);
}
}
/**
* Recursively build the query tree depending on what leaves in the tree
* should be included.
*/
private SqlTreeNode buildSelectChain(String prefix, STreePropertyAssoc prop, STreeType desc, List joinList) {
List myJoinList = new ArrayList<>();
List extraProps = new ArrayList<>();
for (STreePropertyAssocOne one : desc.propsOne()) {
String propPrefix = SplitName.add(prefix, one.name());
if (isIncludeBean(propPrefix)) {
selectIncludes.add(propPrefix);
if (!one.hasForeignKey()) {
extraProps.add(one);
}
buildSelectChain(propPrefix, one, one.target(), myJoinList);
}
}
for (STreePropertyAssocMany many : desc.propsMany()) {
String propPrefix = SplitName.add(prefix, many.name());
if (isIncludeMany(propPrefix, many)) {
selectIncludes.add(propPrefix);
buildSelectChain(propPrefix, many, many.target(), myJoinList);
}
}
OrmQueryProperties queryProps = queryDetail.getChunk(prefix, false);
SqlTreeProperties props = getBaseSelect(desc, queryProps);
if (prefix == null && !rawSql) {
if (props.requireSqlDistinct(manyWhereJoins)) {
sqlDistinct = true;
}
addManyWhereJoins(myJoinList);
}
extraProps.forEach(props::addExtra);
if (!rawSql && manyWhereJoins.isFormulaWithJoin(prefix)) {
for (String property : manyWhereJoins.formulaJoinProperties(prefix)) {
final STreeProperty beanProperty = desc.findPropertyFromPath(property);
myJoinList.add(new SqlTreeNodeFormulaWhereJoin(beanProperty, SqlJoinType.OUTER, null));
}
}
SqlTreeNode selectNode = buildNode(prefix, prop, desc, myJoinList, props);
if (joinList != null) {
joinList.add(selectNode);
}
if (sqlDistinct && platformDistinctNoLobs) {
selectNode.unselectLobsForPlatform();
}
return selectNode;
}
/**
* Add joins used to support where clause predicates on 'many' properties.
*
* These joins are effectively independent of any fetch joins on 'many'
* properties.
*
*/
private void addManyWhereJoins(List myJoinList) {
Collection includes = manyWhereJoins.propertyJoins();
for (PropertyJoin joinProp : includes) {
STreePropertyAssoc beanProperty = (STreePropertyAssoc) desc.findPropertyFromPath(joinProp.property());
SqlTreeNodeManyWhereJoin nodeJoin = new SqlTreeNodeManyWhereJoin(joinProp.property(), beanProperty, joinProp.sqlJoinType(), temporalMode);
myJoinList.add(nodeJoin);
if (manyWhereJoins.isFormulaWithJoin(joinProp.property())) {
for (String property : manyWhereJoins.formulaJoinProperties(joinProp.property())) {
STreeProperty beanProperty2 = desc.findPropertyFromPath(SplitName.add(joinProp.property(), property));
myJoinList.add(new SqlTreeNodeFormulaWhereJoin(beanProperty2, SqlJoinType.OUTER, joinProp.property()));
}
}
}
}
private SqlTreeNode buildNode(String prefix, STreePropertyAssoc prop, STreeType desc, List myList, SqlTreeProperties props) {
if (prefix == null) {
buildExtraJoins(desc, myList);
// Optional many property for lazy loading query
STreePropertyAssocMany lazyLoadMany = (query == null) ? null : query.lazyLoadMany();
boolean withId = !rawNoId && !subQuery && (query == null || query.isWithId());
String baseTable = (query == null) ? null : query.baseTable();
if (baseTable == null) {
baseTable = desc.baseTable(temporalMode);
}
return new SqlTreeNodeRoot(desc, props, myList, withId, lazyLoadMany, common, sqlDistinct, baseTable);
} else if (prop instanceof STreePropertyAssocMany) {
return new SqlTreeNodeManyRoot(prefix, (STreePropertyAssocMany) prop, props, myList, withId(), common);
} else {
return new SqlTreeNodeBean(prefix, prop, props, myList, withId(), common);
}
}
boolean withId() {
return isNotSingleAttribute() && !subQuery;
}
/**
* Return true if the Id property should be excluded (as it is automatically included).
*/
private boolean excludeIdProperty() {
return query == null || !query.isSingleAttribute() && !query.isManualId();
}
/**
* Return true if the query is not a single attribute query.
*/
private boolean isNotSingleAttribute() {
return query == null || !query.isSingleAttribute();
}
/**
* Build extra joins to support properties used in where clause but not
* already in select clause.
*/
private void buildExtraJoins(STreeType desc, List myList) {
if (rawSql) {
return;
}
Set predicateIncludes = predicates.predicateIncludes();
if (predicateIncludes == null) {
return;
}
// Note includes - basically means joins.
// The selectIncludes is the set of joins that are required to support
// the 'select' part of the query. We may need to add other joins to
// support the predicates or order by clauses.
// remove ManyWhereJoins from the predicateIncludes
predicateIncludes.removeAll(manyWhereJoins.propertyNames());
predicateIncludes.addAll(predicates.orderByIncludes());
// look for predicateIncludes that are not in selectIncludes and add
// them as extra joins to the query
IncludesDistiller extraJoinDistill = new IncludesDistiller(desc, selectIncludes, predicateIncludes, manyWhereJoins, temporalMode);
Collection extraJoins = extraJoinDistill.getExtraJoinRootNodes();
if (!extraJoins.isEmpty()) {
// add extra joins required to support predicates
// and/or order by clause
for (SqlTreeNodeExtraJoin extraJoin : extraJoins) {
myList.add(extraJoin);
if (extraJoin.isManyJoin()) {
// as we are now going to join to the many then we need
// to add the distinct to the sql query to stop duplicate
// rows...
sqlDistinct = true;
}
}
}
}
/**
* A subQuery has slightly different rules in that it just generates SQL (into
* the where clause) and its properties are not required to read the resultSet
* etc.
*
* This means it can included individual properties of an embedded bean.
*
*/
private void addPropertyToSubQuery(SqlTreeProperties selectProps, STreeType desc, String propName, String path) {
STreeProperty p = desc.findPropertyWithDynamic(propName, path);
if (p == null) {
log.log(ERROR, "property [{0}] not found on {1} for query - excluding it.", propName, desc);
return;
} else if (p instanceof STreePropertyAssoc && p.isEmbedded()) {
// if the property is embedded we need to lookup the real column name
int pos = propName.indexOf('.');
if (pos > -1) {
String name = propName.substring(pos + 1);
p = ((STreePropertyAssoc) p).target().findProperty(name);
}
}
selectProps.add(p);
}
private void addProperty(SqlTreeProperties selectProps, STreeType desc, OrmQueryProperties queryProps, String propName) {
if (subQuery) {
addPropertyToSubQuery(selectProps, desc, propName, queryProps.getPath());
return;
}
int basePos = propName.indexOf('.');
if (basePos > -1 && !propName.contains(" as ")) {
// property on an embedded bean. Embedded beans do not yet
// support being partially populated so we include the
// 'base' property and make sure we only do that once
String baseName = propName.substring(0, basePos);
// make sure we only included the base/embedded bean once
if (!selectProps.containsProperty(baseName)) {
STreeProperty p = desc.findPropertyWithDynamic(baseName, null);
if (p == null) {
// maybe dynamic formula with schema prefix
p = desc.findPropertyWithDynamic(propName, null);
if (p != null) {
selectProps.add(p);
} else {
log.log(ERROR, "property [{0}] not found on {1} for query - excluding it.", propName, desc);
}
} else if (p.isEmbedded() || (p instanceof STreePropertyAssoc && !queryProps.isIncludedBeanJoin(p.name()))) {
// add the embedded bean or the *ToOne assoc bean. We skip the check that the *ToOne propName maps to Id property ...
selectProps.add(p);
} else {
log.log(ERROR, "property [{0}] expected to be an embedded or *ToOne bean for query - excluding it.", p.fullName());
}
}
} else {
// find the property including searching the
// sub class hierarchy if required
STreeProperty p = desc.findPropertyWithDynamic(propName, queryProps.getPath());
if (p == null) {
log.log(ERROR, "property [{0}] not found on {1} for query - excluding it.", propName, desc);
p = desc.findProperty("id");
selectProps.add(p);
} else if (p.isId() && excludeIdProperty()) {
// do not bother to include id for normal queries as the
// id is always added (except for subQueries)
} else if (p instanceof STreePropertyAssoc) {
// need to check if this property should be
// excluded. This occurs when this property is
// included as a bean join. With a bean join
// the property should be excluded as the bean
// join has its own node in the SqlTree.
if (!queryProps.isIncludedBeanJoin(p.name())) {
// include the property... which basically
// means include the foreign key column(s)
selectProps.add(p);
}
} else {
selectProps.add(p);
if (p.isAggregationManyToOne()) {
p.extraIncludes(predicates.predicateIncludes());
}
}
}
}
private SqlTreeProperties getBaseSelectPartial(STreeType desc, OrmQueryProperties queryProps) {
SqlTreeProperties selectProps = new SqlTreeProperties();
// add properties in the order in which they appear
// in the query. Gives predictable sql/properties for
// use with SqlSelect type queries.
// Also note that this can include transient properties.
// This makes sense for transient properties used to
// hold sum() count() type values (with SqlSelect)
final Set selectInclude = queryProps.getIncluded();
for (String propName : selectInclude) {
if (!propName.isEmpty()) {
addProperty(selectProps, desc, queryProps, propName);
}
}
if (!selectProps.isAggregationManyToOne()) {
final Set selectQueryJoin = queryProps.getSelectQueryJoin();
if (selectQueryJoin != null) {
for (String joinProperty : selectQueryJoin) {
if (!selectInclude.contains(joinProperty)) {
addProperty(selectProps, desc, queryProps, joinProperty);
}
}
}
}
return selectProps;
}
private SqlTreeProperties getBaseSelect(STreeType desc, OrmQueryProperties queryProps) {
boolean partial = queryProps != null && !queryProps.allProperties();
if (partial) {
return getBaseSelectPartial(desc, queryProps);
}
SqlTreeProperties selectProps = new SqlTreeProperties();
selectProps.setAllProperties();
// normal simple properties of the bean
selectProps.add(desc.propsBaseScalar());
selectProps.add(desc.propsEmbedded());
for (STreePropertyAssocOne propertyAssocOne : desc.propsOne()) {
//noinspection StatementWithEmptyBody
if (queryProps != null
&& queryProps.isIncludedBeanJoin(propertyAssocOne.name())
&& propertyAssocOne.hasForeignKey()
&& !propertyAssocOne.isFormula()) {
// if it is a joined bean with FK constraint... then don't add the property
// as it will have its own entire Node in the SqlTree
} else {
selectProps.add(propertyAssocOne);
}
}
return selectProps;
}
/**
* Return true if this many node should be included in the query.
*/
private boolean isIncludeMany(String propName, STreePropertyAssocMany manyProp) {
if (queryDetail.isJoinsEmpty()) {
return false;
}
if (queryDetail.includesPath(propName)) {
if (manyProperty != null) {
// only one many associated allowed to be included in fetch
return false;
}
manyProperty = manyProp;
return true;
}
return false;
}
/**
* Test to see if we are including this node into the query.
*
* Return true if this node is FULLY included resulting in table join. If the
* node is not included but its parent has been included then a "bean proxy"
* is added and false is returned.
*/
private boolean isIncludeBean(String prefix) {
if (queryDetail.includesPath(prefix)) {
// explicitly included
String[] splitNames = SplitName.split(prefix);
queryDetail.includeBeanJoin(splitNames[0], splitNames[1]);
return true;
}
return false;
}
/**
* Takes the select includes and the predicates includes and determines the
* extra joins required to support the predicates (that are not already
* supported by the select includes).
*
* This returns ONLY the leaves. The joins for the leaves
*
*/
private static class IncludesDistiller {
private final STreeType desc;
private final Set selectIncludes;
private final Set predicateIncludes;
private final SpiQuery.TemporalMode temporalMode;
private final ManyWhereJoins manyWhereJoins;
private final Map joinRegister = new HashMap<>();
private final Map rootRegister = new HashMap<>();
private IncludesDistiller(STreeType desc, Set selectIncludes,
Set predicateIncludes, ManyWhereJoins manyWhereJoins, SpiQuery.TemporalMode temporalMode) {
this.desc = desc;
this.selectIncludes = selectIncludes;
this.predicateIncludes = predicateIncludes;
this.manyWhereJoins = manyWhereJoins;
this.temporalMode = temporalMode;
}
/**
* Build the collection of extra joins returning just the roots.
*
* each root returned here could contain a little tree of joins. This
* follows the more natural pattern and allows for forcing outer joins from
* a join to a 'many' down through the rest of its tree.
*
*/
private Collection getExtraJoinRootNodes() {
String[] extras = findExtras();
if (extras.length == 0) {
return rootRegister.values();
}
// sort so we process only getting the leaves
// excluding nodes between root and the leaf
Arrays.sort(extras);
// reverse order so get the leaves first...
for (String extra : extras) {
createExtraJoin(extra);
}
return rootRegister.values();
}
private void createExtraJoin(String includeProp) {
SqlTreeNodeExtraJoin extraJoin = createJoinLeaf(includeProp);
if (extraJoin != null) {
// add the extra join...
// add many where joins
if (manyWhereJoins.isFormulaWithJoin(includeProp)) {
for (String property : manyWhereJoins.formulaJoinProperties(includeProp)) {
STreeProperty beanProperty = desc.findPropertyFromPath(SplitName.add(includeProp, property));
extraJoin.addChild(new SqlTreeNodeFormulaWhereJoin(beanProperty, SqlJoinType.OUTER, null));
}
}
// find root of this extra join... linking back to the
// parents (creating the tree) as it goes.
SqlTreeNodeExtraJoin root = findExtraJoinRoot(includeProp, extraJoin);
// register the root because these are the only ones we
// return back.
rootRegister.put(root.name(), root);
}
}
/**
* Create a SqlTreeNodeExtraJoin, register and return it.
*/
private SqlTreeNodeExtraJoin createJoinLeaf(String propertyName) {
ExtraJoin extra = desc.extraJoin(propertyName);
if (extra == null) {
return null;
} else {
SqlTreeNodeExtraJoin extraJoin = new SqlTreeNodeExtraJoin(propertyName, extra.property(), extra.isContainsMany(), temporalMode);
joinRegister.put(propertyName, extraJoin);
return extraJoin;
}
}
/**
* Find the root the this extra join tree.
*
* This may need to create a parent join implicitly if a predicate join
* 'skips' a level. e.g. where details.user.id = 1 (maybe join to details is
* not specified and is implicitly created.
*
*/
private SqlTreeNodeExtraJoin findExtraJoinRoot(String includeProp, SqlTreeNodeExtraJoin childJoin) {
while (true) {
int dotPos = includeProp.lastIndexOf('.');
if (dotPos == -1) {
// no parent possible(parent is root)
return childJoin;
} else {
// look in register ...
String parentPropertyName = includeProp.substring(0, dotPos);
if (selectIncludes.contains(parentPropertyName)) {
// parent already handled by select
return childJoin;
}
SqlTreeNodeExtraJoin parentJoin = joinRegister.get(parentPropertyName);
if (parentJoin == null) {
// we need to create this the parent implicitly...
parentJoin = createJoinLeaf(parentPropertyName);
}
parentJoin.addChild(childJoin);
childJoin = parentJoin;
includeProp = parentPropertyName;
}
}
}
/**
* Find the extra joins required by predicates and not already taken care of
* by the select.
*/
private String[] findExtras() {
List extras = new ArrayList<>();
for (String predProp : predicateIncludes) {
if (!selectIncludes.contains(predProp)) {
extras.add(predProp);
}
}
return extras.toArray(new String[0]);
}
}
}