/*
* #!
* Ontopia Engine
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed 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 net.ontopia.topicmaps.query.impl.utils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.ontopia.utils.CompactHashSet;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.core.OccurrenceIF;
import net.ontopia.topicmaps.core.index.OccurrenceIndexIF;
import net.ontopia.topicmaps.query.core.InvalidQueryException;
import net.ontopia.topicmaps.query.impl.basic.AbstractQueryProcessor;
import net.ontopia.topicmaps.query.impl.basic.DynamicFailurePredicate;
import net.ontopia.topicmaps.query.impl.basic.DynamicOccurrencePredicate;
import net.ontopia.topicmaps.query.impl.basic.RemoveDuplicatesPredicate;
import net.ontopia.topicmaps.query.impl.basic.RolePlayerPredicate;
import net.ontopia.topicmaps.query.impl.basic.RulePredicate;
import net.ontopia.topicmaps.query.impl.basic.StringModule;
import net.ontopia.topicmaps.query.impl.basic.TypePredicate;
import net.ontopia.topicmaps.query.impl.basic.BasicPredicateIF;
import net.ontopia.topicmaps.query.impl.basic.OccurrencePredicate;
import net.ontopia.topicmaps.query.impl.basic.QueryMatches;
import net.ontopia.topicmaps.query.impl.basic.GreaterThanPredicate;
import net.ontopia.topicmaps.query.impl.basic.GreaterThanEqualsPredicate;
import net.ontopia.topicmaps.query.impl.basic.LessThanPredicate;
import net.ontopia.topicmaps.query.impl.basic.LessThanEqualsPredicate;
import net.ontopia.topicmaps.query.parser.AbstractClause;
import net.ontopia.topicmaps.query.parser.NotClause;
import net.ontopia.topicmaps.query.parser.OrClause;
import net.ontopia.topicmaps.query.parser.Pair;
import net.ontopia.topicmaps.query.parser.Parameter;
import net.ontopia.topicmaps.query.parser.ParsedRule;
import net.ontopia.topicmaps.query.parser.PredicateClause;
import net.ontopia.topicmaps.query.parser.PredicateIF;
import net.ontopia.topicmaps.query.parser.TologOptions;
import net.ontopia.topicmaps.query.parser.TologQuery;
import net.ontopia.topicmaps.query.parser.Variable;
import net.ontopia.utils.OntopiaRuntimeException;
// TODO
// - change architecture of optimizer so that individual optimizers
// can be simpler
// - change so that optimizers can be applied in a specific order
/**
* INTERNAL: An optimizer class that knows how to rewrite queries to
* equivalent, but more efficient queries. Used by the different query
* processor implementations to improve performance. Note that the
* only optimizations this class should perform are those which are
* independent of the tolog implementation used and which only rely
* on the semantics of tolog.</p>
*
* <p>The only optimizations performed at the moment are:</p>
*
* <ul>
* <li>Reordering of query clauses for better performance by limiting
* the number of intermediate results.</li>
* <li>Inlining rules which are simple aliases for a single predicate.
* </ul>
*
*/
public class QueryOptimizer {
private List optimizers;
private static final Class[] TYPES_TOPIC = { TopicIF.class };
/**
* INTERNAL: Get hold of an query optimizer instance.
* @param query The parsed query.
*/
public static QueryOptimizer getOptimizer(TologQuery query) {
// WARNING: method used by basic+rdbms tolog
TologOptions options = query.getOptions();
QueryOptimizer optimizer = new QueryOptimizer();
if (options != null) {
if (options.getBooleanValue("optimizer.inliner"))
optimizer.addOptimizer(new QueryOptimizer.RuleInliner());
if (options.getBooleanValue("optimizer.reorder")) {
// NOTE: new optimizer is now on by default
boolean newapproach =
options.getBooleanValue("optimizer.reorder.predicate-based");
optimizer.addOptimizer(new QueryOptimizer.Reorderer(newapproach));
}
if (options.getBooleanValue("optimizer.typeconflict"))
optimizer.addOptimizer(new QueryOptimizer.TypeConflictResolver());
if (options.getBooleanValue("optimizer.hierarchy-walker"))
optimizer.addOptimizer(new QueryOptimizer.HierarchyWalker());
if (options.getBooleanValue("optimizer.prefix-search"))
optimizer.addOptimizer(new QueryOptimizer.StringPrefixOptimizer());
if (options.getBooleanValue("optimizer.role-player-type"))
optimizer.addOptimizer(new QueryOptimizer.AddTypeToRolePlayer());
if (options.getBooleanValue("optimizer.next-previous"))
optimizer.addOptimizer(new QueryOptimizer.NextPreviousOptimizer());
}
return optimizer;
}
// ===== THE OPTIMIZER =====================================================
public QueryOptimizer() {
optimizers = new ArrayList();
}
public void addOptimizer(QueryOptimizerIF optimizer) {
optimizers.add(optimizer);
}
public TologQuery optimize(TologQuery query)
throws InvalidQueryException {
QueryContext context = new QueryContext(query);
for (int ix = 0; ix < optimizers.size(); ix++) {
QueryOptimizerIF optimizer = (QueryOptimizerIF) optimizers.get(ix);
optimizer.optimize(query, context);
}
query.setClauseList(optimize(query.getClauses(), context));
return query;
}
public ParsedRule optimize(ParsedRule rule)
throws InvalidQueryException {
QueryContext context = new QueryContext(null, rule);
rule.setClauseList(optimize(rule.getClauses(), context));
return rule;
}
public List optimize(List clauses, QueryContext context)
throws InvalidQueryException {
context.enterClauseList();
List newclauses = new ArrayList();
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
if (clause instanceof PredicateClause) {
PredicateClause pclause = (PredicateClause) clause;
for (int i = 0; i < optimizers.size(); i++)
pclause = ((QueryOptimizerIF) optimizers.get(i)).optimize(pclause, context);
clause = pclause;
} else if (clause instanceof OrClause) {
List alts = ((OrClause) clause).getAlternatives();
for (int i = 0; i < alts.size(); i++)
alts.set(i, optimize((List) alts.get(i), context));
} else if (clause instanceof NotClause) {
List notted = ((NotClause) clause).getClauses();
clause = new NotClause(optimize(notted, context));
}
newclauses.add(clause);
}
clauses = newclauses;
for (int ix = 0; ix < optimizers.size(); ix++)
clauses = ((QueryOptimizerIF) optimizers.get(ix)).optimize(clauses, context);
context.leaveClauseList();
return clauses;
}
// ===== ABSTRACT OPTIMIZER ===================================================
public static abstract class AbstractQueryOptimizer implements QueryOptimizerIF {
public void optimize(TologQuery query, QueryContext context)
throws InvalidQueryException {
// do nothing
}
public PredicateClause optimize(PredicateClause clause, QueryContext context)
throws InvalidQueryException {
return clause;
}
public List optimize(List clauses, QueryContext context)
throws InvalidQueryException {
return clauses;
}
}
// ===== INLINING =============================================================
/**
* INTERNAL: Optimizes the query by inlining all rules which are simple
* aliases for a single predicate.
*/
public static class RuleInliner extends AbstractQueryOptimizer {
public PredicateClause optimize(PredicateClause clause, QueryContext context) {
return clause.getReplacement();
}
}
// ===== REORDERING ========================================================
// Rules for optimization of clause order:
//
// 1. start with empty context
// 2. find the clause with the lowest cost
// 3. add variables bound by this clause to context
// 4. if more clauses goto 2
//
// To compute cost within context: use an estimator. There are
// two, which can be swapped, and more can be added.
/**
* INTERNAL: Optimizes the query by reordering the clauses into the
* optimal order for evaluation. See
* http://www.ontopia.net/topicmaps/materials/tolog.html, under
* 'Optimizing queries'.
*/
public static class Reorderer extends AbstractQueryOptimizer {
private boolean predicate_based;
public Reorderer(boolean predicate_based) {
this.predicate_based = predicate_based;
}
public List optimize(List qclauses, QueryContext qcontext) {
if (qcontext.getNestingLevel() > 1)
return qclauses;
CostEstimator estimator;
if (predicate_based)
estimator = new PredicateDrivenCostEstimator();
else
estimator = new SimpleCostEstimator();
return reorder(qclauses, Collections.EMPTY_SET, Collections.EMPTY_SET,
null, estimator);
}
}
/**
* INTERNAL: Optimizes the order of the query clauses in a context
* where the given variables are bound. Done as a static method so
* we can use it from within RulePredicate.
*
* @param qclauses The list of clauses to be reordered.
* @param boundvars Contains the variables bound when we get here
* @param literalvars Contains the variables representing literals.
* Only an issue in rules.
* @param rulename The name of the current rule (so we can delay
* recursive evaluation).
*/
public static List reorder(List qclauses, Set boundvars, Set literalvars,
String rulename, CostEstimator estimator) {
List clauses = new ArrayList(qclauses);
List newOrder = new ArrayList();
Set context = new CompactHashSet(boundvars);
while (clauses.size() > 0) {
int lowest = Integer.MAX_VALUE;
int best = 0;
//System.out.println("-------------------------------------------------");
// find current best clause
for (int ix = 0; ix < clauses.size(); ix++) {
int cost = estimator.computeCost(context,
(AbstractClause) clauses.get(ix),
literalvars, rulename);
//System.out.println("cost (" + cost + "): " + clauses.get(ix));
if (cost < lowest) {
lowest = cost;
best = ix;
}
}
//System.out.println("best (" + lowest +"): " + clauses.get(best));
// update based on choice
AbstractClause clause = (AbstractClause) clauses.get(best);
if (clause instanceof OrClause)
reorder((OrClause) clause, boundvars, literalvars, rulename, estimator);
else if (clause instanceof NotClause)
reorder((NotClause) clause, boundvars, literalvars, rulename, estimator);
context.addAll(clause.getAllVariables());
newOrder.add(clauses.get(best));
clauses.remove(best);
}
qclauses.clear();
for (int ix = 0; ix < newOrder.size(); ix++)
qclauses.add(newOrder.get(ix));
return newOrder;
}
/**
* INTERNAL: Reorders the clauses inside the alternatives in the OR
* branch. Useful for better performance in some cases, and
* certainly for fixing bug #1229.
*/
private static void reorder(OrClause clause, Set boundvars, Set literalvars,
String rulename, CostEstimator estimator) {
int ix = 0;
List alts = clause.getAlternatives();
Iterator it = alts.iterator();
while (it.hasNext()) {
List clauses = (List) it.next();
List newclauses = reorder(clauses, new CompactHashSet(boundvars),
new CompactHashSet(literalvars), rulename,
estimator);
alts.set(ix++, newclauses);
}
}
/**
* INTERNAL: Reorders the clauses inside the alternatives in the NOT
* branch.
*/
private static void reorder(NotClause clause, Set boundvars, Set literalvars,
String rulename, CostEstimator estimator) {
List newclauses = reorder(clause.getClauses(),
new CompactHashSet(boundvars),
new CompactHashSet(literalvars),
rulename,
estimator);
clause.setClauseList(newclauses);
}
// ===== RECURSIVE DUPLICATES ================================================
/**
* This optimizer adds RemoveDuplicatesPredicate on both sides of
* recursive calls within predicate rules. Recursive rules tend to
* generate lots of redundant temporary results, which again
* generate more redundant junk. Removing duplicates cuts down the
* junk dramatically. This optimizer fixes bug #791.
*/
public static class RecursivePruner extends AbstractQueryOptimizer {
public List optimize(List clauses, QueryContext context) {
if (context.getRuleName() == null)
return clauses; // queries are never recursive, only rules
List newclauses = new ArrayList();
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
if (clause instanceof PredicateClause &&
((PredicateClause) clause).getPredicate() instanceof RulePredicate) {
// adding on both sides because that proved to be fastest
newclauses.add(new PredicateClause(new RemoveDuplicatesPredicate(true)));
newclauses.add(clause);
newclauses.add(new PredicateClause(new RemoveDuplicatesPredicate(false)));
} else
newclauses.add(clause);
}
return newclauses;
}
}
// ===== TYPE CONFLICT RESOLVER ==============================================
/**
* Finds cases of conflicting variables and resolves them by
* replacing predicates which can never succeed with
* DynamicFailurePredicate.
*/
public static class TypeConflictResolver extends AbstractQueryOptimizer {
// the general rule is that vartypemap will contain the list of
// all possible types for the variables in it. this means that if
// it is empty there is a conflict and we can short out the
// predicate.
public List optimize(List clauses, QueryContext context)
throws InvalidQueryException {
Map vartypemap = context.getVariableTypes();
Map ptypemap = context.getParameterTypes();
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
if (clause instanceof PredicateClause) {
PredicateClause pclause = (PredicateClause) clause;
boolean ok = true;
PredicateIF predicate = pclause.getPredicate();
PredicateSignature signature = null;
if (predicate instanceof PumpPredicate)
continue;
try {
signature = PredicateSignature.getSignature(predicate);
} catch (InvalidQueryException e) {
// should not be possible to have this sort of error here. it
// should have been caught already.
throw new OntopiaRuntimeException("INTERNAL ERROR", e);
}
List args = pclause.getArguments();
for (int i = 0; i < args.size() && ok; i++) {
Object arg = args.get(i);
if (arg instanceof Variable) {
String varname = ((Variable) arg).getName();
Object[] types = (Object[]) vartypemap.get(varname);
ok = !emptyIntersection(types, signature.getTypes(i), context, arg,
predicate);
} else if (arg instanceof Parameter) {
String pname = ((Parameter) arg).getName();
Object[] types = (Object[]) ptypemap.get(pname);
ok = !emptyIntersection(types, signature.getTypes(i), context, arg,
predicate);
}
else {
if (arg instanceof Pair) {
Pair pair = (Pair)arg;
if (pair.getFirst() instanceof Variable) {
Variable var = (Variable) pair.getFirst();
Object[] types = (Object[]) vartypemap.get(var.getName());
ok = !emptyIntersection(TYPES_TOPIC, types, context, var,
predicate);
} else if (pair.getFirst() instanceof Parameter) {
Parameter par = (Parameter) pair.getFirst();
Object[] types = (Object[]) ptypemap.get(par.getName());
ok = !emptyIntersection(TYPES_TOPIC, types, context, par,
predicate);
} else
ok = containsAssignable(pair.getFirst().getClass(), TYPES_TOPIC);
} else
ok = containsAssignable(arg.getClass(), signature.getTypes(i));
}
}
if (!ok)
clauses.set(ix, new PredicateClause(new DynamicFailurePredicate(),
pclause.getArguments()));
} else if (clause instanceof OrClause) {
List alts = ((OrClause) clause).getAlternatives();
for (int i = 0; i < alts.size(); i++)
alts.set(i, optimize((List) alts.get(i), context));
} else if (clause instanceof NotClause) {
List notted = ((NotClause) clause).getClauses();
clause = new NotClause(optimize(notted, context));
}
}
return clauses;
}
private boolean emptyIntersection(Object[] types1, Object[] types2,
QueryContext context, Object arg,
PredicateIF predicate)
throws InvalidQueryException {
if (types1 == null || types2 == null)
return true;
boolean empty = true;
for (int ix = 0; ix < types1.length; ix++)
for (int i = 0; empty && i < types2.length; i++)
if (types1[ix].equals(types2[i]) ||
types1[ix].equals(Object.class) ||
types2[i].equals(Object.class))
empty = false;
if (empty && context.getBooleanOption("compiler.typecheck"))
throw new InvalidQueryException(
"Type conflict on " + arg + ": cannot be both " +
PredicateSignature.getClassList(types1) + " and, as required by " +
"predicate '" + predicate.getName() + "', " +
PredicateSignature.getClassList(types2));
return empty;
}
private boolean containsAssignable(Class type1, Object[] types2) {
for (int i = 0; i < types2.length; i++)
//! if (type1.isAssignableFrom((Class)types2[i])) return true;
if (((Class)types2[i]).isAssignableFrom(type1))
return true;
return false;
}
}
// ===== HIERARCHY WALKER =====================================================
/**
* Replaces simple recursive rules with a more efficient custom
* implementation that just wraps the recursive step.
*/
public static class HierarchyWalker extends AbstractQueryOptimizer {
public List optimize(List clauses, QueryContext context) {
// scan clause list looking for suitable candidates
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
if (clause instanceof PredicateClause) {
PredicateClause pclause = (PredicateClause) clause;
if (context.getRuleName() != null &&
pclause.getPredicate().getName().equals(context.getRuleName()))
continue; // can't operate inside the rule we are optimizing...
PredicateIF optimized = optimize(pclause);
if (optimized != null) {
PredicateIF predicate = pclause.getPredicate();
pclause.setPredicate(optimized);
}
} else if (clause instanceof OrClause) {
List alts = ((OrClause) clause).getAlternatives();
for (int i = 0; i < alts.size(); i++)
alts.set(i, optimize((List) alts.get(i), context));
} else if (clause instanceof NotClause) {
List notted = ((NotClause) clause).getClauses();
clause = new NotClause(optimize(notted, context));
}
}
return clauses;
}
private PredicateIF optimize(PredicateClause clause) {
// must be a rule
if (!(clause.getPredicate() instanceof RulePredicate))
return null;
RulePredicate rule = (RulePredicate) clause.getPredicate();
List clauses = rule.getClauses();
// must have just one OR clause
if (clauses.size() != 1 || !(clauses.get(0) instanceof OrClause))
return null;
// OR clause must have only two alternatives
OrClause or = (OrClause) clauses.get(0);
if (or.getAlternatives().size() != 2)
return null;
// OR clause must not be short-circuiting
if (or.getShortCircuit())
return null;
List bottom = filter((List) or.getAlternatives().get(0)); // one clause
List pump = filter((List) or.getAlternatives().get(1)); // two clauses
if (bottom.size() != 1) { // swap if needed
List tmp = bottom;
bottom = pump;
pump = tmp;
}
// one alternative must be 2 predicates, the other 1
if (bottom.size() != 1 || pump.size() != 2)
return null;
// bottom must not be a recursive reference
if (!(bottom.get(0) instanceof PredicateClause))
return null;
PredicateClause bottomclause = (PredicateClause) bottom.get(0);
if (bottomclause.getPredicate().getName().equals(rule.getName()))
return null;
// pump must be all predicates
if (!(pump.get(0) instanceof PredicateClause &&
pump.get(1) instanceof PredicateClause))
return null;
// pump must have one recursive reference
int ix;
for (ix = 0; ix < 2; ix++) {
if (pump.get(ix) instanceof PredicateClause) {
if (((PredicateClause) pump.get(ix)).getPredicate().getName().
equals(rule.getName()))
break;
}
}
if (ix >= 2)
return null; // no recursive reference found
// structure is now as follows:
// rule(...) :- {
// whatever(...), rule(...) |
// whatever(...)
// }.
// where pumpclause is the rule invocation on the first line,
// midclause is the first whatever invocation, and
// bottomclause is the second whatever invocation
PredicateClause pumpclause = (PredicateClause) pump.get(ix);
PredicateClause midclause = (PredicateClause) pump.get(1 - ix);
// verify that bottom and pump are same predicates (bug #2029)
// this avoids using a pump to walk hierarchically if the rule
// actually has the following form:
// rule(...) :- {
// foo(...) |
// bar(...), rule(...)
// }.
if (!bottomclause.getPredicate().getName()
.equals(midclause.getPredicate().getName()))
return null; // predicates don't match
// verify variables
// bottom: (PARENT, CHILD, *)
// pump: (PARENT, MID, *)
// mid: (CHILD, MID, *)
//
// PARENT and CHILD must be rule parameters; MID cannot be
// (note that this code may mix up PARENT and CHILD; it should
// still work)
Collection ruleargs = rule.getParameters();
Collection pumpvars = pumpclause.getAllVariables();
Collection midvars = midclause.getAllVariables();
Collection bottomvars = bottomclause.getAllVariables();
Variable parent = null;
Variable child = null;
Variable mid = null;
Iterator it = midvars.iterator();
while (it.hasNext()) {
Variable var = (Variable) it.next();
if (ruleargs.contains(var) &&
!pumpvars.contains(var) &&
bottomvars.contains(var) &&
midvars.contains(var)) {
if (child != null)
return null; // ambiguous variable
child = var;
}
if (!ruleargs.contains(var) &&
!bottomvars.contains(var) &&
midvars.contains(var) &&
pumpvars.contains(var)) {
if (mid != null)
return null; // ambiguous variable
mid = var;
}
}
if (child == null || mid == null)
return null; // couldn't find necessary vars
it = pumpvars.iterator();
while (it.hasNext()) {
Variable var = (Variable) it.next();
if (ruleargs.contains(var) &&
pumpvars.contains(var) &&
bottomvars.contains(var) &&
!midvars.contains(var)) {
if (parent != null)
return null; // ambiguous variable
parent = var;
}
}
if (parent == null)
return null; // couldn't find parent
// ok, we are done. now we can create the optimized version
return new HierarchyWalkerRulePredicate(rule, parent, child, mid, bottomclause);
}
/**
* INTERNAL: Removes the 'remove-duplicates' predicate from the
* lists, since this interferes with the logic that is trying to
* work out whether or not we can apply this optimization.
*/
private List filter(List clauses) {
List filtered = new ArrayList();
for (int ix = 0; ix < clauses.size(); ix++) {
Object clause = clauses.get(ix);
if (!(clause instanceof PredicateClause) ||
!(((PredicateClause) clause).getPredicate() instanceof RemoveDuplicatesPredicate))
filtered.add(clause);
}
return filtered;
}
}
// ===== STRING PREFIX SEARCHES ===============================================
// TODO
// 1) implement isBoundAt
// 2) generalize so variable can be SELECTed
// Given a query containg the two following predicates
// pred1($A, $B)
// str:starts-with($B, $C)?
// conditions for the optimization to kick in are
// 1) $A must be unbound at pred1
// 2) $B must be unbound at pred1
// 3) $B must not be used anywhere else
// 4) $C must be a literal
// 5) pred1 must be dynamic occ predicate, 'resource', or 'value'
// may have to strengthen #4 to say that $C must be a literal
/**
* INTERNAL: Optimizes queries that do lookup of occurrences by
* string value, then filter the string value by a prefix.
*/
public static class StringPrefixOptimizer extends AbstractQueryOptimizer {
public void optimize(TologQuery query, QueryContext context) {
// find str:starts-with predicate
PredicatePosition startsp = findPredicate(query.getClauses(),
StringModule.StartsWithPredicate.class);
if (startsp == null)
return;
PredicateClause starts = startsp.getClause();
// #5: find (dynamic occ | value | resource) predicate
PredicatePosition valuep = findPredicate(query.getClauses(),
DynamicOccurrencePredicate.class);
if (valuep == null)
return; // FIXME: extend to cover 'value' and 'resource'
PredicateClause value = valuep.getClause();
// #X: verify that predicates appear in same clause list
if (startsp.getContainingList() != valuep.getContainingList())
return;
// #1: arg1 must be unbound at value
Object arg1 = value.getArguments().get(0);
if (!(arg1 instanceof Variable))
return;
if (isBoundAt(query.getClauses(), (Variable) arg1, value))
return;
// #2: arg2 must be unbound at value
Object arg2 = value.getArguments().get(1);
if (!(arg2 instanceof Variable))
return;
if (isBoundAt(query.getClauses(), (Variable) arg2, value))
return;
// starts must have arg2 as first argument
if (!starts.getArguments().get(0).equals(arg2))
return;
// #3: arg2 must not be used anywhere else
Collection users = findPredicatesUsing(query.getClauses(), (Variable) arg2);
if (users.size() > 2)
return;
// #4: arg3 must be a literal
Object arg3 = starts.getArguments().get(1);
if (!(arg3 instanceof String))
return;
/// do the actual optimization
// a) if arg2 is not in the SELECT list: change arg2 to be arg3
if (!query.getSelectedVariables().contains(arg2))
value.getArguments().set(1, arg3);
// b) remove the starts clause
removePredicate(query.getClauses(), starts);
// c) inform value that it should do a prefix search
value.addArgument(new PredicateOptions((String) arg3));
}
}
// ===== HOISTING getRolesByType() INTO role-player ====================
/**
* INTERNAL: <p>Optimizes the role-player() predicate when the type of
* the role is known from a type() predicate in the same query.
* Typical usage is:</p>
*
* <pre>
* role-player($R1, fixed-point),
* association-role($A, $R1),
* association-role($A, $R2),
* $R1 /= $R2,
* role-player($R2, $OTHER),
* type($R1, required-type)?
* </pre>
*
* <p>What's really needed for the optimization to kick in is two
* things:</p>
*
* <ul>
* <li>In the role-player($A, $B) predicate $A must be unbound and
* $B must be bound
* <li>In the type($C, $D) predicate the $C must match $A, and $D
* must be bound <em>when the role-player() runs</em>, and <em>not
* by the type() predicate
* </ul>
*/
public static class AddTypeToRolePlayer extends AbstractQueryOptimizer {
public void optimize(TologQuery query, QueryContext context) {
Iterator it = findPredicates(query.getClauses(),
RolePlayerPredicate.class).iterator();
while (it.hasNext()) {
PredicatePosition rolepp = (PredicatePosition) it.next();
optimize(query, rolepp);
}
}
private void optimize(TologQuery query, PredicatePosition rolepp) {
// --- role-player predicate
// analyze predicate
PredicateClause rolec = rolepp.getClause();
Object arg1 = rolec.getArguments().get(0);
if (!(arg1 instanceof Variable) ||
isBoundAt(query.getClauses(), (Variable) arg1, rolec))
return;
Object arg2 = rolec.getArguments().get(1);
if (arg2 instanceof Variable &&
!isBoundAt(query.getClauses(), (Variable) arg2, rolec))
return;
// --- type predicate
PredicatePosition typepp = findTypePredicate(query, (Variable) arg1);
if (typepp == null)
return;
PredicateClause typec = typepp.getClause();
// INV: first arg of typec matches first of rolec
Object arg4 = typec.getArguments().get(1);
if (arg4 instanceof Variable &&
!isBoundAt(query.getClauses(), (Variable) arg1, rolec))
return;
// FIXME: verify that rolec before typec in execution order
if (rolepp.getContainingList() != typepp.getContainingList())
return;
// --- apply optimization
// 1) send PredicateOptions to role-player()
rolec.addArgument(new PredicateOptions(arg4));
// 2) remove type predicate
removePredicate(typepp.getContainingList(), typec);
}
private PredicatePosition findTypePredicate(TologQuery query, Variable arg1) {
Iterator it = findPredicatesUsing(query.getClauses(), (Variable) arg1).
iterator();
while (it.hasNext()) {
PredicatePosition pp = (PredicatePosition) it.next();
PredicateClause pc = pp.getClause();
if (pc.getPredicate() instanceof TypePredicate &&
arg1.equals(pc.getArguments().get(0)))
return pp;
}
return null;
}
}
// ===== INTERNAL UTILITIES ==================================================
private static PredicatePosition findPredicate(List clauses, Class predicate) {
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
if (clause instanceof PredicateClause) {
PredicateClause pclause = (PredicateClause) clause;
if (predicate.isInstance(pclause.getPredicate()))
return new PredicatePosition(clauses, pclause);
} else if (clause instanceof OrClause) {
List alts = ((OrClause) clause).getAlternatives();
for (int i = 0; i < alts.size(); i++)
return findPredicate((List) alts.get(i), predicate);
} else if (clause instanceof NotClause)
return findPredicate(((NotClause) clause).getClauses(), predicate);
}
return null; // couldn't find it
}
// collection of PredicatePosition objects
private static Collection findPredicates(List clauses, Class predicate) {
List pps = new ArrayList();
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
if (clause instanceof PredicateClause) {
PredicateClause pclause = (PredicateClause) clause;
if (predicate.isInstance(pclause.getPredicate()))
pps.add(new PredicatePosition(clauses, pclause));
} else if (clause instanceof OrClause) {
List alts = ((OrClause) clause).getAlternatives();
for (int i = 0; i < alts.size(); i++)
pps.addAll(findPredicates((List) alts.get(i), predicate));
} else if (clause instanceof NotClause)
pps.addAll(findPredicates(((NotClause) clause).getClauses(), predicate));
}
return pps;
}
// WARN: doesn't support dynamic association predicates
// returns collection of PredicatePosition objects
private static Collection findPredicatesUsing(List clauses, Variable var) {
Collection predicates = new ArrayList();
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
if (clause instanceof PredicateClause) {
PredicateClause pclause = (PredicateClause) clause;
if (pclause.getArguments().contains(var))
predicates.add(new PredicatePosition(clauses, pclause));
} else if (clause instanceof OrClause) {
List alts = ((OrClause) clause).getAlternatives();
for (int i = 0; i < alts.size(); i++)
predicates.addAll(findPredicatesUsing((List) alts.get(i), var));
} else if (clause instanceof NotClause)
predicates.addAll(findPredicatesUsing(((NotClause) clause).getClauses(),
var));
}
return predicates;
}
private static boolean isBoundAt(List clauses, Variable var, PredicateClause pclause) {
return isBoundAt(clauses, var, pclause, new CompactHashSet());
}
private static boolean isBoundAt(List clauses, Variable var,
PredicateClause theclause, Set bound) {
Collection predicates = new ArrayList();
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
if (clause instanceof PredicateClause) {
PredicateClause pclause = (PredicateClause) clause;
if (pclause == theclause)
return bound.contains(var);
bound.addAll(pclause.getAllVariables());
} else if (clause instanceof OrClause) {
List alts = ((OrClause) clause).getAlternatives();
for (int i = 0; i < alts.size(); i++)
if (isBoundAt((List) alts.get(i), var, theclause, bound))
return true;
} else if (clause instanceof NotClause)
if (isBoundAt(((NotClause) clause).getClauses(), var, theclause, bound))
return true;
}
return false;
}
private static boolean removePredicate(List clauses, PredicateClause pclause) {
// see if the clause is in this list, and if it is we are done
if (clauses.contains(pclause)) {
clauses.remove(pclause);
return true;
}
// traverse
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
if (clause instanceof OrClause) {
List alts = ((OrClause) clause).getAlternatives();
for (int i = 0; i < alts.size(); i++)
if (removePredicate((List) alts.get(i), pclause))
return true;
} else if (clause instanceof NotClause)
if (removePredicate(((NotClause) clause).getClauses(), pclause))
return true;
}
return false;
}
// used to capture where the predicates came from
static class PredicatePosition {
private List clauses;
private PredicateClause clause;
public PredicatePosition(List clauses, PredicateClause clause) {
this.clauses = clauses;
this.clause = clause;
}
public List getContainingList() {
return clauses;
}
public PredicateClause getClause() {
return clause;
}
}
// ===== NEXT IN SEQUENCE SEARCH =============================================
// Given a query starting with:
// occurrence-type($TOPIC, $VALUE),
// $VALUE > %value% (any comparison, literal/parameter)
//
// where the query is sorted on $VALUE and there is a limit set, but no
// offset,
//
// replace the beginning with:
// value-pumper($::OBJECT, $VALUE, %value%),
// type($::OBJECT, occurrence-type),
// occurrence($TOPIC, $::OBJECT)
//
// then look up the next values in sequence above %value% in batches
// and pump them through until enough values have been found to
// satisfy the limit.
/**
* INTERNAL: Optimizes queries that look for the next or the
* previous value in a sequence from a given start value to not load
* all values and then do it the hard way, but instead to use a
* sorted index.
*/
public static class NextPreviousOptimizer extends AbstractQueryOptimizer {
public void optimize(TologQuery query, QueryContext context) {
// ===== CHECK IF OPTIMIZATION APPLIES
// must have limit, must not have offset
if (query.getLimit() == -1 || query.getOffset() != -1)
return;
// must have order by with a single variable
List orderby = query.getOrderBy();
if (orderby.size() != 1)
return;
Variable value = (Variable) orderby.get(0);
// must start with dynamic occurrence predicate
List clauses = query.getClauses();
if (!(clauses.get(0) instanceof PredicateClause))
return;
PredicateClause dynocc = (PredicateClause) clauses.get(0);
if (!(dynocc.getPredicate() instanceof DynamicOccurrencePredicate))
return;
DynamicOccurrencePredicate dynoccpred = (DynamicOccurrencePredicate)
dynocc.getPredicate();
// second predicate must be a comparison predicate
if (clauses.size() < 2 ||
!(clauses.get(1) instanceof PredicateClause))
return;
PredicateClause compar = (PredicateClause) clauses.get(1);
PredicateIF comparpred = compar.getPredicate();
if (!((comparpred instanceof GreaterThanPredicate) ||
(comparpred instanceof GreaterThanEqualsPredicate) ||
(comparpred instanceof LessThanPredicate) ||
(comparpred instanceof LessThanEqualsPredicate)))
return;
// topic parameter to dynamic occurrence predicate must be open
if (!(dynocc.getArguments().get(0) instanceof Variable))
return;
Variable topicvar = (Variable) dynocc.getArguments().get(0);
// second parameter to dynamic occurrence predicate must be our value
if (!dynocc.getArguments().get(1).equals(value))
return;
// one parameter to comparison predicate must be our value
boolean literalIsFirst;
Object literal = null;
if (compar.getArguments().get(0).equals(value)) {
literalIsFirst = false;
literal = compar.getArguments().get(1);
} else if (compar.getArguments().get(1).equals(value)) {
literalIsFirst = true;
literal = compar.getArguments().get(0);
} else
return;
// other parameter must be a literal or a parameter
if (!((literal instanceof String) ||
(literal instanceof Parameter)))
return;
// ===== APPLY OPTIMIZATION
// figure out what kind of comparison it is
boolean bigger =
(!literalIsFirst &&
((comparpred instanceof GreaterThanPredicate) ||
(comparpred instanceof GreaterThanEqualsPredicate))) ||
(literalIsFirst &&
((comparpred instanceof LessThanPredicate) ||
(comparpred instanceof LessThanEqualsPredicate)));
boolean equals =
(comparpred instanceof LessThanEqualsPredicate) ||
(comparpred instanceof GreaterThanEqualsPredicate);
// make new wrapper predicate
Variable object = new Variable("::OBJECT");
TopicIF type = dynoccpred.getType();
PumpPredicate pumppred = new PumpPredicate(type.getTopicMap(), clauses,
query.getLimit(), value,
object, literal, equals,
bigger);
List args = new ArrayList();
args.add(literal);
args.add(value);
PredicateClause pump = new PumpClause(pumppred, args);
List newclauses = new ArrayList();
newclauses.add(pump);
query.setClauseList(newclauses);
// make type predicate clause
PredicateIF typepred = new TypePredicate(type.getTopicMap());
args = new ArrayList();
args.add(object);
args.add(type);
PredicateClause typeclause = new PredicateClause(typepred, args);
// make occurrence predicate clause
PredicateIF occpred = new OccurrencePredicate(type.getTopicMap());
args = new ArrayList();
args.add(topicvar);
args.add(object);
PredicateClause occclause = new PredicateClause(occpred, args);
// rewrite old clause list
clauses.set(0, typeclause); // replace dynocc with type predicate
clauses.set(1, occclause); // replace comparison with occurrence predicate
}
}
public static class PumpPredicate implements BasicPredicateIF {
private OccurrenceIndexIF index;
private List subclauses;
private int limit;
private Variable valuevar;
private Variable objectvar;
private Object literal;
private boolean equals;
private boolean bigger;
public PumpPredicate(TopicMapIF topicmap, List subclauses, int limit,
Variable valuevar, Variable objectvar,
Object literal, boolean equals, boolean bigger) {
this.index = (OccurrenceIndexIF) topicmap.getIndex("net.ontopia.topicmaps.core.index.OccurrenceIndexIF");
this.subclauses = subclauses;
this.limit = limit;
this.valuevar = valuevar;
this.objectvar = objectvar;
this.literal = literal;
this.equals = equals;
this.bigger = bigger;
}
public String getName() {
return "::pump-previous-next";
}
public String getSignature() throws InvalidQueryException {
return "s s";
}
public int getCost(boolean[] boundparams) {
// after the optimization, this is the only top-level predicate,
// so what we return doesn't much matter. in any case, we'll
// produce at most this.limit hits, so SMALL_RESULT seems fair
return PredicateDrivenCostEstimator.SMALL_RESULT;
}
public QueryMatches satisfy(QueryMatches input, Object[] arguments)
throws InvalidQueryException {
int valueix = input.getIndex(valuevar);
int objectix = input.getIndex(objectvar);
int batchsize = limit * 5; // multiplied by 2 before we start
QueryMatches result = new QueryMatches(input);
Iterator it;
String literalvalue;
if (literal instanceof String)
literalvalue = (String) literal;
else
literalvalue = (String) input.data[0][input.getIndex(literal)];
if (bigger)
it = index.getValuesGreaterThanOrEqual(literalvalue);
else
it = index.getValuesSmallerThanOrEqual(literalvalue);
while (result.last + 1 < limit && it.hasNext()) {
// double batch size from previous try to avoid checking too many batches
batchsize *= 2;
// set up batch of new values
QueryMatches batch = new QueryMatches(input);
batch.ensureCapacity(batchsize);
for (int ix = 0; ix < batchsize && it.hasNext(); ) {
String value = (String) it.next();
if (!equals && value.equals(literalvalue))
continue;
Iterator it2 = index.getOccurrences(value).iterator();
for (; it2.hasNext(); ix++) {
OccurrenceIF occ = (OccurrenceIF) it2.next();
if (batch.last+1 == batch.size)
batch.increaseCapacity();
Object[] newrow = (Object[]) input.data[0].clone();
batch.data[++batch.last] = newrow;
newrow[valueix] = value;
newrow[objectix] = occ;
}
}
// pump it through the subclauses
batch = AbstractQueryProcessor.satisfy(subclauses, batch);
// add output rows to result
result.add(batch);
}
return result;
}
}
public static class PumpClause extends PredicateClause {
public PumpClause(PredicateIF predicate, List arguments) {
super(predicate, arguments);
}
public List getArguments() {
Collection items = new CompactHashSet(arguments);
List clauses = ((PumpPredicate) predicate).subclauses;
for (int ix = 0; ix < clauses.size(); ix++) {
AbstractClause clause = (AbstractClause) clauses.get(ix);
items.addAll(clause.getArguments());
}
List list = new ArrayList();
list.addAll(items);
return list;
}
}
}
// UNUSED OPTIMIZATION OPPORTUNITIES
// Had to make this optimization manually. WHY?
// <logic:set name="parents">
// <tm:tolog query="selected(%product% : product, $CHILD : feature),
//+ is-version-of($CHILDV : version, $CHILD : versioned),
// parent-of($PARENT, $CHILDV),
// is-version-of($PARENTV : version, $PARENT : versioned),
// included-in($PARENTV : feature-node, %version% : platform-version),
//- is-version-of($CHILDV : version, $CHILD : versioned),
// included-in($CHILDV : feature-node, %version% : platform-version),
// not(selected(%product% : product, $PARENT : feature))?"
// rulesfile="/rules.tl"/>
// had to move is-feature() to after is-version-of manually. Shouldn't
// be necessary.
// String query =
// "value-like($TN, \"" + search + "\"), " +
// "topic-name($VERSION, $TN), " +
// "value($TN, $NAME), " +
// "is-version-of($VERSION : version, $ABSTRACT : versioned), " +
// "is-feature($ABSTRACT), " +
// "$VERSION /= %feature%, " +
// "included-in($VERSION : feature-node, %current-version% : platform-version) " +
// "order by $VERSION?";
// replace variables with literals where possible
// base-locator($A)?
// subject-identifier($A, "http:/....")?
// common clause can be lifted out
//
// select $CITY, count($OPERA) from
// { instance-of($CITY, city),
// premiere($OPERA : opera, $CITY : place)
// | instance-of($CITY, city),
// located-in($THEATRE : containee, $CITY : container),
// premiere($OPERA : opera, $THEATRE : place)
// }
// order by $OPERA desc?
// ditto for early version of commentsquery on teacher-search.jsp
// the two topic() clauses can be removed
// the query should start with userownstopic
// the not-equals should be done immediately afterwards
// String query = "select $TOPIC1, $TOPIC2 from " +
// " value($n1, $value), " +
// " value($n2, $value), " +
// " $n1 /= $n2, " +
// " topic-name($TOPIC1, $n1), " +
// " topic-name($TOPIC2, $n2), " +
// " topic($TOPIC1), " +
// " topic($TOPIC2), " +
// " $TOPIC1 /= $TOPIC2, " +
// " userownstopic($TOPIC1 : ownedtopic, @" + user.getObjectId() + " : bruker), " +
// " userownstopic($TOPIC2 : ownedtopic, @" + user.getObjectId() + " : bruker)?";
// map automatically to dynamic occurrence predicate
// occurrence($A, $O), type($O, rekkefolge), value($O, $VALUE)
// ->
// rekkefolge($A, $VALUE)
// redundant use of predicates for "typing"
//
// topic($T),
// topic-name($T, $SCOPED),
// scope($SCOPED, $SCOPE1)?
// ===== ESSAY =======================================================
// Nokia has a problem that deleting feature groups takes ages and ages,
// and this causes all sorts of difficulties on the system. We need to
// handle those difficulties at some stage, but for now, the simple fix
// is to speed up the deletion of feature groups.
// The problem is this query from the DeleteFeature action:
// // dissociate feature and descendants from current version
// query =
// "import \"rules.tl\" as fm " +
// "import \"http://psi.ontopia.net/tolog/experimental/\" as exp " +
// "select $ASSOC from " +
// " { fm:descendant-v(%featurev%, $NODE, %pv%) | exp:in($NODE, %featurev%) }, " +
// " role-player($R1, $NODE), type($R1, feature-node), " +
// " association-role($ASSOC, $R1), type($ASSOC, included-in), " +
// " association-role($ASSOC, $R2), type($R2, platform-version), " +
// " role-player($R2, %pv%)?";
// helper.delete(query);
// The optimizer decides (wrongly) that the type() predicate should go
// first, which means that the rule gets a huge result set to operate on
// when it starts, and this makes it very slow, since it has to do the
// same things over and over again.
// It's struck me before that on occasion developers will know that they
// are smarter than the optimizer, and so they should be able to override
// it. The question is how?
// So I came up with this:
// // dissociate feature and descendants from current version
// query =
// "/* #OPTION: optimizer.reorder = false */ " + // UNDOCUMENTED MAGIC... :-(
// "import \"rules.tl\" as fm " +
// "import \"http://psi.ontopia.net/tolog/experimental/\" as exp " +
// "select $ASSOC from " +
// " { fm:descendant-v(%featurev%, $NODE, %pv%) | exp:in($NODE, %featurev%) }, " +
// " role-player($R1, $NODE), type($R1, feature-node), " +
// " association-role($ASSOC, $R1), type($ASSOC, included-in), " +
// " association-role($ASSOC, $R2), type($R2, platform-version), " +
// " role-player($R2, %pv%)?";
// helper.delete(query);
// That would solve the problem for Nokia. Thoughts on this approach?
// (I'm only doing this for Nokia, and not checking it in just yet,
// anyway, so we can still change this around or take a different
// approach entirely.)
// (Below this point I'm mostly thinking out loud, but doing so helps
// me. Read it if you're interested.)
// On a different note: what struck me while writing this is that this
// problem could be avoided if tolog were a bit smarter. I'll try to
// explain. If you run the query with the type predicate first, then the
// fm:descendant-v predicate, it will work like this:
// Step 1, result set before type(...):
// +------+------+----
// | NODE | R1 | ...
// +------+------+----
// | null | null | ...
// +------+------+----
// Step 2, result set after type(...), and before fm:descendant-v(...)
// +------+------+----
// | NODE | R1 | ...
// +------+------+----
// | null | r1 | ...
// +------+------+----
// | null | r2 | ...
// +------+------+----
// | null | r3 | ...
// +------+------+----
// | null | r4 | ...
// +------+------+----
// | ... | ... | ...
// And so on, for quite a few rows downwards.
// Now, there are two important things to observe here:
// 1) fm:descendant-v(...) doesn't actually *use* the R1 column, so the
// fact that we've got a zillion different values here doesn't
// affect it; fm:descendant-v(...) is going to do its thing on the
// NODE column, over and over again repeating the same operation on
// an unbound value
// 2) fm:descendant-v(...) is a rule, which means that it will start by
// translating the result set from step 2 into an internal result
// set, then do its thing, then translate it back to an external
// result set
// In other words, if fm:descendant-v(...) collapsed the duplicate rows
// in the internal result set before doing its thing, then did a
// cartesian product when creating the external result at the end we
// would avoid repeating the same calculation thousands of times. The
// question is, I suppose, how to know when this is a useful thing to do,
// and when it would just be a waste of time. It might be that we want to
// add an additional optimizer for this case.
// ======================================================================
// denne tar 6500 msec
// select $FELTVERDI from
// har-asstype-rep(%FELT%: objekt, $ASSTYPE: feltverdi),
// type($ASSOSIASJON, $ASSTYPE),
// type($ROLLE1, objekt),
// role-player($ROLLE1, %OBJEKT%),
// association-role($ASSOSIASJON, $ROLLE1),
// association-role($ASSOSIASJON, $ROLLE2),
// role-player($ROLLE2, $FELTVERDI),
// $ROLLE1 /= $ROLLE2
// order by $FELTVERDI?
// denne tar 80 msec
// select $FELTVERDI from
// har-asstype-rep(%FELT%: objekt, $ASSTYPE: feltverdi),
// type($ASSOSIASJON, $ASSTYPE),
// role-player($ROLLE1, %OBJEKT%),
// association-role($ASSOSIASJON, $ROLLE1),
// association-role($ASSOSIASJON, $ROLLE2),
// role-player($ROLLE2, $FELTVERDI),
// $ROLLE1 /= $ROLLE2
// order by $FELTVERDI?
// forskjellen er at "type($ROLLE1, objekt)" er fjernet. grunnen til
// at dette gjoer saa stor forskjell er at den kjoeres foer linjen etter,
// som binder $ROLLE1 til mye faerre verdier. derfor skulle
// role-player() kjoert foer type(), men det gjoer den ikke. jaja.
// ==================================================
// | select $TYPE, $TOPIC, $ID from
// | {
// | topic-name($TOPIC, $NAME), value-like($NAME, %SEARCHITEM%) |
// | occurrence($TOPIC, $OCC), value-like($OCC, %SEARCHITEM%) |
// | topic-name($TOPIC, $TN), variant($TN, $V), value-like($V, %SEARCHITEM%)
// | },
// | instance-of($TOPIC, $TYPE),
// | object-id($TYPE, $ID)
// | order by $TYPE, $TOPIC?
// |
// | I made several experiments in the Omnigator, which I did not made
// | before I wrote this support ticket (sorry!), and I found out that
// | the "instance-of" and "object-id" make the query slow (betwenn two
// | and four seconds).
//
// I think I see what your problem is. Most likely the optimizer screws
// this query up for you and runs it like this
//
// instance-of
// object-id
// { OR clause }
//
// with the result being that you get all instances of all topic types,
// and then do the full-text search three times (once in each OR branch)
// for each instance/type combination.
// IDEA: evaluate OR clauses on the worst of the first clauses inside
// each OR branch. would here keep the OR first
// ==================================================
// reordering screws this one up somehow
// direct-instance-of(%OBJEKT%, $OBJEKTKLASSE),
// arver-felt-fra($OBJEKTKLASSE : objekt, $FELTKILDE : feltkilde),
// role-player($ROLLE1, %OBJEKT%),
// association-role($ASSOSIASJON, $ROLLE1),
// association-role($ASSOSIASJON, $ROLLE2),
// role-player($ROLLE2, $FELTKILDEINSTANS),
// $FELTKILDEINSTANS /= %OBJEKT%,
// direct-instance-of($FELTKILDEINSTANS, $FELTKILDE)?
// ==================================================
// reordering also screws this one up
// select $GRUPPE, $FELT, $TYPE, $FV, $FORMAT, $ELEMGRUPPENAVN, $ELEMNAVN, $SKJULT, $GRUPPESORTERING, $FELTSORTERING from
// {
// direct-instance-of(%OBJEKT%, $OBJEKTKLASSE),
// {
// har-felt($OBJEKTKLASSE : objekt, $FELT : feltverdi)
// |
// har-arvelig-felt(%FELTKILDEINSTANS% : objekt, $FELT : feltverdi)
// }
// |
// har-formatversjon(%OBJEKT% : publikasjon, $FV : formatversjon),
// har-format($FV : objekt, $FORMAT : feltverdi),
// direct-instance-of($FV, formatversjon),
// {
// har-felt(formatversjon : objekt, $FELT : feltverdi)
// |
// har-arvelig-felt($FORMAT : objekt, $FELT : feltverdi)
// }
// },
// har-feltgruppe($FELT : objekt, $GRUPPE : feltverdi),
// har-felttype($FELT : objekt, $TYPE : feltverdi),
// xmltag($FELT, $ELEMNAVN),
// { xmlgruppe($FELT, $ELEMGRUPPENAVN) },
// { er-skjult-felt($FELT : objekt, $SKJULT : feltverdi) },
// { feltrekkefolge($GRUPPE, $GRUPPESORTERING) },
// { feltrekkefolge($FELT, $FELTSORTERING) }
// order by $GRUPPESORTERING, $FELTSORTERING ?
// ==================================================
// screwed up by reordering
// select $GRUPPE, $FELT, $TYPE, $BESKRIVELSE, $GRUPPESORTERING, $FELTSORTERING from
// direct-instance-of(%OBJEKT%, $OBJEKTKLASSE),
// {
// har-felt($OBJEKTKLASSE : objekt, $FELT : feltverdi)
// |
// arver-felt-fra($OBJEKTKLASSE : objekt, $FELTKILDE : feltkilde),
// role-player($ROLLE1, %OBJEKT%),
// association-role($ASSOSIASJON, $ROLLE1),
// association-role($ASSOSIASJON, $ROLLE2),
// role-player($ROLLE2, $FELTKILDEINSTANS),
// $FELTKILDEINSTANS /= %OBJEKT%,
// direct-instance-of($FELTKILDEINSTANS, $FELTKILDE),
// har-arvelig-felt($FELTKILDEINSTANS : objekt, $FELT : feltverdi)
// },
// har-feltgruppe($FELT : objekt, $GRUPPE : feltverdi),
// har-felttype($FELT : objekt, $TYPE : feltverdi),
// { beskrivelse($FELT, $BESKRIVELSE) },
// { feltrekkefolge($GRUPPE, $GRUPPESORTERING) },
// { feltrekkefolge($FELT, $FELTSORTERING) }
// order by $GRUPPESORTERING, $FELTSORTERING ?
// ==================================================
// broken by reordering
// select $FELTVERDI, $ASSOSIASJONSTYPE from
// har-asstype-rep(%FELT% : objekt, $ASSOSIASJONSTYPE : feltverdi),
// role-player($ROLLE1, %OBJEKT%),
// association-role($ASSOSIASJON, $ROLLE1),
// type($ASSOSIASJON, $ASSOSIASJONSTYPE),
// association-role($ASSOSIASJON, $ROLLE2),
// role-player($ROLLE2, $FELTVERDI),
// $ROLLE1 /= $ROLLE2
// order by $ASSOSIASJONSTYPE, $FELTVERDI?
// ==================================================
// reordering doesn't see what best choice is
// using FTM for i"http://cch.com/xtm/fedtax/#"
// instance-of($DOC, FTM:document),
// FTM:normval($DOC, "2790-New"),
// FTM:pub($DOC, "MTG")?
// the instance-of gives some 100k topics
// the normval gives 3
// ==================================================
// switching order of roles in Falls_under takes query time from
// 3.6 sec to 0.12 secs...
// using dlo for i"http://test.greenwood.com/DLOXTM/"
// Select $CONTENT, $CONTENTID from
// dlo:Available_in(@47677 : dlo:MODULE, $CONTENT : dlo:CONTENT_OBJECT),
// dlo:Falls_under($CONTENT : dlo:CONTENT_OBJECT, @92153 : dlo:HEADING),
// dlo:contentID($CONTENT, $CONTENTID),
// not(instance-of($CONTENT, dlo:Related_Content_Object))
// order by $CONTENT LIMIT 20?