blob: b9c0d142323d0139117a11ee5f8bef970ea2f001 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.jdbc.kernel.exps;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.openjpa.jdbc.kernel.JDBCStoreQuery;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.Joins;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.Select;
import org.apache.openjpa.kernel.exps.AbstractExpressionVisitor;
import org.apache.openjpa.kernel.exps.Constant;
import org.apache.openjpa.kernel.exps.Context;
import org.apache.openjpa.kernel.exps.Expression;
import org.apache.openjpa.kernel.exps.QueryExpressions;
import org.apache.openjpa.kernel.exps.Subquery;
import org.apache.openjpa.kernel.exps.Value;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.UnsupportedException;
/**
* Turns parsed queries into selects.
*
* @author Abe White
*/
public class SelectConstructor
implements Serializable {
private static final long serialVersionUID = 1L;
private boolean _extent = false;
private Select _subselect = null;
private static final Localizer _loc = Localizer.forPackage(SelectConstructor.class);
/**
* Return true if we know the select to have on criteria; to be an extent.
* Note that even if this method returns false, {@link #evaluate} may still
* return null if we haven't cached whether the query is an extent yet.
*/
public boolean isExtent() {
return _extent;
}
public void setSubselect(Select subselect) {
_subselect = subselect;
}
/**
* Evaluate the expression, returning a new select and filling in any
* associated expression state. Use {@link #select} to then select the data.
*
* @param ctx fill with execution context
* @param state will be filled with expression state
*/
public Select evaluate(ExpContext ctx, Select parent, String alias,
QueryExpressions exps, QueryExpressionsState state) {
// already know that this query is equivalent to an extent?
Select sel;
if (_extent) {
sel = ctx.store.getSQLFactory().newSelect();
sel.setAutoDistinct((exps.distinct & QueryExpressions.DISTINCT_AUTO) != 0);
return sel;
}
// create a new select and initialize it with the joins needed for
// the criteria of this query
sel = newSelect(ctx, parent, alias, exps, state);
sel.setTablePerClassMeta(ctx.tpcMeta);
// create where clause; if there are no where conditions and
// no ordering or projections, we return null to signify that this
// query should be treated like an extent
Select inner = sel.getFromSelect();
SQLBuffer where = buildWhere((inner != null) ? inner : sel, ctx,
state.filter, exps.filter);
if (where == null && exps.projections.length == 0
&& exps.ordering.length == 0
&& (sel.getJoins() == null || sel.getJoins().isEmpty())) {
_extent = true;
sel.setAutoDistinct((exps.distinct & QueryExpressions.DISTINCT_AUTO) != 0);
return sel;
}
// now set sql criteria; it goes on the inner select if present
if (inner != null)
inner.where(where);
else
sel.where(where);
// apply grouping and having. this does not select the grouping
// columns, just builds the GROUP BY clauses. we don't build the
// ORDER BY clauses yet because if we decide to add this select
// to a union, the ORDER BY values get aliased differently
if (exps.having != null) {
Exp havingExp = (Exp) exps.having;
SQLBuffer buf = new SQLBuffer(ctx.store.getDBDictionary());
havingExp.appendTo(sel, ctx, state.having, buf);
sel.having(buf);
}
for (int i = 0; i < exps.grouping.length; i++)
((Val) exps.grouping[i]).groupBy(sel, ctx, state.grouping[i]);
if (exps.projections.length == 1) {
Val val = (Val) exps.projections[0];
if (val instanceof Count && ((Count)val).isCountDistinctMultiCols()) {
Select newSel = ctx.store.getSQLFactory().newSelect();
newSel.select("COUNT(*)", val);
newSel.setExpectedResultCount(1, true);
newSel.setFromSelect(sel);
sel.setExpectedResultCount(0, true);
sel = newSel;
}
}
return sel;
}
/**
* Return a new select with expressions initialized.
*/
private Select newSelect(ExpContext ctx, Select parent,
String alias, QueryExpressions exps, QueryExpressionsState state) {
Select subselect = JDBCStoreQuery.getThreadLocalSelect(_subselect);
Select sel = parent != null ? subselect
: ctx.store.getSQLFactory().newSelect();
sel.setAutoDistinct((exps.distinct & QueryExpressions.DISTINCT_AUTO) != 0);
sel.setJoinSyntax(ctx.fetch.getJoinSyntax());
sel.setParent(parent, alias);
Context[] qryCtx = JDBCStoreQuery.getThreadLocalContext();
Context lctx = null;
for (Context context : qryCtx) {
if (context.cloneFrom == exps.ctx()) {
lctx = context;
break;
}
}
if (sel.ctx() == null)
sel.setContext(lctx);
if (parent == null && lctx.getSubselContexts() != null) {
// this is the case subselect was created before parent got created
List<Context> subselCtxs = lctx.getSubselContexts();
for (Context subselCtx : subselCtxs) {
Select subsel = (Select) subselCtx.getSelect();
Subquery subquery = subselCtx.getSubquery();
subsel.setParent(sel, subquery.getCandidateAlias());
}
}
if (HasContainsExpressionVisitor.hasContains(exps.filter)) {
sel.setHasSubselect(true);
}
initialize(sel, ctx, exps, state);
if (!sel.getAutoDistinct()) {
if ((exps.distinct & QueryExpressions.DISTINCT_TRUE) != 0)
sel.setDistinct(true);
else if ((exps.distinct & QueryExpressions.DISTINCT_FALSE) != 0)
sel.setDistinct(false);
} else if (exps.projections.length > 0) {
if (!sel.isDistinct() && (exps.distinct & QueryExpressions.DISTINCT_TRUE) != 0){
// if the select is not distinct but the query is, force
// the select to be distinct
sel.setDistinct(true);
} else if (sel.isDistinct()) {
// when aggregating data or making a non-distinct projection
// from a distinct select, we have to select from a tmp
// table formed by a distinct subselect in the from clause;
// this subselect selects the pks of the candidate (to
// get unique candidate values) and needed field values and
// applies the where conditions; the outer select applies
// ordering, grouping, etc
boolean agg = exps.isAggregate();
boolean candidate = ProjectionExpressionVisitor.
hasCandidateProjections(exps.projections);
if (agg || (candidate
&& (exps.distinct & QueryExpressions.DISTINCT_TRUE) == 0)) {
DBDictionary dict = ctx.store.getDBDictionary();
dict.assertSupport(dict.supportsSubselect,
"SupportsSubselect");
Select inner = sel;
sel = ctx.store.getSQLFactory().newSelect();
sel.setParent(parent, alias);
sel.setDistinct(agg
&& (exps.distinct & QueryExpressions.DISTINCT_TRUE) != 0);
sel.setFromSelect(inner);
// auto-distincting happens to get unique candidate instances
// back; don't auto-distinct if the user isn't selecting
// candidate data
} else if (!candidate
&& (exps.distinct & QueryExpressions.DISTINCT_TRUE) == 0)
sel.setDistinct(false);
}
}
return sel;
}
/**
* Initialize all expressions.
*/
private void initialize(Select sel, ExpContext ctx, QueryExpressions exps,
QueryExpressionsState state) {
Map contains = null;
if (HasContainsExpressionVisitor.hasContains(exps.filter)
|| HasContainsExpressionVisitor.hasContains(exps.having))
contains = new HashMap(7);
// initialize filter and having expressions
Exp filterExp = (Exp) exps.filter;
state.filter = filterExp.initialize(sel, ctx, contains);
Exp havingExp = (Exp) exps.having;
if (havingExp != null)
state.having = havingExp.initialize(sel, ctx, contains);
// get the top-level joins and null the expression's joins
// at the same time so they aren't included in the where/having SQL
Joins filterJoins = state.filter.joins;
Joins havingJoins = (state.having == null) ? null : state.having.joins;
Joins joins = sel.and(filterJoins, havingJoins);
// initialize result values
if (exps.projections.length > 0) {
state.projections = new ExpState[exps.projections.length];
Val resultVal;
for (int i = 0; i < exps.projections.length; i++) {
resultVal = (Val) exps.projections[i];
if (!ctx.store.getDBDictionary().supportsParameterInSelect && resultVal instanceof Lit) {
((Lit)resultVal).setRaw(true);
}
// have to join through to related type for pc object
// projections; this ensures that we have all our joins cached
state.projections[i] = resultVal.initialize(sel, ctx,
Val.JOIN_REL | Val.FORCE_OUTER);
if (exps.projections.length > 1 && resultVal instanceof Count) {
if (((Count)resultVal).isCountDistinctMultiCols())
throw new UnsupportedException(_loc.get("count-distinct-multi-col-only"));
}
joins = sel.and(joins, state.projections[i].joins);
}
}
// initialize grouping
if (exps.grouping.length > 0) {
state.grouping = new ExpState[exps.grouping.length];
Val groupVal;
for (int i = 0; i < exps.grouping.length; i++) {
groupVal = (Val) exps.grouping[i];
// have to join through to related type for pc object groupings;
// this ensures that we have all our joins cached
state.grouping[i] = groupVal.initialize(sel, ctx, Val.JOIN_REL);
joins = sel.and(joins, state.grouping[i].joins);
}
}
// initialize ordering
if (exps.ordering.length > 0) {
state.ordering = new ExpState[exps.ordering.length];
Val orderVal;
for (int i = 0; i < exps.ordering.length; i++) {
orderVal = (Val) exps.ordering[i];
if (contains(orderVal, exps.grouping))
state.ordering[i] = orderVal.initialize(sel, ctx, Val.JOIN_REL);
else
state.ordering[i] = orderVal.initialize(sel, ctx, 0);
joins = sel.and(joins, state.ordering[i].joins);
}
}
sel.where(joins);
}
private boolean contains(Val orderVal, Value[] grouping) {
for (Value value : grouping) {
Val groupVal = (Val) value;
if (orderVal.equals(groupVal))
return true;
}
return false;
}
/**
* Create the where sql.
*/
private SQLBuffer buildWhere(Select sel, ExpContext ctx, ExpState state,
Expression filter) {
// create where buffer
SQLBuffer where = new SQLBuffer(ctx.store.getDBDictionary());
where.append("(");
Exp filterExp = (Exp) filter;
filterExp.appendTo(sel, ctx, state, where);
if (where.sqlEquals("(") || where.sqlEquals("(1 = 1"))
return null;
return where.append(")");
}
/**
* Select the data for this query.
*/
public void select(Select sel, ExpContext ctx, ClassMapping mapping,
boolean subclasses, QueryExpressions exps, QueryExpressionsState state,
int eager) {
Select inner = sel.getFromSelect();
Val val;
Joins joins = null;
boolean isCountDistinctMultiCols = false;
if (sel.getSubselectPath() != null)
joins = sel.newJoins().setSubselect(sel.getSubselectPath());
// build ordering clauses before select so that any eager join
// ordering gets applied after query ordering
for (int i = 0; i < exps.ordering.length; i++)
((Val) exps.ordering[i]).orderBy(sel, ctx, state.ordering[i],
exps.ascending[i]);
// if no result string set, select matching objects like normal
if (exps.projections.length == 0 && sel.getParent() == null) {
int subs = (subclasses) ? Select.SUBS_JOINABLE : Select.SUBS_NONE;
sel.selectIdentifier(mapping, subs, ctx.store, ctx.fetch, eager);
} else if (exps.projections.length == 0) {
// subselect for objects; we really just need the primary key values
sel.select(mapping.getPrimaryKeyColumns(), joins);
} else {
if (exps.projections.length == 1) {
val = (Val) exps.projections[0];
if (val instanceof Count && ((Count)val).isCountDistinctMultiCols()) {
isCountDistinctMultiCols = true;
if (sel.getParent() != null)
throw new UnsupportedException(_loc.get("count-distinct-multi-col-subselect-unsupported"));
}
}
// if we have an inner select, we need to select the candidate
// class' pk columns to guarantee unique instances
if (inner != null && !isCountDistinctMultiCols)
inner.select(mapping.getPrimaryKeyColumns(), joins);
// select each result value; no need to pass on the eager mode since
// under projections we always use EAGER_NONE
boolean pks = sel.getParent() != null;
for (int i = 0; i < exps.projections.length; i++) {
val = (Val) exps.projections[i];
if (inner != null) {
if (!isCountDistinctMultiCols)
val.selectColumns(inner, ctx, state.projections[i], pks);
else
val.select(inner, ctx, state.projections[i], pks);
} else
val.select(sel, ctx, state.projections[i], pks);
}
// make sure having columns are selected since it is required by
// some DBs. put them last so they don't affect result processing
if (exps.having != null && inner != null)
((Exp) exps.having).selectColumns(inner, ctx, state.having,
true);
}
// select ordering columns, since it is required by some DBs. put them
// last so they don't affect result processing
for (int i = 0; i < exps.ordering.length; i++) {
val = (Val) exps.ordering[i];
if (inner != null)
val.selectColumns(inner, ctx, state.ordering[i], true);
val.select(sel, ctx, state.ordering[i], true);
}
// add conditions limiting the projections to the proper classes; if
// this isn't a projection or a subq then they will already be added
if (exps.projections.length > 0 || sel.getParent() != null) {
ctx.store.loadSubclasses(mapping);
mapping.getDiscriminator().addClassConditions((inner != null)
? inner : sel, subclasses, joins);
}
}
/**
* Used to check whether a query's result projections are on the candidate.
*/
private static class ProjectionExpressionVisitor
extends AbstractExpressionVisitor {
private boolean _candidate = false;
private int _level = 0;
public static boolean hasCandidateProjections(Value[] projs) {
ProjectionExpressionVisitor v = new ProjectionExpressionVisitor();
for (Value proj : projs) {
proj.acceptVisit(v);
if (v._candidate)
return true;
}
return false;
}
@Override
public void enter(Value val) {
if (!_candidate) {
_candidate = (_level == 0 && val instanceof Constant)
|| (val instanceof PCPath
&& !((PCPath) val).isVariablePath());
}
_level++;
}
@Override
public void exit(Value val) {
_level--;
}
}
}