/*
* Copyright Aduna (http://www.aduna-software.com/) (c) 2008.
*
* Licensed under the Aduna BSD-style license.
*/
package org.openrdf.sail.rdbms.optimizers;
import static org.openrdf.sail.rdbms.algebra.ColumnVar.createCtx;
import static org.openrdf.sail.rdbms.algebra.ColumnVar.createObj;
import static org.openrdf.sail.rdbms.algebra.ColumnVar.createPred;
import static org.openrdf.sail.rdbms.algebra.ColumnVar.createSubj;
import static org.openrdf.sail.rdbms.algebra.base.SqlExprSupport.coalesce;
import static org.openrdf.sail.rdbms.algebra.base.SqlExprSupport.eq;
import static org.openrdf.sail.rdbms.algebra.base.SqlExprSupport.isNull;
import static org.openrdf.sail.rdbms.algebra.base.SqlExprSupport.or;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.openrdf.model.BNode;
import org.openrdf.model.Literal;
import org.openrdf.model.Resource;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.query.BindingSet;
import org.openrdf.query.Dataset;
import org.openrdf.query.algebra.And;
import org.openrdf.query.algebra.Distinct;
import org.openrdf.query.algebra.Extension;
import org.openrdf.query.algebra.Filter;
import org.openrdf.query.algebra.Join;
import org.openrdf.query.algebra.LeftJoin;
import org.openrdf.query.algebra.Order;
import org.openrdf.query.algebra.OrderElem;
import org.openrdf.query.algebra.Projection;
import org.openrdf.query.algebra.ProjectionElem;
import org.openrdf.query.algebra.ProjectionElemList;
import org.openrdf.query.algebra.Slice;
import org.openrdf.query.algebra.StatementPattern;
import org.openrdf.query.algebra.TupleExpr;
import org.openrdf.query.algebra.UnaryTupleOperator;
import org.openrdf.query.algebra.Union;
import org.openrdf.query.algebra.ValueExpr;
import org.openrdf.query.algebra.Var;
import org.openrdf.query.algebra.StatementPattern.Scope;
import org.openrdf.query.algebra.evaluation.QueryOptimizer;
import org.openrdf.sail.rdbms.RdbmsValueFactory;
import org.openrdf.sail.rdbms.algebra.BNodeColumn;
import org.openrdf.sail.rdbms.algebra.ColumnVar;
import org.openrdf.sail.rdbms.algebra.DatatypeColumn;
import org.openrdf.sail.rdbms.algebra.IdColumn;
import org.openrdf.sail.rdbms.algebra.JoinItem;
import org.openrdf.sail.rdbms.algebra.LabelColumn;
import org.openrdf.sail.rdbms.algebra.LanguageColumn;
import org.openrdf.sail.rdbms.algebra.LongLabelColumn;
import org.openrdf.sail.rdbms.algebra.LongURIColumn;
import org.openrdf.sail.rdbms.algebra.NumberValue;
import org.openrdf.sail.rdbms.algebra.RefIdColumn;
import org.openrdf.sail.rdbms.algebra.SelectProjection;
import org.openrdf.sail.rdbms.algebra.SelectQuery;
import org.openrdf.sail.rdbms.algebra.SqlEq;
import org.openrdf.sail.rdbms.algebra.SqlOr;
import org.openrdf.sail.rdbms.algebra.URIColumn;
import org.openrdf.sail.rdbms.algebra.UnionItem;
import org.openrdf.sail.rdbms.algebra.base.RdbmsQueryModelVisitorBase;
import org.openrdf.sail.rdbms.algebra.base.SqlExpr;
import org.openrdf.sail.rdbms.algebra.factories.SqlExprFactory;
import org.openrdf.sail.rdbms.exceptions.RdbmsException;
import org.openrdf.sail.rdbms.exceptions.RdbmsRuntimeException;
import org.openrdf.sail.rdbms.exceptions.UnsupportedRdbmsOperatorException;
import org.openrdf.sail.rdbms.managers.TransTableManager;
import org.openrdf.sail.rdbms.model.RdbmsResource;
import org.openrdf.sail.rdbms.schema.IdSequence;
/**
* Rewrites the core algebra model with a relation optimised model, using SQL.
*
* @author James Leigh
*
*/
public class SelectQueryOptimizer extends RdbmsQueryModelVisitorBase<RuntimeException> implements
QueryOptimizer
{
private static final String ALIAS = "t";
private SqlExprFactory sql;
private int aliasCount = 0;
private BindingSet bindings;
private Dataset dataset;
private RdbmsValueFactory vf;
private TransTableManager tables;
private IdSequence ids;
public void setSqlExprFactory(SqlExprFactory sql) {
this.sql = sql;
}
public void setValueFactory(RdbmsValueFactory vf) {
this.vf = vf;
}
public void setTransTableManager(TransTableManager statements) {
this.tables = statements;
}
public void setIdSequence(IdSequence ids) {
this.ids = ids;
}
public void optimize(TupleExpr tupleExpr, Dataset dataset, BindingSet bindings) {
this.dataset = dataset;
this.bindings = bindings;
tupleExpr.visit(this);
}
@Override
public void meet(Distinct node)
throws RuntimeException
{
super.meet(node);
if (node.getArg() instanceof SelectQuery) {
SelectQuery query = (SelectQuery)node.getArg();
query.setDistinct(true);
node.replaceWith(query);
}
}
@Override
public void meet(Union node)
throws RuntimeException
{
super.meet(node);
TupleExpr l = node.getLeftArg();
TupleExpr r = node.getRightArg();
if (!(l instanceof SelectQuery && r instanceof SelectQuery))
return;
SelectQuery left = (SelectQuery)l;
SelectQuery right = (SelectQuery)r;
if (left.isComplex() || right.isComplex())
return;
UnionItem union = new UnionItem("u" + aliasCount++);
union.addUnion(left.getFrom().clone());
union.addUnion(right.getFrom().clone());
SelectQuery query = new SelectQuery();
query.setFrom(union);
mergeSelectClause(query, left);
mergeSelectClause(query, right);
node.replaceWith(query);
}
@Override
public void meet(Join node)
throws RuntimeException
{
super.meet(node);
TupleExpr l = node.getLeftArg();
TupleExpr r = node.getRightArg();
if (!(l instanceof SelectQuery && r instanceof SelectQuery))
return;
SelectQuery left = (SelectQuery)l;
SelectQuery right = (SelectQuery)r;
if (left.isComplex() || right.isComplex())
return;
left = left.clone();
right = right.clone();
filterOn(left, right);
mergeSelectClause(left, right);
left.addJoin(right);
node.replaceWith(left);
}
@Override
public void meet(LeftJoin node)
throws RuntimeException
{
super.meet(node);
TupleExpr l = node.getLeftArg();
TupleExpr r = node.getRightArg();
if (!(l instanceof SelectQuery && r instanceof SelectQuery))
return;
SelectQuery left = (SelectQuery)l;
SelectQuery right = (SelectQuery)r;
if (left.isComplex() || right.isComplex())
return;
left = left.clone();
right = right.clone();
filterOn(left, right);
mergeSelectClause(left, right);
left.addLeftJoin(right);
List<SqlExpr> filters = new ArrayList<SqlExpr>();
if (node.getCondition() != null) {
for (ValueExpr expr : flatten(node.getCondition())) {
try {
filters.add(sql.createBooleanExpr(expr));
}
catch (UnsupportedRdbmsOperatorException e) {
return;
}
}
}
for (SqlExpr filter : filters) {
right.addFilter(filter);
}
node.replaceWith(left);
}
@Override
public void meet(StatementPattern sp) {
super.meet(sp);
Var subjVar = sp.getSubjectVar();
Var predVar = sp.getPredicateVar();
Var objVar = sp.getObjectVar();
Var ctxVar = sp.getContextVar();
Value subjValue = getVarValue(subjVar, bindings);
Value predValue = getVarValue(predVar, bindings);
Value objValue = getVarValue(objVar, bindings);
Value ctxValue = getVarValue(ctxVar, bindings);
if (subjValue instanceof Literal || predValue instanceof Literal || predValue instanceof BNode
|| ctxValue instanceof Literal)
{
// subj and ctx must be a Resource and pred must be a URI
return;
}
Resource[] contexts = getContexts(sp, ctxValue);
if (contexts == null)
return;
String alias = getTableAlias(predValue) + aliasCount++;
Number predId = getInternalId(predValue);
String tableName;
boolean present;
try {
tableName = tables.getTableName(predId);
present = tables.isPredColumnPresent(predId);
}
catch (SQLException e) {
throw new RdbmsRuntimeException(e);
}
JoinItem from = new JoinItem(alias, tableName, predId);
ColumnVar s = createSubj(alias, subjVar, (Resource)subjValue);
ColumnVar p = createPred(alias, predVar, (URI)predValue, !present);
ColumnVar o = createObj(alias, objVar, objValue);
ColumnVar c = createCtx(alias, ctxVar, (Resource)ctxValue);
s.setTypes(tables.getSubjTypes(predId));
o.setTypes(tables.getObjTypes(predId));
SelectQuery query = new SelectQuery();
query.setFrom(from);
Map<String, ColumnVar> vars = new HashMap<String, ColumnVar>(4);
for (ColumnVar var : new ColumnVar[] { s, p, o, c }) {
from.addVar(var);
Value value = var.getValue();
if (vars.containsKey(var.getName())) {
IdColumn existing = new IdColumn(vars.get(var.getName()));
from.addFilter(new SqlEq(new IdColumn(var), existing));
}
else if (value != null && !var.isImplied()) {
try {
NumberValue vc = new NumberValue(vf.getInternalId(value));
from.addFilter(new SqlEq(new RefIdColumn(var), vc));
}
catch (RdbmsException e) {
throw new RdbmsRuntimeException(e);
}
}
else {
vars.put(var.getName(), var);
}
if (!var.isHidden() && value == null) {
SelectProjection proj = new SelectProjection();
proj.setVar(var);
proj.setId(new RefIdColumn(var));
proj.setStringValue(coalesce(new URIColumn(var), new BNodeColumn(var), new LabelColumn(var),
new LongLabelColumn(var), new LongURIColumn(var)));
proj.setDatatype(new DatatypeColumn(var));
proj.setLanguage(new LanguageColumn(var));
query.addSqlSelectVar(proj);
}
}
if (contexts.length > 0) {
RdbmsResource[] ids = vf.asRdbmsResource(contexts);
RefIdColumn var = new RefIdColumn(c);
SqlExpr in = null;
for (RdbmsResource id : ids) {
NumberValue longValue;
try {
longValue = new NumberValue(vf.getInternalId(id));
}
catch (RdbmsException e) {
throw new RdbmsRuntimeException(e);
}
SqlEq eq = new SqlEq(var.clone(), longValue);
if (in == null) {
in = eq;
}
else {
in = new SqlOr(in, eq);
}
}
from.addFilter(in);
}
sp.replaceWith(query);
}
@Override
public void meet(Filter node)
throws RuntimeException
{
super.meet(node);
if (node.getArg() instanceof SelectQuery) {
SelectQuery query = (SelectQuery)node.getArg();
ValueExpr condition = null;
for (ValueExpr expr : flatten(node.getCondition())) {
try {
query.addFilter(sql.createBooleanExpr(expr));
}
catch (UnsupportedRdbmsOperatorException e) {
if (condition == null) {
condition = expr;
}
else {
condition = new And(condition, expr);
}
}
}
if (condition == null) {
node.replaceWith(node.getArg());
}
else {
node.setCondition(condition);
}
}
}
@Override
public void meet(Projection node)
throws RuntimeException
{
super.meet(node);
if (node.getArg() instanceof SelectQuery) {
SelectQuery query = (SelectQuery)node.getArg();
Map<String, String> bindingNames = new HashMap<String, String>();
List<SelectProjection> selection = new ArrayList<SelectProjection>();
ProjectionElemList list = node.getProjectionElemList();
for (ProjectionElem e : list.getElements()) {
String source = e.getSourceName();
String target = e.getTargetName();
bindingNames.put(source, target);
SelectProjection s = query.getSelectProjection(source);
if (s != null) {
selection.add(s);
}
}
query.setBindingNames(bindingNames);
query.setSqlSelectVar(selection);
node.replaceWith(query);
}
}
@Override
public void meet(Slice node)
throws RuntimeException
{
super.meet(node);
if (node.getArg() instanceof SelectQuery) {
SelectQuery query = (SelectQuery)node.getArg();
if (node.getOffset() > 0) {
query.setOffset(node.getOffset());
}
if (node.getLimit() >= 0) {
query.setLimit(node.getLimit());
}
node.replaceWith(query);
}
if (node.getArg() instanceof UnaryTupleOperator &&
((UnaryTupleOperator)node.getArg()).getArg() instanceof Extension &&
((Extension)((UnaryTupleOperator)node.getArg()).getArg()).getArg() instanceof SelectQuery) {
SelectQuery query = (SelectQuery) ((Extension)((UnaryTupleOperator)node.getArg()).getArg()).getArg();
if (node.getOffset() > 0) {
query.setOffset(node.getOffset());
}
if (node.getLimit() >= 0) {
query.setLimit(node.getLimit());
}
node.replaceWith(node.getArg());
}
}
@Override
public void meet(Order node)
throws RuntimeException
{
super.meet(node);
if (!(node.getArg() instanceof SelectQuery))
return;
SelectQuery query = (SelectQuery)node.getArg();
try {
for (OrderElem e : node.getElements()) {
ValueExpr expr = e.getExpr();
boolean asc = e.isAscending();
query.addOrder(sql.createBNodeExpr(expr), asc);
query.addOrder(sql.createUriExpr(expr), asc);
query.addOrder(sql.createNumericExpr(expr), asc);
query.addOrder(sql.createDatatypeExpr(expr), asc);
query.addOrder(sql.createTimeExpr(expr), asc);
query.addOrder(sql.createLanguageExpr(expr), asc);
query.addOrder(sql.createLabelExpr(expr), asc);
}
node.replaceWith(query);
}
catch (UnsupportedRdbmsOperatorException e) {
// unsupported
}
}
private void filterOn(SelectQuery left, SelectQuery right) {
Map<String, ColumnVar> lvars = left.getVarMap();
Map<String, ColumnVar> rvars = right.getVarMap();
Set<String> names = new HashSet<String>(rvars.keySet());
names.retainAll(lvars.keySet());
for (String name : names) {
ColumnVar l = lvars.get(name);
ColumnVar r = rvars.get(name);
if (!l.isImplied() && !r.isImplied()) {
IdColumn rid = new IdColumn(r);
SqlExpr filter = eq(rid, new IdColumn(l));
if (r.isNullable()) {
filter = or(isNull(rid), filter);
}
right.addFilter(filter);
}
}
}
private Number getInternalId(Value predValue) {
try {
return vf.getInternalId(predValue);
}
catch (RdbmsException e) {
throw new RdbmsRuntimeException(e);
}
}
private Resource[] getContexts(StatementPattern sp, Value ctxValue) {
if (dataset == null) {
if (ctxValue != null)
return new Resource[] { (Resource)ctxValue };
return new Resource[0];
}
Set<URI> graphs = getGraphs(sp);
if (graphs.isEmpty())
return null; // Search zero contexts
if (ctxValue == null)
return graphs.toArray(new Resource[graphs.size()]);
if (graphs.contains(ctxValue))
return new Resource[] { (Resource)ctxValue };
// pattern specifies a context that is not part of the dataset
return null;
}
private Set<URI> getGraphs(StatementPattern sp) {
if (sp.getScope() == Scope.DEFAULT_CONTEXTS)
return dataset.getDefaultGraphs();
return dataset.getNamedGraphs();
}
private String getTableAlias(Value predValue) {
if (predValue != null) {
String localName = ((URI)predValue).getLocalName();
if (localName.length() >= 1) {
String alias = localName.substring(0, 1);
if (isLetters(alias)) {
return alias;
}
}
}
return ALIAS;
}
private Value getVarValue(Var var, BindingSet bindings) {
if (var == null) {
return null;
}
else if (var.hasValue()) {
return var.getValue();
}
else {
return bindings.getValue(var.getName());
}
}
private boolean isLetters(String alias) {
for (int i = 0, n = alias.length(); i < n; i++) {
if (!Character.isLetter(alias.charAt(i)))
return false;
}
return true;
}
private void mergeSelectClause(SelectQuery left, SelectQuery right) {
for (SelectProjection proj : right.getSqlSelectVar()) {
if (!left.hasSqlSelectVar(proj)) {
proj = proj.clone();
ColumnVar var = proj.getVar();
String name = var.getName();
ColumnVar existing = left.getVar(name);
if (existing != null) {
proj.setVar(existing);
}
left.addSqlSelectVar(proj);
}
}
}
private List<ValueExpr> flatten(ValueExpr condition) {
return flatten(condition, new ArrayList<ValueExpr>());
}
private List<ValueExpr> flatten(ValueExpr condition, List<ValueExpr> conditions) {
if (condition instanceof And) {
And and = (And)condition;
flatten(and.getLeftArg(), conditions);
flatten(and.getRightArg(), conditions);
}
else {
conditions.add(condition);
}
return conditions;
}
}