From f5a69fbf5e7e343d094687263604ce18b7b6dae5 Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Sat, 30 Sep 2006 23:07:46 +0000 Subject: Finished join optimization. --- .../carbonado/cursor/JoinedCursorFactory.java | 467 --------------- .../carbonado/cursor/MultiTransformedCursor.java | 1 - .../amazon/carbonado/cursor/TransformedCursor.java | 1 - .../com/amazon/carbonado/qe/CompositeScore.java | 38 +- .../qe/DelegatedQueryExecutorFactory.java | 51 ++ .../amazon/carbonado/qe/FullScanQueryExecutor.java | 6 + .../amazon/carbonado/qe/IndexedQueryAnalyzer.java | 130 +++-- .../amazon/carbonado/qe/IndexedQueryExecutor.java | 32 +- .../amazon/carbonado/qe/IterableQueryExecutor.java | 2 +- .../amazon/carbonado/qe/JoinedQueryExecutor.java | 643 ++++++++++++++++----- .../com/amazon/carbonado/qe/KeyQueryExecutor.java | 6 + .../java/com/amazon/carbonado/qe/OrderingList.java | 17 + .../java/com/amazon/carbonado/qe/QueryEngine.java | 12 +- .../amazon/carbonado/qe/QueryExecutorFactory.java | 2 - .../amazon/carbonado/qe/SortedQueryExecutor.java | 7 + .../com/amazon/carbonado/qe/StorageAccess.java | 5 + .../carbonado/repo/indexed/IndexedStorage.java | 5 + .../com/amazon/carbonado/spi/CodeBuilderUtil.java | 6 + .../carbonado/qe/TestIndexedQueryAnalyzer.java | 179 +++++- .../carbonado/qe/TestIndexedQueryExecutor.java | 119 ++-- .../carbonado/qe/TestJoinedQueryExecutor.java | 129 ++++- .../com/amazon/carbonado/qe/TestOrderingList.java | 15 + .../carbonado/qe/TestUnionQueryAnalyzer.java | 12 +- 23 files changed, 1107 insertions(+), 778 deletions(-) delete mode 100644 src/main/java/com/amazon/carbonado/cursor/JoinedCursorFactory.java create mode 100644 src/main/java/com/amazon/carbonado/qe/DelegatedQueryExecutorFactory.java (limited to 'src') diff --git a/src/main/java/com/amazon/carbonado/cursor/JoinedCursorFactory.java b/src/main/java/com/amazon/carbonado/cursor/JoinedCursorFactory.java deleted file mode 100644 index b689c7e..0000000 --- a/src/main/java/com/amazon/carbonado/cursor/JoinedCursorFactory.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright 2006 Amazon Technologies, Inc. or its affiliates. - * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks - * of Amazon Technologies, Inc. or its affiliates. All rights reserved. - * - * 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 com.amazon.carbonado.cursor; - -import java.lang.reflect.Field; - -import java.util.Map; - -import org.cojen.classfile.ClassFile; -import org.cojen.classfile.CodeBuilder; -import org.cojen.classfile.FieldInfo; -import org.cojen.classfile.Label; -import org.cojen.classfile.LocalVariable; -import org.cojen.classfile.MethodInfo; -import org.cojen.classfile.Modifiers; -import org.cojen.classfile.TypeDesc; - -import org.cojen.util.ClassInjector; -import org.cojen.util.KeyFactory; -import org.cojen.util.SoftValuedHashMap; -import org.cojen.util.WeakIdentityMap; - -import com.amazon.carbonado.Cursor; -import com.amazon.carbonado.FetchException; -import com.amazon.carbonado.Query; -import com.amazon.carbonado.Repository; -import com.amazon.carbonado.RepositoryException; -import com.amazon.carbonado.Storable; -import com.amazon.carbonado.Storage; -import com.amazon.carbonado.SupportException; - -import com.amazon.carbonado.info.ChainedProperty; -import com.amazon.carbonado.info.StorableInfo; -import com.amazon.carbonado.info.StorableIntrospector; -import com.amazon.carbonado.info.StorableProperty; - -import com.amazon.carbonado.util.QuickConstructorGenerator; - -import com.amazon.carbonado.spi.CodeBuilderUtil; -import static com.amazon.carbonado.spi.CommonMethodNames.*; - -/** - * Given two joined types source and target, this factory - * converts a cursor over type source into a cursor over type - * target. For example, consider two storable types, Employer and - * Person. A query filter for persons with a given employer might look like: - * {@code "employer.name = ?" } The join can be manually implemented by - * querying for employers (the source) and then using this factory to produce - * persons (the target): - * - *
- * JoinedCursorFactory<Employer, Person> factory = new JoinedCursorFactory<Employer, Person>
- *     (repo, Person.class, "employer", Employer.class);
- *
- * Cursor<Employer> employerCursor = repo.storageFor(Employer.class)
- *     .query("name = ?").with(...).fetch();
- *
- * Cursor<Person> personCursor = factory.join(employerCursor);
- * 
- * - * Chained properties are supported as well. A query filter for persons with an - * employer in a given state might look like: {@code "employer.address.state = ?" } - * The join can be manually implemented as: - * - *
- * JoinedCursorFactory<Address, Person> factory = new JoinedCursorFactory<Address, Person>
- *     (repo, Person.class, "employer.address", Address.class);
- *
- * Cursor<Address> addressCursor = repo.storageFor(Address.class)
- *     .query("state = ?").with(...).fetch();
- *
- * Cursor<Person> personCursor = factory.join(addressCursor);
- * 
- * - * @author Brian S O'Neill - * @see TransformedCursor - * @see MultiTransformedCursor - * @param source type, can be anything - * @param target type, must be a Storable - */ -public class JoinedCursorFactory { - private static final String STORAGE_FIELD_NAME = "storage"; - private static final String QUERY_FIELD_NAME = "query"; - private static final String QUERY_FILTER_FIELD_NAME = "queryFilter"; - private static final String ACTIVE_SOURCE_FIELD_NAME = "active"; - - private static final Map cJoinerCursorClassCache; - - static { - cJoinerCursorClassCache = new SoftValuedHashMap(); - } - - private static synchronized Joiner - newBasicJoiner(StorableProperty targetToSourceProperty, Storage targetStorage) - throws FetchException - { - Class sourceType = targetToSourceProperty.getType(); - - final Object key = KeyFactory.createKey - (new Object[] {sourceType, - targetToSourceProperty.getEnclosingType(), - targetToSourceProperty.getName()}); - - Class clazz = cJoinerCursorClassCache.get(key); - - if (clazz == null) { - clazz = generateBasicJoinerCursor(sourceType, targetToSourceProperty); - cJoinerCursorClassCache.put(key, clazz); - } - - // Transforming cursor class may need a Query to operate on. - Query targetQuery = null; - try { - String filter = (String) clazz.getField(QUERY_FILTER_FIELD_NAME).get(null); - targetQuery = targetStorage.query(filter); - } catch (NoSuchFieldException e) { - } catch (IllegalAccessException e) { - } - - BasicJoiner.Factory factory = (BasicJoiner.Factory) QuickConstructorGenerator - .getInstance(clazz, BasicJoiner.Factory.class); - - return new BasicJoiner(factory, targetStorage, targetQuery); - } - - private static Class> - generateBasicJoinerCursor(Class sourceType, StorableProperty targetToSourceProperty) - { - final int propCount = targetToSourceProperty.getJoinElementCount(); - - // Determine if join is one-to-one, in which case slightly more optimal - // code can be generated. - boolean isOneToOne = true; - for (int i=0; i targetType = targetToSourceProperty.getEnclosingType(); - - String packageName; - { - String name = targetType.getName(); - int index = name.lastIndexOf('.'); - if (index >= 0) { - packageName = name.substring(0, index); - } else { - packageName = ""; - } - } - - ClassLoader loader = targetType.getClassLoader(); - - ClassInjector ci = ClassInjector.create(packageName + ".JoinedCursor", loader); - Class superclass = isOneToOne ? TransformedCursor.class : MultiTransformedCursor.class; - ClassFile cf = new ClassFile(ci.getClassName(), superclass); - cf.markSynthetic(); - cf.setSourceFile(JoinedCursorFactory.class.getName()); - cf.setTarget("1.5"); - - final TypeDesc queryType = TypeDesc.forClass(Query.class); - final TypeDesc cursorType = TypeDesc.forClass(Cursor.class); - final TypeDesc storageType = TypeDesc.forClass(Storage.class); - final TypeDesc storableType = TypeDesc.forClass(Storable.class); - - if (isOneToOne) { - cf.addField(Modifiers.PRIVATE.toFinal(true), STORAGE_FIELD_NAME, storageType); - } else { - // Field to hold query which fetches type T. - cf.addField(Modifiers.PRIVATE.toFinal(true), QUERY_FIELD_NAME, queryType); - } - - boolean canSetSourceReference = targetToSourceProperty.getWriteMethod() != null; - - if (canSetSourceReference && !isOneToOne) { - // Field to hold active S storable. - cf.addField(Modifiers.PRIVATE, ACTIVE_SOURCE_FIELD_NAME, - TypeDesc.forClass(sourceType)); - } - - // Constructor accepts a Storage and Query, but Storage is only used - // for one-to-one, and Query is only used for one-to-many. - { - MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, - new TypeDesc[] {cursorType, storageType, queryType}); - CodeBuilder b = new CodeBuilder(mi); - - b.loadThis(); - b.loadLocal(b.getParameter(0)); // pass S cursor to superclass - b.invokeSuperConstructor(new TypeDesc[] {cursorType}); - - if (isOneToOne) { - b.loadThis(); - b.loadLocal(b.getParameter(1)); // push T storage to stack - b.storeField(STORAGE_FIELD_NAME, storageType); - } else { - b.loadThis(); - b.loadLocal(b.getParameter(2)); // push T query to stack - b.storeField(QUERY_FIELD_NAME, queryType); - } - - b.returnVoid(); - } - - // For one-to-many, a query is needed. Save the query filter in a - // public static field to be grabbed later. - if (!isOneToOne) { - StringBuilder queryBuilder = new StringBuilder(); - - for (int i=0; i 0) { - queryBuilder.append(" & "); - } - queryBuilder.append(targetToSourceProperty.getInternalJoinElement(i).getName()); - queryBuilder.append(" = ?"); - } - - FieldInfo fi = cf.addField(Modifiers.PUBLIC.toStatic(true).toFinal(true), - QUERY_FILTER_FIELD_NAME, TypeDesc.STRING); - fi.setConstantValue(queryBuilder.toString()); - } - - // Implement the transform method. - if (isOneToOne) { - MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, "transform", TypeDesc.OBJECT, - new TypeDesc[] {TypeDesc.OBJECT}); - mi.addException(TypeDesc.forClass(FetchException.class)); - CodeBuilder b = new CodeBuilder(mi); - - LocalVariable sourceVar = b.createLocalVariable(null, storableType); - b.loadLocal(b.getParameter(0)); - b.checkCast(TypeDesc.forClass(sourceType)); - b.storeLocal(sourceVar); - - // Prepare T storable. - b.loadThis(); - b.loadField(STORAGE_FIELD_NAME, storageType); - b.invokeInterface(storageType, PREPARE_METHOD_NAME, storableType, null); - LocalVariable targetVar = b.createLocalVariable(null, storableType); - b.checkCast(TypeDesc.forClass(targetType)); - b.storeLocal(targetVar); - - // Copy pk property values from S to T. - for (int i=0; i internal = targetToSourceProperty.getInternalJoinElement(i); - StorableProperty external = targetToSourceProperty.getExternalJoinElement(i); - - b.loadLocal(targetVar); - b.loadLocal(sourceVar); - b.invoke(external.getReadMethod()); - b.invoke(internal.getWriteMethod()); - } - - // tryLoad target. - b.loadLocal(targetVar); - b.invokeInterface(storableType, TRY_LOAD_METHOD_NAME, TypeDesc.BOOLEAN, null); - Label wasLoaded = b.createLabel(); - b.ifZeroComparisonBranch(wasLoaded, "!="); - - b.loadNull(); - b.returnValue(storableType); - - wasLoaded.setLocation(); - - if (canSetSourceReference) { - b.loadLocal(targetVar); - b.loadLocal(sourceVar); - b.invoke(targetToSourceProperty.getWriteMethod()); - } - - b.loadLocal(targetVar); - b.returnValue(storableType); - } else { - MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, "transform", cursorType, - new TypeDesc[] {TypeDesc.OBJECT}); - mi.addException(TypeDesc.forClass(FetchException.class)); - CodeBuilder b = new CodeBuilder(mi); - - LocalVariable sourceVar = b.createLocalVariable(null, storableType); - b.loadLocal(b.getParameter(0)); - b.checkCast(TypeDesc.forClass(sourceType)); - b.storeLocal(sourceVar); - - if (canSetSourceReference) { - b.loadThis(); - b.loadLocal(sourceVar); - b.storeField(ACTIVE_SOURCE_FIELD_NAME, TypeDesc.forClass(sourceType)); - } - - // Populate query parameters. - b.loadThis(); - b.loadField(QUERY_FIELD_NAME, queryType); - - for (int i=0; i external = targetToSourceProperty.getExternalJoinElement(i); - b.loadLocal(sourceVar); - b.invoke(external.getReadMethod()); - - TypeDesc bindType = CodeBuilderUtil.bindQueryParam(external.getType()); - CodeBuilderUtil.convertValue(b, external.getType(), bindType.toClass()); - b.invokeInterface(queryType, WITH_METHOD_NAME, queryType, - new TypeDesc[] {bindType}); - } - - // Now fetch and return. - b.invokeInterface(queryType, FETCH_METHOD_NAME, cursorType, null); - b.returnValue(cursorType); - } - - if (canSetSourceReference && !isOneToOne) { - // Override the "next" method to set S object on T. - MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, "next", TypeDesc.OBJECT, null); - mi.addException(TypeDesc.forClass(FetchException.class)); - CodeBuilder b = new CodeBuilder(mi); - - b.loadThis(); - b.invokeSuper(TypeDesc.forClass(MultiTransformedCursor.class), - "next", TypeDesc.OBJECT, null); - b.checkCast(TypeDesc.forClass(targetType)); - b.dup(); - - b.loadThis(); - b.loadField(ACTIVE_SOURCE_FIELD_NAME, TypeDesc.forClass(sourceType)); - b.invoke(targetToSourceProperty.getWriteMethod()); - - b.returnValue(storableType); - } - - return (Class>) ci.defineClass(cf); - } - - private final Joiner mJoiner; - - /** - * @param repo access to storage instances for properties - * @param targetType type of target instances - * @param targetToSourceProperty property of target type which maps - * to instances of source type. - * @param sourceType type of source instances - * @throws IllegalArgumentException if property type is not source - */ - public JoinedCursorFactory(Repository repo, - Class targetType, - String targetToSourceProperty, - Class sourceType) - throws SupportException, FetchException, RepositoryException - { - this(repo, - ChainedProperty.parse(StorableIntrospector.examine(targetType), - targetToSourceProperty), - sourceType); - } - - /** - * @param repo access to storage instances for properties - * @param targetToSourceProperty property of target type which maps - * to instances of source type. - * @param sourceType type of source instances - * @throws IllegalArgumentException if property type is not source - */ - public JoinedCursorFactory(Repository repo, - ChainedProperty targetToSourceProperty, - Class sourceType) - throws SupportException, FetchException, RepositoryException - { - if (targetToSourceProperty.getType() != sourceType) { - throw new IllegalArgumentException - ("Property is not of type \"" + sourceType.getName() + "\": " + - targetToSourceProperty); - } - - StorableProperty primeTarget = targetToSourceProperty.getPrimeProperty(); - Storage primeTargetStorage = repo.storageFor(primeTarget.getEnclosingType()); - - Joiner joiner = newBasicJoiner(primeTarget, primeTargetStorage); - - int chainCount = targetToSourceProperty.getChainCount(); - for (int i=0; i) joiner; - } - - /** - * Given a cursor over type source, returns a new cursor over joined - * property of type target. - */ - public Cursor join(Cursor cursor) { - return mJoiner.join(cursor); - } - - private static interface Joiner { - Cursor join(Cursor cursor); - } - - /** - * Support for joins without an intermediate hop. - */ - private static class BasicJoiner implements Joiner { - private final Factory mJoinerFactory; - private final Storage mTargetStorage; - private final Query mTargetQuery; - - BasicJoiner(Factory factory, Storage targetStorage, Query targetQuery) { - mJoinerFactory = factory; - mTargetStorage = targetStorage; - mTargetQuery = targetQuery; - } - - public Cursor join(Cursor cursor) { - return mJoinerFactory.newJoinedCursor(cursor, mTargetStorage, mTargetQuery); - } - - /** - * Needs to be public for {@link QuickConstructorGenerator}. - */ - public static interface Factory { - Cursor newJoinedCursor(Cursor cursor, - Storage targetStorage, Query targetQuery); - } - } - - /** - * Support for joins with an intermediate hop -- multi-way joins. - */ - private static class MultiJoiner - implements Joiner - { - private final Joiner mSourceToMid; - private final Joiner mMidToTarget; - - MultiJoiner(Joiner sourceToMidJoiner, Joiner midToTargetJoiner) { - mSourceToMid = sourceToMidJoiner; - mMidToTarget = midToTargetJoiner; - } - - public Cursor join(Cursor cursor) { - return mMidToTarget.join(mSourceToMid.join(cursor)); - } - } -} diff --git a/src/main/java/com/amazon/carbonado/cursor/MultiTransformedCursor.java b/src/main/java/com/amazon/carbonado/cursor/MultiTransformedCursor.java index 3f06421..7606eab 100644 --- a/src/main/java/com/amazon/carbonado/cursor/MultiTransformedCursor.java +++ b/src/main/java/com/amazon/carbonado/cursor/MultiTransformedCursor.java @@ -31,7 +31,6 @@ import com.amazon.carbonado.FetchInterruptedException; * joins. * * @author Brian S O'Neill - * @see JoinedCursorFactory * @param source type, can be anything * @param target type, can be anything */ diff --git a/src/main/java/com/amazon/carbonado/cursor/TransformedCursor.java b/src/main/java/com/amazon/carbonado/cursor/TransformedCursor.java index 428e74e..70161cc 100644 --- a/src/main/java/com/amazon/carbonado/cursor/TransformedCursor.java +++ b/src/main/java/com/amazon/carbonado/cursor/TransformedCursor.java @@ -30,7 +30,6 @@ import com.amazon.carbonado.FetchInterruptedException; * one-to-one joins. Use {@link MultiTransformedCursor} for one-to-many joins. * * @author Brian S O'Neill - * @see JoinedCursorFactory * @param source type, can be anything * @param target type, can be anything */ diff --git a/src/main/java/com/amazon/carbonado/qe/CompositeScore.java b/src/main/java/com/amazon/carbonado/qe/CompositeScore.java index 6d0fad5..eba6106 100644 --- a/src/main/java/com/amazon/carbonado/qe/CompositeScore.java +++ b/src/main/java/com/amazon/carbonado/qe/CompositeScore.java @@ -91,6 +91,18 @@ public class CompositeScore { return new CompositeScore(filteringScore, orderingScore); } + /** + * Returns a partial comparator suited for comparing local indexes to + * foreign indexes. It determines which CompositeScores are better by + * examining identity matches, range matches and ordering. It does not + * matter if the scores were evaluated for different indexes or storable + * types. The comparator returns {@code <0} if first score is better, + * {@code 0} if equal, or {@code >0} if second is better. + */ + public static Comparator> localForeignComparator() { + return Comp.LOCAL_FOREIGN; + } + /** * Returns a comparator which determines which CompositeScores are * better. It compares identity matches, range matches, ordering, open @@ -100,7 +112,7 @@ public class CompositeScore { * {@code 0} if equal, or {@code >0} if second is better. */ public static Comparator> fullComparator() { - return Full.INSTANCE; + return Comp.FULL; } private final FilteringScore mFilteringScore; @@ -157,8 +169,15 @@ public class CompositeScore { return "CompositeScore {" + getFilteringScore() + ", " + getOrderingScore() + '}'; } - private static class Full implements Comparator> { - static final Comparator> INSTANCE = new Full(); + private static class Comp implements Comparator> { + static final Comparator> LOCAL_FOREIGN = new Comp(false); + static final Comparator> FULL = new Comp(true); + + private final boolean mFull; + + private Comp(boolean full) { + mFull = full; + } public int compare(CompositeScore first, CompositeScore second) { int result = FilteringScore.nullCompare(first, second); @@ -189,7 +208,18 @@ public class CompositeScore { } } - result = FilteringScore.fullComparator().compare(firstScore, secondScore); + if (mFull) { + result = FilteringScore.fullComparator().compare(firstScore, secondScore); + } else { + // Favor index that has any matches. + if (firstScore.hasAnyMatches()) { + if (!secondScore.hasAnyMatches()) { + return -1; + } + } else if (secondScore.hasAnyMatches()) { + return 1; + } + } return result; } diff --git a/src/main/java/com/amazon/carbonado/qe/DelegatedQueryExecutorFactory.java b/src/main/java/com/amazon/carbonado/qe/DelegatedQueryExecutorFactory.java new file mode 100644 index 0000000..fae22b4 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/qe/DelegatedQueryExecutorFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * 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 com.amazon.carbonado.qe; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; + +import com.amazon.carbonado.filter.Filter; + +/** + * QueryExecutorFactory which produces executors which delegate via {@link DelegatedQueryExecutor}. + * + * @author Brian S O'Neill + */ +public class DelegatedQueryExecutorFactory implements QueryExecutorFactory { + private final Storage mStorage; + + public DelegatedQueryExecutorFactory(Storage rootStorage) { + if (rootStorage == null) { + throw new IllegalArgumentException(); + } + mStorage = rootStorage; + } + + public Class getStorableType() { + return mStorage.getStorableType(); + } + + public QueryExecutor executor(Filter filter, OrderingList ordering) + throws FetchException + { + return new DelegatedQueryExecutor(mStorage, filter, ordering); + } +} diff --git a/src/main/java/com/amazon/carbonado/qe/FullScanQueryExecutor.java b/src/main/java/com/amazon/carbonado/qe/FullScanQueryExecutor.java index b8035f0..f0e4517 100644 --- a/src/main/java/com/amazon/carbonado/qe/FullScanQueryExecutor.java +++ b/src/main/java/com/amazon/carbonado/qe/FullScanQueryExecutor.java @@ -45,6 +45,9 @@ public class FullScanQueryExecutor extends AbstractQueryExec * @throws IllegalArgumentException if support is null */ public FullScanQueryExecutor(Support support) { + if (support == null && this instanceof Support) { + support = (Support) this; + } if (support == null) { throw new IllegalArgumentException(); } @@ -88,6 +91,9 @@ public class FullScanQueryExecutor extends AbstractQueryExec return true; } + /** + * Provides support for {@link FullScanQueryExecutor}. + */ public static interface Support { Class getStorableType(); diff --git a/src/main/java/com/amazon/carbonado/qe/IndexedQueryAnalyzer.java b/src/main/java/com/amazon/carbonado/qe/IndexedQueryAnalyzer.java index dfa76ac..219400c 100644 --- a/src/main/java/com/amazon/carbonado/qe/IndexedQueryAnalyzer.java +++ b/src/main/java/com/amazon/carbonado/qe/IndexedQueryAnalyzer.java @@ -90,26 +90,35 @@ public class IndexedQueryAnalyzer { throw new IllegalArgumentException("Filter must be bound"); } - final Comparator> comparator = CompositeScore.fullComparator(); - // First find best local index. - CompositeScore bestScore = null; + CompositeScore bestLocalScore = null; StorableIndex bestLocalIndex = null; + final Comparator> fullComparator = CompositeScore.fullComparator(); + Collection> localIndexes = indexesFor(getStorableType()); if (localIndexes != null) { for (StorableIndex index : localIndexes) { CompositeScore candidateScore = CompositeScore.evaluate(index, filter, ordering); - if (bestScore == null || comparator.compare(candidateScore, bestScore) < 0) { - bestScore = candidateScore; + if (bestLocalScore == null + || fullComparator.compare(candidateScore, bestLocalScore) < 0) + { + bestLocalScore = candidateScore; bestLocalIndex = index; } } } - // Now try to find better foreign index. + // Now try to find best foreign index. + + if (bestLocalScore.getFilteringScore().isKeyMatch()) { + // Don't bother checking foreign indexes. The local one is perfect. + return new Result(filter, bestLocalScore, bestLocalIndex, null, null); + } + + CompositeScore bestForeignScore = null; StorableIndex bestForeignIndex = null; ChainedProperty bestForeignProperty = null; @@ -134,20 +143,47 @@ public class IndexedQueryAnalyzer { filter, ordering); - if (bestScore == null || comparator.compare(candidateScore, bestScore) < 0) { - bestScore = candidateScore; - bestLocalIndex = null; + if (bestForeignScore == null + || fullComparator.compare(candidateScore, bestForeignScore) < 0) + { + bestForeignScore = candidateScore; bestForeignIndex = index; bestForeignProperty = foreignIndexes.mProperty; } } } - return new Result(filter, - bestScore, - bestLocalIndex, - bestForeignIndex, - bestForeignProperty); + // Check if foreign index is better than local index. + + if (bestLocalScore != null && bestForeignScore != null) { + // When comparing local index to foreign index, use a slightly less + // discriminating comparator, to prevent foreign indexes from + // looking too good. + + Comparator> comp = CompositeScore.localForeignComparator(); + + if (comp.compare(bestForeignScore, bestLocalScore) < 0) { + // Foreign is better. + bestLocalScore = null; + } else { + // Local is better. + bestForeignScore = null; + } + } + + CompositeScore bestScore; + + if (bestLocalScore != null) { + bestScore = bestLocalScore; + bestForeignIndex = null; + bestForeignProperty = null; + } else { + bestScore = bestForeignScore; + bestLocalIndex = null; + } + + return new Result + (filter, bestScore, bestLocalIndex, bestForeignIndex, bestForeignProperty); } /** @@ -233,23 +269,6 @@ public class IndexedQueryAnalyzer { return false; } - private boolean simpleAnalyze(Filter filter) - throws SupportException, RepositoryException - { - Collection> indexes = indexesFor(filter.getStorableType()); - - if (indexes != null) { - for (StorableIndex index : indexes) { - FilteringScore score = FilteringScore.evaluate(index, filter); - if (score.getRemainderCount() == 0) { - return true; - } - } - } - - return false; - } - private Collection> indexesFor(Class type) throws SupportException, RepositoryException { @@ -363,7 +382,7 @@ public class IndexedQueryAnalyzer { /** * Returns the simple or chained property that maps to the selected * foreign index. Returns null if foreign index was not selected. This - * property corresponds to the "bToAProperty" of {@link + * property corresponds to the "targetToSourceProperty" of {@link * JoinedQueryExecutor}. */ public ChainedProperty getForeignProperty() { @@ -475,7 +494,12 @@ public class IndexedQueryAnalyzer { } } - QueryExecutor executor = baseExecutor(localAccess); + QueryExecutor executor = baseLocalExecutor(localAccess); + + if (executor == null) { + return JoinedQueryExecutor.build + (mRepoAccess, getForeignProperty(), getFilter(), getOrdering()); + } Filter remainderFilter = getRemainderFilter(); if (remainderFilter != null) { @@ -494,9 +518,10 @@ public class IndexedQueryAnalyzer { return executor; } - private QueryExecutor baseExecutor(StorageAccess localAccess) - throws SupportException, FetchException, RepositoryException - { + /** + * Returns local executor or null if foreign executor should be used. + */ + private QueryExecutor baseLocalExecutor(StorageAccess localAccess) { if (!handlesAnything()) { return new FullScanQueryExecutor(localAccess); } @@ -504,34 +529,15 @@ public class IndexedQueryAnalyzer { StorableIndex localIndex = getLocalIndex(); if (localIndex != null) { - return indexExecutor(localAccess, localIndex); - } - - StorableIndex foreignIndex = getForeignIndex(); - StorageAccess foreignAccess = mRepoAccess - .storageAccessFor(foreignIndex.getStorableType()); - - QueryExecutor foreignExecutor; - Storage delegate = foreignAccess.storageDelegate(foreignIndex); - if (delegate != null) { - foreignExecutor = new DelegatedQueryExecutor(delegate, getFilter(), getOrdering()); - } else { - foreignExecutor = indexExecutor(foreignAccess, foreignIndex); + CompositeScore score = getCompositeScore(); + FilteringScore fScore = score.getFilteringScore(); + if (fScore.isKeyMatch()) { + return new KeyQueryExecutor(localAccess, localIndex, fScore); + } + return new IndexedQueryExecutor(localAccess, localIndex, score); } - return new JoinedQueryExecutor - (mRepoAccess.getRootRepository(), getForeignProperty(), foreignExecutor); - } - - private QueryExecutor indexExecutor(StorageAccess access, - StorableIndex index) - { - CompositeScore score = getCompositeScore(); - FilteringScore fScore = score.getFilteringScore(); - if (fScore.isKeyMatch()) { - return new KeyQueryExecutor(access, index, fScore); - } - return new IndexedQueryExecutor(access, index, score); + return null; } public String toString() { diff --git a/src/main/java/com/amazon/carbonado/qe/IndexedQueryExecutor.java b/src/main/java/com/amazon/carbonado/qe/IndexedQueryExecutor.java index fd3ab4f..46527bf 100644 --- a/src/main/java/com/amazon/carbonado/qe/IndexedQueryExecutor.java +++ b/src/main/java/com/amazon/carbonado/qe/IndexedQueryExecutor.java @@ -52,6 +52,7 @@ public class IndexedQueryExecutor extends AbstractQueryExecu private final Support mSupport; private final StorableIndex mIndex; + private final int mHandledCount; private final int mIdentityCount; private final Filter mIdentityFilter; private final List> mExclusiveRangeStartFilters; @@ -70,6 +71,9 @@ public class IndexedQueryExecutor extends AbstractQueryExecu StorableIndex index, CompositeScore score) { + if (support == null && this instanceof Support) { + support = (Support) this; + } if (support == null || index == null || score == null) { throw new IllegalArgumentException(); } @@ -78,6 +82,9 @@ public class IndexedQueryExecutor extends AbstractQueryExecu mIndex = index; FilteringScore fScore = score.getFilteringScore(); + OrderingScore oScore = score.getOrderingScore(); + + mHandledCount = oScore.getHandledCount(); mIdentityCount = fScore.getIdentityCount(); mIdentityFilter = fScore.getIdentityFilter(); @@ -86,7 +93,7 @@ public class IndexedQueryExecutor extends AbstractQueryExecu mExclusiveRangeEndFilters = fScore.getExclusiveRangeEndFilters(); mInclusiveRangeEndFilters = fScore.getInclusiveRangeEndFilters(); - mReverseOrder = score.getOrderingScore().shouldReverseOrder(); + mReverseOrder = oScore.shouldReverseOrder(); mReverseRange = fScore.shouldReverseRange(); } @@ -185,12 +192,20 @@ public class IndexedQueryExecutor extends AbstractQueryExecu } public OrderingList getOrdering() { - OrderingList list = OrderingList.get(mIndex.getOrderedProperties()); - if (mIdentityCount > 0) { - list = OrderingList.get(list.subList(mIdentityCount, list.size())); - } - if (mReverseOrder) { - list = list.reverseDirections(); + OrderingList list; + if (mHandledCount == 0) { + list = OrderingList.emptyList(); + } else { + list = OrderingList.get(mIndex.getOrderedProperties()); + if (mIdentityCount > 0) { + list = list.subList(mIdentityCount, list.size()); + } + if (mHandledCount < list.size()) { + list = list.subList(0, mHandledCount); + } + if (mReverseOrder) { + list = list.reverseDirections(); + } } return list; } @@ -253,6 +268,9 @@ public class IndexedQueryExecutor extends AbstractQueryExecu return true; } + /** + * Provides support for {@link IndexedQueryExecutor}. + */ public static interface Support { /** * Perform an index scan of a subset of Storables referenced by an diff --git a/src/main/java/com/amazon/carbonado/qe/IterableQueryExecutor.java b/src/main/java/com/amazon/carbonado/qe/IterableQueryExecutor.java index de0864f..a1b636d 100644 --- a/src/main/java/com/amazon/carbonado/qe/IterableQueryExecutor.java +++ b/src/main/java/com/amazon/carbonado/qe/IterableQueryExecutor.java @@ -87,7 +87,7 @@ public class IterableQueryExecutor extends AbstractQueryExec throws IOException { indent(app, indentLevel); - app.append("iterable: "); + app.append("collection iterator: "); app.append(mType.getName()); newline(app); return true; diff --git a/src/main/java/com/amazon/carbonado/qe/JoinedQueryExecutor.java b/src/main/java/com/amazon/carbonado/qe/JoinedQueryExecutor.java index 11df215..62aab14 100644 --- a/src/main/java/com/amazon/carbonado/qe/JoinedQueryExecutor.java +++ b/src/main/java/com/amazon/carbonado/qe/JoinedQueryExecutor.java @@ -20,14 +20,29 @@ package com.amazon.carbonado.qe; import java.io.IOException; +import java.util.Comparator; +import java.util.Map; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.FieldInfo; +import org.cojen.classfile.Label; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.TypeDesc; + +import org.cojen.util.ClassInjector; +import org.cojen.util.SoftValuedHashMap; + import com.amazon.carbonado.Cursor; import com.amazon.carbonado.FetchException; import com.amazon.carbonado.Repository; import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; -import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.Storage; -import com.amazon.carbonado.cursor.JoinedCursorFactory; +import com.amazon.carbonado.cursor.MultiTransformedCursor; import com.amazon.carbonado.filter.AndFilter; import com.amazon.carbonado.filter.ClosedFilter; @@ -36,205 +51,571 @@ import com.amazon.carbonado.filter.FilterValues; import com.amazon.carbonado.filter.OpenFilter; import com.amazon.carbonado.filter.OrFilter; import com.amazon.carbonado.filter.PropertyFilter; -import com.amazon.carbonado.filter.Visitor; +import com.amazon.carbonado.filter.RelOp; import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableIndex; import com.amazon.carbonado.info.StorableInfo; import com.amazon.carbonado.info.StorableIntrospector; import com.amazon.carbonado.info.StorableProperty; +import com.amazon.carbonado.util.QuickConstructorGenerator; + +import com.amazon.carbonado.spi.CodeBuilderUtil; + /** - * QueryExecutor which wraps an executor for type source, follows a - * join, and produces type target. + * QueryExecutor which joins a source and target executor, + * producing results of target type. The source executor is called once per + * fetch (outer loop), but the target executor is called once per source result + * (inner loop). * * @author Brian S O'Neill - * @see JoinedCursorFactory * @param source type * @param target type */ public class JoinedQueryExecutor extends AbstractQueryExecutor { + /** + * Builds and returns a complex joined excutor against a chained property, + * supporting multi-way joins. Filtering and ordering may also be supplied, + * in order to better distribute work throughout the join. + * + * @param repoAccess used to create query executors for outer and inner loops + * @param targetToSourceProperty join property of target type which maps + * to instances of source type + * @param targetFilter optional filter for fetching target instances + * @param targetOrdering optional ordering to apply to target executor + * @throws IllegalArgumentException if any parameter is null or if join + * property is not a Storable type + * @throws RepositoryException from RepositoryAccess + */ + public static QueryExecutor + build(RepositoryAccess repoAccess, + ChainedProperty targetToSourceProperty, + Filter targetFilter, + OrderingList targetOrdering) + throws RepositoryException + { + if (targetOrdering == null) { + targetOrdering = OrderingList.emptyList(); + } + + QueryExecutor executor = + buildJoin(repoAccess, targetToSourceProperty, targetFilter, targetOrdering); + + OrderingList handledOrdering = executor.getOrdering(); + + // Apply sort if any remaining ordering properties. + int handledCount = commonOrderingCount(handledOrdering, targetOrdering); + + OrderingList remainderOrdering = + targetOrdering.subList(handledCount, targetOrdering.size()); + + if (remainderOrdering.size() > 0) { + SortedQueryExecutor.Support support = repoAccess + .storageAccessFor(targetToSourceProperty.getPrimeProperty().getEnclosingType()); + executor = new SortedQueryExecutor + (support, executor, handledOrdering, remainderOrdering); + } + + return executor; + } + + private static JoinedQueryExecutor + buildJoin(RepositoryAccess repoAccess, + ChainedProperty targetToSourceProperty, + Filter targetFilter, + OrderingList targetOrdering) + throws RepositoryException + { + StorableProperty primeTarget = targetToSourceProperty.getPrimeProperty(); + + Filter tailFilter; + if (targetFilter == null) { + tailFilter = null; + } else { + Filter.NotJoined nj = targetFilter.notJoinedFrom(ChainedProperty.get(primeTarget)); + tailFilter = nj.getNotJoinedFilter(); + targetFilter = nj.getRemainderFilter(); + } + + // Determine the most ordering properties the source (outer loop + // executor) can provide. It may use less if its selected index does + // not provide the ordering for free. + final OrderingList outerLoopOrdering = mostOrdering(primeTarget, targetOrdering); + + QueryExecutor outerLoopExecutor; + if (targetToSourceProperty.getChainCount() > 0) { + ChainedProperty tailProperty = targetToSourceProperty.tail(); + outerLoopExecutor = buildJoin(repoAccess, tailProperty, tailFilter, outerLoopOrdering); + } else { + Class sourceType = targetToSourceProperty.getType(); + + if (!Storable.class.isAssignableFrom(sourceType)) { + throw new IllegalArgumentException + ("Property type is not a Storable: " + targetToSourceProperty); + } + + StorageAccess sourceAccess = repoAccess.storageAccessFor(sourceType); + + OrderingList expectedOrdering = + expectedOrdering(sourceAccess, tailFilter, outerLoopOrdering); + + QueryExecutorFactory outerLoopExecutorFactory = sourceAccess.getQueryExecutorFactory(); + + outerLoopExecutor = outerLoopExecutorFactory.executor(tailFilter, expectedOrdering); + } + + if (targetOrdering.size() > 0) { + // If outer loop handles some of the ordering, then it can be + // removed from the target ordering. This simplifies or eliminates + // a final sort operation. + + int handledCount = + commonOrderingCount(outerLoopExecutor.getOrdering(), outerLoopOrdering); + + targetOrdering = targetOrdering.subList(handledCount, targetOrdering.size()); + } + + Class targetType = primeTarget.getEnclosingType(); + StorageAccess targetAccess = repoAccess.storageAccessFor(targetType); + + QueryExecutorFactory innerLoopExecutorFactory = targetAccess.getQueryExecutorFactory(); + + return new JoinedQueryExecutor(outerLoopExecutor, + innerLoopExecutorFactory, + primeTarget, + targetFilter, + targetOrdering, + targetAccess); + } + + private static final String INNER_LOOP_EX_FIELD_NAME = "innerLoopExecutor"; + private static final String INNER_LOOP_FV_FIELD_NAME = "innerLoopFilterValues"; + private static final String ACTIVE_SOURCE_FIELD_NAME = "active"; + + private static final Map cJoinerCursorClassCache; + + static { + cJoinerCursorClassCache = new SoftValuedHashMap(); + } + + private static synchronized Joiner.Factory + getJoinerFactory(StorableProperty targetToSourceProperty) + { + Class clazz = cJoinerCursorClassCache.get(targetToSourceProperty); + + if (clazz == null) { + clazz = generateJoinerCursor(targetToSourceProperty); + cJoinerCursorClassCache.put(targetToSourceProperty, clazz); + } + + return (Joiner.Factory) QuickConstructorGenerator + .getInstance(clazz, Joiner.Factory.class); + } + + private static Class> + generateJoinerCursor(StorableProperty targetToSourceProperty) + { + final Class sourceType = targetToSourceProperty.getType(); + final Class targetType = targetToSourceProperty.getEnclosingType(); + + String packageName; + { + String name = targetType.getName(); + int index = name.lastIndexOf('.'); + if (index >= 0) { + packageName = name.substring(0, index); + } else { + packageName = ""; + } + } + + ClassLoader loader = targetType.getClassLoader(); + + ClassInjector ci = ClassInjector.create(packageName + ".JoinedCursor", loader); + ClassFile cf = new ClassFile(ci.getClassName(), MultiTransformedCursor.class); + cf.markSynthetic(); + cf.setSourceFile(JoinedQueryExecutor.class.getName()); + cf.setTarget("1.5"); + + final TypeDesc cursorType = TypeDesc.forClass(Cursor.class); + final TypeDesc queryExecutorType = TypeDesc.forClass(QueryExecutor.class); + final TypeDesc filterValuesType = TypeDesc.forClass(FilterValues.class); + + // Define fields for inner loop executor and filter values, which are + // passed into the constructor. + cf.addField(Modifiers.PRIVATE.toFinal(true), INNER_LOOP_EX_FIELD_NAME, queryExecutorType); + cf.addField(Modifiers.PRIVATE.toFinal(true), INNER_LOOP_FV_FIELD_NAME, filterValuesType); + + // If target storable can set a reference to the joined source + // storable, then stash a copy of it as we go. This way, when user of + // target storable accesses the joined property, it costs nothing. + boolean canSetSourceReference = targetToSourceProperty.getWriteMethod() != null; + + if (canSetSourceReference) { + // Field to hold active source storable. + cf.addField(Modifiers.PRIVATE, ACTIVE_SOURCE_FIELD_NAME, + TypeDesc.forClass(sourceType)); + } + + // Define constructor. + { + TypeDesc[] params = {cursorType, queryExecutorType, filterValuesType}; + + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadLocal(b.getParameter(0)); // pass source cursor to superclass + b.invokeSuperConstructor(new TypeDesc[] {cursorType}); + + b.loadThis(); + b.loadLocal(b.getParameter(1)); + b.storeField(INNER_LOOP_EX_FIELD_NAME, queryExecutorType); + + b.loadThis(); + b.loadLocal(b.getParameter(2)); + b.storeField(INNER_LOOP_FV_FIELD_NAME, filterValuesType); + + b.returnVoid(); + } + + // Implement the transform method. + { + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, "transform", cursorType, + new TypeDesc[] {TypeDesc.OBJECT}); + mi.addException(TypeDesc.forClass(FetchException.class)); + CodeBuilder b = new CodeBuilder(mi); + + LocalVariable sourceVar = b.createLocalVariable(null, TypeDesc.forClass(sourceType)); + b.loadLocal(b.getParameter(0)); + b.checkCast(TypeDesc.forClass(sourceType)); + b.storeLocal(sourceVar); + + if (canSetSourceReference) { + b.loadThis(); + b.loadLocal(sourceVar); + b.storeField(ACTIVE_SOURCE_FIELD_NAME, TypeDesc.forClass(sourceType)); + } + + // Prepare to call fetch on innerLoopExecutor. + b.loadThis(); + b.loadField(INNER_LOOP_EX_FIELD_NAME, queryExecutorType); + + // Fill in values for innerLoopFilterValues. + b.loadThis(); + b.loadField(INNER_LOOP_FV_FIELD_NAME, filterValuesType); + + int propCount = targetToSourceProperty.getJoinElementCount(); + for (int i=0; i external = targetToSourceProperty.getExternalJoinElement(i); + b.loadLocal(sourceVar); + b.invoke(external.getReadMethod()); + + TypeDesc bindType = CodeBuilderUtil.bindQueryParam(external.getType()); + CodeBuilderUtil.convertValue(b, external.getType(), bindType.toClass()); + b.invokeVirtual(filterValuesType, "with", filterValuesType, + new TypeDesc[] {bindType}); + } + + // Now fetch and return. + b.invokeInterface(queryExecutorType, "fetch", cursorType, + new TypeDesc[] {filterValuesType}); + b.returnValue(cursorType); + } + + if (canSetSourceReference) { + // Override the "next" method to set source object on target. + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, "next", TypeDesc.OBJECT, null); + mi.addException(TypeDesc.forClass(FetchException.class)); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.invokeSuper(TypeDesc.forClass(MultiTransformedCursor.class), + "next", TypeDesc.OBJECT, null); + b.checkCast(TypeDesc.forClass(targetType)); + b.dup(); + + b.loadThis(); + b.loadField(ACTIVE_SOURCE_FIELD_NAME, TypeDesc.forClass(sourceType)); + b.invoke(targetToSourceProperty.getWriteMethod()); + + b.returnValue(TypeDesc.OBJECT); + } + + return (Class>) ci.defineClass(cf); + } + private static OrderingList transformOrdering(Class targetType, String targetToSourceProperty, QueryExecutor sourceExecutor) { + OrderingList targetOrdering = OrderingList.emptyList(); StorableInfo targetInfo = StorableIntrospector.examine(targetType); - OrderingList sourceOrdering = sourceExecutor.getOrdering(); - int size = sourceOrdering.size(); - OrderedProperty[] targetOrdering = new OrderedProperty[size]; - - for (int i=0; i sourceProp = sourceOrdering.get(i); + for (OrderedProperty sourceProp : sourceExecutor.getOrdering()) { String targetName = targetToSourceProperty + '.' + sourceProp.getChainedProperty(); OrderedProperty targetProp = OrderedProperty .get(ChainedProperty.parse(targetInfo, targetName), sourceProp.getDirection()); - targetOrdering[i] = targetProp; + targetOrdering = targetOrdering.concat(targetProp); } - return OrderingList.get(targetOrdering); + return targetOrdering; } - private final ChainedProperty mTargetToSourceProperty; - private final JoinedCursorFactory mFactory; - private final QueryExecutor mSourceExecutor; + /** + * Given a list of chained ordering properties, returns the properties + * stripped of the matching chain prefix for the targetToSourceProperty. As + * the target ordering is scanned left-to-right, if any property is found + * which doesn't match the targetToSourceProperty, the building of the new + * list stops. In other words, it returns a consecutive run of matching + * properties. + */ + private static OrderingList + mostOrdering(StorableProperty primeTarget, OrderingList targetOrdering) + { + OrderingList handledOrdering = OrderingList.emptyList(); + for (OrderedProperty targetProp : targetOrdering) { + ChainedProperty chainedProp = targetProp.getChainedProperty(); + if (chainedProp.getPrimeProperty().equals(primeTarget)) { + handledOrdering = handledOrdering + // I hate Java generics. Note the stupid cast. I have finally + // realized the core problem: the wildcard model is broken. + .concat(OrderedProperty + .get((ChainedProperty) chainedProp.tail(), + targetProp.getDirection())); + } else { + break; + } + } - private final FilterValues mSourceFilterValues; - private final Filter mTargetFilter; - private final OrderingList mTargetOrdering; + return handledOrdering; + } /** - * @param repo access to storage instances for properties - * @param targetType type of target instances - * @param targetToSourceProperty property of target type which maps - * to instances of source type. - * @param sourceExecutor executor for source instances - * @throws IllegalArgumentException if property type is not source + * Examines the given ordering against available indexes, returning the + * ordering that the best index can provide for free. */ - public JoinedQueryExecutor(Repository repo, - Class targetType, - String targetToSourceProperty, - QueryExecutor sourceExecutor) - throws SupportException, FetchException, RepositoryException + private static OrderingList + expectedOrdering(StorageAccess access, Filter filter, OrderingList ordering) { - this(repo, - ChainedProperty.parse(StorableIntrospector.examine(targetType), - targetToSourceProperty), - sourceExecutor); + Comparator comparator = CompositeScore.fullComparator(); + + CompositeScore bestScore = null; + for (StorableIndex index : access.getAllIndexes()) { + CompositeScore candidateScore = CompositeScore.evaluate(index, filter, ordering); + if (bestScore == null || comparator.compare(candidateScore, bestScore) < 0) { + bestScore = candidateScore; + } + } + + // Reduce source ordering to that which can be handled for + // free. Otherwise, a sort would be performed which is a waste of time + // if some source results will later be filtered out. + int handledCount = bestScore == null ? 0 : bestScore.getOrderingScore().getHandledCount(); + return ordering.subList(0, handledCount); } /** - * @param repo access to storage instances for properties - * @param targetToSourceProperty property of target type which maps - * to instances of source type. - * @param aExecutor executor for A instances - * @throws IllegalArgumentException if property type is not A + * Returns the count of exactly matching properties from the two + * orderings. The match must be consecutive and start at the first + * property. */ - public JoinedQueryExecutor(Repository repo, - ChainedProperty targetToSourceProperty, - QueryExecutor sourceExecutor) - throws SupportException, FetchException, RepositoryException + private static int + commonOrderingCount(OrderingList orderingA, OrderingList orderingB) { - mTargetToSourceProperty = targetToSourceProperty; - mFactory = new JoinedCursorFactory - (repo, targetToSourceProperty, sourceExecutor.getStorableType()); - mSourceExecutor = sourceExecutor; + int commonCount = Math.min(orderingA.size(), orderingB.size()); - Filter sourceFilter = sourceExecutor.getFilter(); - - mSourceFilterValues = sourceFilter.initialFilterValues(); - mTargetFilter = sourceFilter.accept(new FilterTransformer(), null); + for (int i=0; i getFilter() { - return mTargetFilter; - } + private final Filter mTargetFilter; + private final StorableProperty mTargetToSourceProperty; - public Cursor fetch(FilterValues values) throws FetchException { - return mFactory.join(mSourceExecutor.fetch(transferValues(values))); - } + private final QueryExecutor mOuterLoopExecutor; + private final FilterValues mOuterLoopFilterValues; - public OrderingList getOrdering() { - return mTargetOrdering; - } + private final QueryExecutor mInnerLoopExecutor; + private final FilterValues mInnerLoopFilterValues; - public boolean printPlan(Appendable app, int indentLevel, FilterValues values) - throws IOException + private final Filter mSourceFilterAsFromTarget; + private final Filter mCombinedFilter; + private final OrderingList mCombinedOrdering; + + private final Joiner.Factory mJoinerFactory; + + /** + * @param outerLoopExecutor executor for source instances + * @param innerLoopExecutorFactory used to construct inner loop executor + * @param targetToSourceProperty join property of target type which maps + * to instances of source type + * @param targetFilter optional initial filter for fetching target instances + * @param targetOrdering optional desired ordering to apply to + * target executor + * @param targetAccess used with target ordering to determine actual + * ordering which an index provides for free + * @throws IllegalArgumentException if any parameter is null or if join + * property is not of source type + * @throws RepositoryException from innerLoopExecutorFactory + */ + private JoinedQueryExecutor(QueryExecutor outerLoopExecutor, + QueryExecutorFactory innerLoopExecutorFactory, + StorableProperty targetToSourceProperty, + Filter targetFilter, + OrderingList targetOrdering, + StorageAccess targetAccess) + throws RepositoryException { - int chainCount = mTargetToSourceProperty.getChainCount(); + if (targetToSourceProperty == null || outerLoopExecutor == null) { + throw new IllegalArgumentException("Null parameter"); + } - for (int i = -1; i < chainCount; i++) { - indent(app, indentLevel); - app.append("join: "); + Class sourceType = outerLoopExecutor.getStorableType(); + if (targetToSourceProperty.getType() != sourceType) { + throw new IllegalArgumentException + ("Property is not of type \"" + sourceType.getName() + "\": " + + targetToSourceProperty); + } - StorableProperty prop; - if (i == -1) { - prop = mTargetToSourceProperty.getPrimeProperty(); - } else { - prop = mTargetToSourceProperty.getChainedProperty(i); - } + if (!targetToSourceProperty.isJoin()) { + throw new IllegalArgumentException + ("Property is not a join: " + targetToSourceProperty); + } - app.append(prop.getEnclosingType().getName()); - newline(app); - indent(app, indentLevel); - app.append("...via property: "); - app.append(prop.getName()); - newline(app); - indentLevel = increaseIndent(indentLevel); + if (targetFilter != null && !targetFilter.isBound()) { + throw new IllegalArgumentException("Target filter must be bound"); } - mSourceExecutor.printPlan(app, indentLevel, transferValues(values)); - return true; - } + if (!outerLoopExecutor.getFilter().isBound()) { + throw new IllegalArgumentException("Outer loop executor filter must be bound"); + } - private FilterValues transferValues(FilterValues values) { - if (values == null || mSourceFilterValues == null) { - return null; + if (targetFilter instanceof OpenFilter) { + targetFilter = null; } - return mSourceFilterValues.withValues(values.getSuppliedValues()); - } - private class FilterTransformer extends Visitor, Object> { - private final Class mTargetType; + mTargetFilter = targetFilter; + mTargetToSourceProperty = targetToSourceProperty; + mOuterLoopExecutor = outerLoopExecutor; + mOuterLoopFilterValues = outerLoopExecutor.getFilter().initialFilterValues(); + + Class targetType = targetToSourceProperty.getEnclosingType(); - FilterTransformer() { - mTargetType = mTargetToSourceProperty.getPrimeProperty().getEnclosingType(); + // Prepare inner loop filter which is and'd by the join property elements. + Filter innerLoopExecutorFilter = Filter.getOpenFilter(targetType); + if (targetFilter != null) { + innerLoopExecutorFilter = innerLoopExecutorFilter.and(targetFilter); } + int count = targetToSourceProperty.getJoinElementCount(); + for (int i=0; i visit(OrFilter sourceFilter, Object param) { - return sourceFilter.getLeftFilter().accept(this, param) - .and(sourceFilter.getRightFilter().accept(this, param)); + // Only perform requested ordering if it index provides it for free. + if (targetOrdering != null) { + targetOrdering = + expectedOrdering(targetAccess, innerLoopExecutorFilter, targetOrdering); } - public Filter visit(AndFilter sourceFilter, Object param) { - return sourceFilter.getLeftFilter().accept(this, param) - .or(sourceFilter.getRightFilter().accept(this, param)); + mInnerLoopExecutor = innerLoopExecutorFactory + .executor(innerLoopExecutorFilter, targetOrdering); + + Filter filter = outerLoopExecutor.getFilter() + .asJoinedFrom(ChainedProperty.get(targetToSourceProperty)); + + mSourceFilterAsFromTarget = filter; + + if (targetFilter != null) { + filter = filter.and(targetFilter); } - public Filter visit(PropertyFilter sourceFilter, Object param) { - String name; + mCombinedFilter = filter; - ChainedProperty sourceChainedProp = sourceFilter.getChainedProperty(); - if (mTargetType == sourceChainedProp.getPrimeProperty().getEnclosingType()) { - // If type of S is already T, (which violates generic type - // signature) then it came from join index analysis. - name = sourceChainedProp.toString(); - } else { - StringBuilder nameBuilder = new StringBuilder(); - try { - mTargetToSourceProperty.appendTo(nameBuilder); - nameBuilder.append('.'); - sourceChainedProp.appendTo(nameBuilder); - } catch (IOException e) { - // Not gonna happen - } - name = nameBuilder.toString(); - } + // Prepare combined ordering. + OrderingList ordering = transformOrdering + (targetType, targetToSourceProperty.getName(), outerLoopExecutor); - Filter targetFilter = Filter.getOpenFilter(mTargetType); - if (sourceFilter.isConstant()) { - targetFilter = targetFilter - .and(name, sourceFilter.getOperator(), sourceFilter.constant()); - } else { - targetFilter = targetFilter.and(name, sourceFilter.getOperator()); - } + if (targetOrdering != null) { + ordering = ordering.concat(targetOrdering); + } + + mCombinedOrdering = ordering; + + mJoinerFactory = getJoinerFactory(targetToSourceProperty); + } + + public Cursor fetch(FilterValues values) throws FetchException { + FilterValues innerLoopFilterValues = mInnerLoopFilterValues; - return targetFilter; + if (mTargetFilter != null) { + // Prepare this before opening source cursor, in case an exception is thrown. + innerLoopFilterValues = innerLoopFilterValues + .withValues(values.getValuesFor(mTargetFilter)); } - public Filter visit(OpenFilter sourceFilter, Object param) { - return Filter.getOpenFilter(mTargetType); + Cursor outerLoopCursor = mOuterLoopExecutor.fetch(transferValues(values)); + + return mJoinerFactory.newJoinedCursor + (outerLoopCursor, mInnerLoopExecutor, innerLoopFilterValues); + } + + public Filter getFilter() { + return mCombinedFilter; + } + + public OrderingList getOrdering() { + return mCombinedOrdering; + } + + public boolean printPlan(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + indent(app, indentLevel); + app.append("join: "); + app.append(mTargetToSourceProperty.getEnclosingType().getName()); + newline(app); + indent(app, indentLevel); + app.append("...inner loop: "); + app.append(mTargetToSourceProperty.getName()); + newline(app); + mInnerLoopExecutor.printPlan(app, increaseIndent(indentLevel), values); + indent(app, indentLevel); + app.append("...outer loop"); + newline(app); + mOuterLoopExecutor.printPlan(app, increaseIndent(indentLevel), transferValues(values)); + return true; + } + + private FilterValues transferValues(FilterValues values) { + if (values == null) { + return null; } + return mOuterLoopFilterValues.withValues(values.getValuesFor(mSourceFilterAsFromTarget)); + } - public Filter visit(ClosedFilter sourceFilter, Object param) { - return Filter.getClosedFilter(mTargetType); + private static interface Joiner { + /** + * Needs to be public for {@link QuickConstructorGenerator}, but hide + * it inside a private interface. + */ + public static interface Factory { + Cursor newJoinedCursor(Cursor outerLoopCursor, + QueryExecutor innerLoopExecutor, + FilterValues innerLoopFilterValues); } } } diff --git a/src/main/java/com/amazon/carbonado/qe/KeyQueryExecutor.java b/src/main/java/com/amazon/carbonado/qe/KeyQueryExecutor.java index d8436c0..fcf728d 100644 --- a/src/main/java/com/amazon/carbonado/qe/KeyQueryExecutor.java +++ b/src/main/java/com/amazon/carbonado/qe/KeyQueryExecutor.java @@ -51,6 +51,9 @@ public class KeyQueryExecutor extends AbstractQueryExecutor< * not unique or if score is not a key match */ public KeyQueryExecutor(Support support, StorableIndex index, FilteringScore score) { + if (support == null && this instanceof Support) { + support = (Support) this; + } if (support == null || index == null || score == null) { throw new IllegalArgumentException(); } @@ -101,6 +104,9 @@ public class KeyQueryExecutor extends AbstractQueryExecutor< return true; } + /** + * Provides support for {@link KeyQueryExecutor}. + */ public static interface Support { /** * Select at most one Storable referenced by an index. The identity diff --git a/src/main/java/com/amazon/carbonado/qe/OrderingList.java b/src/main/java/com/amazon/carbonado/qe/OrderingList.java index f6366e5..d100cc9 100644 --- a/src/main/java/com/amazon/carbonado/qe/OrderingList.java +++ b/src/main/java/com/amazon/carbonado/qe/OrderingList.java @@ -246,6 +246,23 @@ public class OrderingList extends AbstractList subList(int fromIndex, int toIndex) { + // Check for optimization opportunity. + if (fromIndex == 0 && toIndex >= 0 && toIndex <= mSize) { + if (toIndex == 0) { + return emptyList(); + } + OrderingList list = this; + while (toIndex < list.mSize) { + list = list.mParent; + } + return list; + } + + return get(super.subList(fromIndex, toIndex)); + } + /** * This method is not public because the array is not a clone. */ diff --git a/src/main/java/com/amazon/carbonado/qe/QueryEngine.java b/src/main/java/com/amazon/carbonado/qe/QueryEngine.java index 1362045..59c9872 100644 --- a/src/main/java/com/amazon/carbonado/qe/QueryEngine.java +++ b/src/main/java/com/amazon/carbonado/qe/QueryEngine.java @@ -19,9 +19,11 @@ package com.amazon.carbonado.qe; import com.amazon.carbonado.IsolationLevel; +import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; import com.amazon.carbonado.Transaction; +import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; /** @@ -29,7 +31,9 @@ import com.amazon.carbonado.filter.FilterValues; * * @author Brian S O'Neill */ -public class QueryEngine extends StandardQueryFactory { +public class QueryEngine extends StandardQueryFactory + implements QueryExecutorFactory +{ final RepositoryAccess mRepoAccess; final QueryExecutorFactory mExecutorFactory; @@ -39,6 +43,12 @@ public class QueryEngine extends StandardQueryFactory { mExecutorFactory = new QueryExecutorCache(new UnionQueryAnalyzer(type, access)); } + public QueryExecutor executor(Filter filter, OrderingList ordering) + throws RepositoryException + { + return mExecutorFactory.executor(filter, ordering); + } + protected StandardQuery createQuery(FilterValues values, OrderingList ordering) { return new Query(values, ordering); } diff --git a/src/main/java/com/amazon/carbonado/qe/QueryExecutorFactory.java b/src/main/java/com/amazon/carbonado/qe/QueryExecutorFactory.java index 1b1e680..5568f01 100644 --- a/src/main/java/com/amazon/carbonado/qe/QueryExecutorFactory.java +++ b/src/main/java/com/amazon/carbonado/qe/QueryExecutorFactory.java @@ -23,8 +23,6 @@ import com.amazon.carbonado.Storable; import com.amazon.carbonado.filter.Filter; -import com.amazon.carbonado.info.OrderedProperty; - /** * Produces {@link QueryExecutor} instances from a query specification. * diff --git a/src/main/java/com/amazon/carbonado/qe/SortedQueryExecutor.java b/src/main/java/com/amazon/carbonado/qe/SortedQueryExecutor.java index cae03d8..38a8e36 100644 --- a/src/main/java/com/amazon/carbonado/qe/SortedQueryExecutor.java +++ b/src/main/java/com/amazon/carbonado/qe/SortedQueryExecutor.java @@ -64,9 +64,13 @@ public class SortedQueryExecutor extends AbstractQueryExecut OrderingList handledOrdering, OrderingList remainderOrdering) { + if (support == null && this instanceof Support) { + support = (Support) this; + } if (executor == null) { throw new IllegalArgumentException(); } + mSupport = support; mExecutor = executor; @@ -128,6 +132,9 @@ public class SortedQueryExecutor extends AbstractQueryExecut return true; } + /** + * Provides support for {@link SortedQueryExecutor}. + */ public static interface Support { /** * Implementation must return an empty buffer for sorting. diff --git a/src/main/java/com/amazon/carbonado/qe/StorageAccess.java b/src/main/java/com/amazon/carbonado/qe/StorageAccess.java index 2ab2313..643d61c 100644 --- a/src/main/java/com/amazon/carbonado/qe/StorageAccess.java +++ b/src/main/java/com/amazon/carbonado/qe/StorageAccess.java @@ -46,6 +46,11 @@ public interface StorageAccess */ Class getStorableType(); + /** + * Returns a QueryExecutorFactory instance for storage. + */ + QueryExecutorFactory getQueryExecutorFactory(); + /** * Returns all the available indexes. */ diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java index 2901ea0..fde1952 100644 --- a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java @@ -59,6 +59,7 @@ import com.amazon.carbonado.cursor.SortBuffer; import com.amazon.carbonado.qe.BoundaryType; import com.amazon.carbonado.qe.QueryEngine; +import com.amazon.carbonado.qe.QueryExecutorFactory; import com.amazon.carbonado.qe.StorageAccess; import com.amazon.carbonado.spi.RepairExecutor; @@ -266,6 +267,10 @@ class IndexedStorage implements Storage, StorageAccess return accessors.toArray(new IndexEntryAccessor[accessors.size()]); } + public QueryExecutorFactory getQueryExecutorFactory() { + return mQueryEngine; + } + public Collection> getAllIndexes() { return mIndexSet; } diff --git a/src/main/java/com/amazon/carbonado/spi/CodeBuilderUtil.java b/src/main/java/com/amazon/carbonado/spi/CodeBuilderUtil.java index 4e2e777..e3b24bd 100644 --- a/src/main/java/com/amazon/carbonado/spi/CodeBuilderUtil.java +++ b/src/main/java/com/amazon/carbonado/spi/CodeBuilderUtil.java @@ -385,6 +385,8 @@ public class CodeBuilderUtil { * Determines which overloaded "with" method on Query should be bound to. */ public static TypeDesc bindQueryParam(Class clazz) { + // This method is a bit vestigial. Once upon a time the Query class did + // not support all primitive types. if (clazz.isPrimitive()) { TypeDesc type = TypeDesc.forClass(clazz); switch (type.getTypeCode()) { @@ -392,6 +394,10 @@ public class CodeBuilderUtil { case TypeDesc.LONG_CODE: case TypeDesc.FLOAT_CODE: case TypeDesc.DOUBLE_CODE: + case TypeDesc.BOOLEAN_CODE: + case TypeDesc.CHAR_CODE: + case TypeDesc.BYTE_CODE: + case TypeDesc.SHORT_CODE: return type; } } diff --git a/src/test/java/com/amazon/carbonado/qe/TestIndexedQueryAnalyzer.java b/src/test/java/com/amazon/carbonado/qe/TestIndexedQueryAnalyzer.java index ef1711f..6b1919a 100644 --- a/src/test/java/com/amazon/carbonado/qe/TestIndexedQueryAnalyzer.java +++ b/src/test/java/com/amazon/carbonado/qe/TestIndexedQueryAnalyzer.java @@ -27,12 +27,17 @@ import junit.framework.TestCase; import junit.framework.TestSuite; import com.amazon.carbonado.Cursor; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Query; import com.amazon.carbonado.Repository; +import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; import com.amazon.carbonado.Storage; +import com.amazon.carbonado.cursor.ArraySortBuffer; import com.amazon.carbonado.cursor.SortBuffer; +import com.amazon.carbonado.info.OrderedProperty; import com.amazon.carbonado.info.StorableIndex; import com.amazon.carbonado.filter.Filter; @@ -47,8 +52,6 @@ import com.amazon.carbonado.stored.Shipment; import com.amazon.carbonado.stored.Shipper; import com.amazon.carbonado.stored.StorableTestBasic; -import static com.amazon.carbonado.qe.TestIndexedQueryExecutor.Mock; - /** * * @@ -118,7 +121,8 @@ public class TestIndexedQueryAnalyzer extends TestCase { assertTrue(result.handlesAnything()); assertEquals(filter, result.getCompositeScore().getFilteringScore().getIdentityFilter()); - assertEquals(makeIndex(Shipment.class, "orderID"), result.getLocalIndex()); + assertEquals(makeIndex(Shipment.class, "orderID", "shipmentNotes"), + result.getLocalIndex()); assertEquals(null, result.getForeignIndex()); assertEquals(null, result.getForeignProperty()); @@ -133,7 +137,8 @@ public class TestIndexedQueryAnalyzer extends TestCase { result.getCompositeScore().getFilteringScore().getRangeStartFilters(); assertEquals(1, rangeFilters.size()); assertEquals(filter, rangeFilters.get(0)); - assertEquals(makeIndex(Shipment.class, "orderID"), result.getLocalIndex()); + assertEquals(makeIndex(Shipment.class, "orderID", "shipmentNotes"), + result.getLocalIndex()); assertEquals(null, result.getForeignIndex()); assertEquals(null, result.getForeignProperty()); } @@ -183,7 +188,8 @@ public class TestIndexedQueryAnalyzer extends TestCase { assertTrue(result.getCompositeScore().getFilteringScore().hasRangeStart()); assertEquals(Filter.filterFor(Shipment.class, "order.orderTotal >= ?").bind(), result.getCompositeScore().getFilteringScore().getRemainderFilter()); - assertEquals(makeIndex(Shipment.class, "orderID"), result.getLocalIndex()); + assertEquals(makeIndex(Shipment.class, "orderID", "shipmentNotes"), + result.getLocalIndex()); assertEquals(null, result.getForeignIndex()); assertEquals(null, result.getForeignProperty()); } @@ -198,20 +204,19 @@ public class TestIndexedQueryAnalyzer extends TestCase { assertTrue(result.handlesAnything()); assertEquals(filter, result.getCompositeScore().getFilteringScore().getIdentityFilter()); assertEquals(null, result.getLocalIndex()); - assertEquals(makeIndex(Address.class, "addressState"), result.getForeignIndex()); + assertEquals(makeIndex(Address.class, "addressState", "-addressCity"), + result.getForeignIndex()); assertEquals("order.address", result.getForeignProperty().toString()); } public void testChainedJoinExecutor() throws Exception { - Repository repo = new ToyRepository(); - IndexedQueryAnalyzer iqa = new IndexedQueryAnalyzer(Shipment.class, RepoAccess.INSTANCE); Filter filter = Filter.filterFor (Shipment.class, "order.address.addressState = ? & order.address.addressZip = ?"); FilterValues values = filter.initialFilterValues(); filter = values.getFilter(); - IndexedQueryAnalyzer.Result result = iqa.analyze(filter, null); + IndexedQueryAnalyzer.Result result = iqa.analyze(filter, null); assertTrue(result.handlesAnything()); assertEquals(Filter.filterFor(Shipment.class, "order.address.addressState = ?").bind(), @@ -219,30 +224,117 @@ public class TestIndexedQueryAnalyzer extends TestCase { assertEquals(Filter.filterFor(Shipment.class, "order.address.addressZip = ?").bind(), result.getCompositeScore().getFilteringScore().getRemainderFilter()); assertEquals(null, result.getLocalIndex()); - assertEquals(makeIndex(Address.class, "addressState"), result.getForeignIndex()); + assertEquals(makeIndex(Address.class, "addressState", "-addressCity"), + result.getForeignIndex()); assertEquals("order.address", result.getForeignProperty().toString()); - Mock ixExec = new Mock(result.getForeignIndex(), result.getCompositeScore()); + QueryExecutor joinExec = JoinedQueryExecutor.build + (RepoAccess.INSTANCE, + result.getForeignProperty(), result.getFilter(), result.getOrdering()); + + FilterValues fv = values.with("WA").with("12345"); - QueryExecutor joinExec = new JoinedQueryExecutor - (repo, result.getForeignProperty(), ixExec); + StringBuffer buf = new StringBuffer(); + joinExec.printPlan(buf, 0, fv); + String plan = buf.toString(); - QueryExecutor filteredExec = new FilteredQueryExecutor - (joinExec, result.getCompositeScore().getFilteringScore().getRemainderFilter()); + // This is actually a pretty terrible plan due to the iterators. This + // is expected however, since we lied and said we had indexes. + String expected = + "join: com.amazon.carbonado.stored.Shipment\n" + + "...inner loop: order\n" + + " filter: orderID = ?\n" + + " collection iterator: com.amazon.carbonado.stored.Shipment\n" + + "...outer loop\n" + + " join: com.amazon.carbonado.stored.Order\n" + + " ...inner loop: address\n" + + " filter: addressID = ?\n" + + " collection iterator: com.amazon.carbonado.stored.Order\n" + + " ...outer loop\n" + + " filter: addressState = WA & addressZip = 12345\n" + + " collection iterator: com.amazon.carbonado.stored.Address\n"; - //System.out.println(); - //filteredExec.printPlan(System.out, 0, null); + assertEquals(expected, plan); - joinExec.fetch(values.with("WA")); + joinExec.fetch(fv); + + } + + public void testComplexChainedJoinExecutor() throws Exception { + IndexedQueryAnalyzer iqa = + new IndexedQueryAnalyzer(Shipment.class, RepoAccess.INSTANCE); + Filter filter = Filter.filterFor + (Shipment.class, + "order.address.addressState = ? & order.address.addressID != ? " + + "& order.address.addressZip = ? & order.orderTotal > ? & shipmentNotes <= ? " + + "& order.addressID > ?"); + FilterValues values = filter.initialFilterValues(); + filter = values.getFilter(); + OrderingList ordering = OrderingList + .get(Shipment.class, "order.address.addressCity", "shipmentNotes", "order.orderTotal"); + IndexedQueryAnalyzer.Result result = iqa.analyze(filter, ordering); + + assertTrue(result.handlesAnything()); + assertEquals(Filter.filterFor(Shipment.class, "order.address.addressState = ?").bind(), + result.getCompositeScore().getFilteringScore().getIdentityFilter()); + assertEquals(null, result.getLocalIndex()); + assertEquals(makeIndex(Address.class, "addressState", "-addressCity"), + result.getForeignIndex()); + assertEquals("order.address", result.getForeignProperty().toString()); - assertEquals(1, ixExec.mIdentityValues.length); - assertEquals("WA", ixExec.mIdentityValues[0]); - assertEquals(BoundaryType.OPEN, ixExec.mRangeStartBoundary); - assertEquals(null, ixExec.mRangeStartValue); - assertEquals(BoundaryType.OPEN, ixExec.mRangeEndBoundary); - assertEquals(null, ixExec.mRangeEndValue); - assertFalse(ixExec.mReverseRange); - assertFalse(ixExec.mReverseOrder); + QueryExecutor joinExec = JoinedQueryExecutor.build + (RepoAccess.INSTANCE, + result.getForeignProperty(), result.getFilter(), result.getOrdering()); + + FilterValues fv = + values.with("WA").with(45).with("12345").with(100).with("Z").with(2); + + StringBuffer buf = new StringBuffer(); + joinExec.printPlan(buf, 0, fv); + String plan = buf.toString(); + + // This is actually a pretty terrible plan due to the iterators. This + // is expected however, since we lied and said we had indexes. + String expected = + "sort: [+order.address.addressCity, +shipmentNotes], [+order.orderTotal]\n" + + " join: com.amazon.carbonado.stored.Shipment\n" + + " ...inner loop: order\n" + + " sort: [+shipmentNotes]\n" + + " filter: shipmentNotes <= Z & orderID = ?\n" + + " collection iterator: com.amazon.carbonado.stored.Shipment\n" + + " ...outer loop\n" + + " join: com.amazon.carbonado.stored.Order\n" + + " ...inner loop: address\n" + + " filter: orderTotal > 100 & addressID > 2 & addressID = ?\n" + + " collection iterator: com.amazon.carbonado.stored.Order\n" + + " ...outer loop\n" + + " sort: [+addressCity]\n" + + " filter: addressState = WA & addressID != 45 & addressZip = 12345\n" + + " collection iterator: com.amazon.carbonado.stored.Address\n"; + + //System.out.println(plan); + assertEquals(expected, plan); + + joinExec.fetch(fv); + + // Now do it the easier way and compare plans. + QueryExecutor joinExec2 = result.createExecutor(); + + StringBuffer buf2 = new StringBuffer(); + joinExec2.printPlan(buf2, 0, fv); + String plan2 = buf2.toString(); + + assertEquals(expected, plan2); + + Filter expectedFilter = Filter.filterFor + (Shipment.class, + "order.address.addressState = ? & order.address.addressID != ? " + + "& order.address.addressZip = ? & order.orderTotal > ? " + + "& order.addressID > ?" + + "& shipmentNotes <= ? "); + + assertEquals(expectedFilter.disjunctiveNormalForm(), + joinExec2.getFilter().unbind().disjunctiveNormalForm()); } static class RepoAccess implements RepositoryAccess { @@ -261,7 +353,9 @@ public class TestIndexedQueryAnalyzer extends TestCase { * Partially implemented StorageAccess which only supplies information * about indexes. */ - static class StoreAccess implements StorageAccess { + static class StoreAccess + implements StorageAccess, QueryExecutorFactory + { private final Class mType; StoreAccess(Class type) { @@ -272,24 +366,45 @@ public class TestIndexedQueryAnalyzer extends TestCase { return mType; } + public QueryExecutorFactory getQueryExecutorFactory() { + return this; + } + + public QueryExecutor executor(Filter filter, OrderingList ordering) { + Iterable iterable = Collections.emptyList(); + + QueryExecutor exec = new IterableQueryExecutor + (filter.getStorableType(), iterable); + + if (filter != null) { + exec = new FilteredQueryExecutor(exec, filter); + } + + if (ordering != null && ordering.size() > 0) { + exec = new SortedQueryExecutor(null, exec, null, ordering); + } + + return exec; + } + public Collection> getAllIndexes() { StorableIndex[] indexes; if (Address.class.isAssignableFrom(mType)) { indexes = new StorableIndex[] { makeIndex(mType, "addressID"), - makeIndex(mType, "addressState") + makeIndex(mType, "addressState", "-addressCity") }; } else if (Order.class.isAssignableFrom(mType)) { indexes = new StorableIndex[] { makeIndex(mType, "orderID"), makeIndex(mType, "orderTotal"), - makeIndex(mType, "addressID") + makeIndex(mType, "addressID", "orderTotal") }; } else if (Shipment.class.isAssignableFrom(mType)) { indexes = new StorableIndex[] { makeIndex(mType, "shipmentID"), - makeIndex(mType, "orderID"), + makeIndex(mType, "orderID", "shipmentNotes"), }; } else if (Shipper.class.isAssignableFrom(mType)) { indexes = new StorableIndex[] { @@ -315,6 +430,10 @@ public class TestIndexedQueryAnalyzer extends TestCase { } public SortBuffer createSortBuffer() { + return new ArraySortBuffer(); + } + + public long countAll() { throw new UnsupportedOperationException(); } diff --git a/src/test/java/com/amazon/carbonado/qe/TestIndexedQueryExecutor.java b/src/test/java/com/amazon/carbonado/qe/TestIndexedQueryExecutor.java index 8908825..a68fbac 100644 --- a/src/test/java/com/amazon/carbonado/qe/TestIndexedQueryExecutor.java +++ b/src/test/java/com/amazon/carbonado/qe/TestIndexedQueryExecutor.java @@ -76,7 +76,7 @@ public class TestIndexedQueryExecutor extends TestCase { CompositeScore score = CompositeScore.evaluate(index, filter, null); - Mock executor = new Mock(index, score); + Mock executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -96,7 +96,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100).with(5)); @@ -117,7 +117,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(200)); @@ -140,7 +140,7 @@ public class TestIndexedQueryExecutor extends TestCase { CompositeScore score = CompositeScore.evaluate(index, filter, null); - Mock executor = new Mock(index, score); + Mock executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -159,7 +159,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -178,7 +178,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(10).with(30)); @@ -197,7 +197,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(10).with(30)); @@ -216,7 +216,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(10).with(10)); @@ -235,7 +235,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(30).with(10)); @@ -255,7 +255,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, makeOrdering(StorableTestBasic.class, "-intProp")); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100).with(30)); @@ -276,7 +276,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -296,7 +296,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, makeOrdering(StorableTestBasic.class, "-intProp")); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -316,7 +316,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, makeOrdering(StorableTestBasic.class, "intProp")); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -339,7 +339,7 @@ public class TestIndexedQueryExecutor extends TestCase { CompositeScore score = CompositeScore.evaluate(index, filter, null); - Mock executor = new Mock(index, score); + Mock executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -358,7 +358,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -377,7 +377,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(10).with(30)); @@ -396,7 +396,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(10).with(30)); @@ -415,7 +415,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(10).with(10)); @@ -434,7 +434,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(30).with(10)); @@ -454,7 +454,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, makeOrdering(StorableTestBasic.class, "-intProp")); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100).with(30)); @@ -475,7 +475,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -495,7 +495,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, makeOrdering(StorableTestBasic.class, "-intProp")); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -515,7 +515,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, makeOrdering(StorableTestBasic.class, "intProp")); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100)); @@ -541,7 +541,7 @@ public class TestIndexedQueryExecutor extends TestCase { CompositeScore score = CompositeScore.evaluate(index, filter, null); - Mock executor = new Mock(index, score); + Mock executor = new Mock(index, score); executor.fetch(values.with(100).with(200)); @@ -560,7 +560,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(100).with(10)); @@ -580,7 +580,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(10).with(100).with(30)); @@ -612,7 +612,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, null); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(3).with(56.5).with(200.2)); @@ -628,7 +628,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, makeOrdering(StorableTestBasic.class, "doubleProp")); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(3).with(56.5).with(200.2)); @@ -644,7 +644,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, makeOrdering(StorableTestBasic.class, "stringProp")); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(3).with(56.5).with(200.2)); @@ -665,7 +665,7 @@ public class TestIndexedQueryExecutor extends TestCase { score = CompositeScore.evaluate(index, filter, makeOrdering(StorableTestBasic.class, "-stringProp")); - executor = new Mock(index, score); + executor = new Mock(index, score); executor.fetch(values.with(3).with(56.5).with("foo")); @@ -684,11 +684,40 @@ public class TestIndexedQueryExecutor extends TestCase { assertEquals(expectedOrdering, executor.getOrdering()); } + public void testHandledOrdering() throws Exception { + // Tests that ordering of executor only reveals what it actually uses. + + StorableIndex index; + Filter filter; + FilterValues values; + CompositeScore score; + Mock executor; + + index = makeIndex(StorableTestBasic.class, "intProp", "-doubleProp", "stringProp"); + + filter = Filter.filterFor(StorableTestBasic.class, "intProp = ?"); + values = filter.initialFilterValues(); + filter = values.getFilter(); + + score = CompositeScore.evaluate + (index, filter, + makeOrdering(StorableTestBasic.class, "intProp", "doubleProp")); + + executor = new Mock(index, score); + + assertEquals(values.getFilter(), executor.getFilter()); + List> expectedOrdering = + makeOrdering(StorableTestBasic.class, "+doubleProp"); + assertEquals(expectedOrdering, executor.getOrdering()); + } + /** * Mock object doesn't really open a cursor -- it just captures the passed * parameters. */ - static class Mock extends IndexedQueryExecutor { + static class Mock extends IndexedQueryExecutor + implements IndexedQueryExecutor.Support + { Object[] mIdentityValues; BoundaryType mRangeStartBoundary; Object mRangeStartValue; @@ -697,19 +726,9 @@ public class TestIndexedQueryExecutor extends TestCase { boolean mReverseRange; boolean mReverseOrder; - Mock(StorableIndex index, CompositeScore score) { - this(index, score, new MockSupport[1]); - } - - Mock(StorableIndex index, CompositeScore score, MockSupport[] ref) { - // Extremely bizarre hack to allow support to know about us. - super(ref[0] = new MockSupport(), index, score); - ((MockSupport) ref[0]).mMock = this; + public Mock(StorableIndex index, CompositeScore score) { + super(null, index, score); } - } - - static class MockSupport implements IndexedQueryExecutor.Support { - Mock mMock; public Cursor fetchSubset(StorableIndex index, Object[] identityValues, @@ -720,13 +739,13 @@ public class TestIndexedQueryExecutor extends TestCase { boolean reverseRange, boolean reverseOrder) { - mMock.mIdentityValues = identityValues; - mMock.mRangeStartBoundary = rangeStartBoundary; - mMock.mRangeStartValue = rangeStartValue; - mMock.mRangeEndBoundary = rangeEndBoundary; - mMock.mRangeEndValue = rangeEndValue; - mMock.mReverseRange = reverseRange; - mMock.mReverseOrder = reverseOrder; + mIdentityValues = identityValues; + mRangeStartBoundary = rangeStartBoundary; + mRangeStartValue = rangeStartValue; + mRangeEndBoundary = rangeEndBoundary; + mRangeEndValue = rangeEndValue; + mReverseRange = reverseRange; + mReverseOrder = reverseOrder; Collection empty = Collections.emptyList(); return new IteratorCursor(empty); diff --git a/src/test/java/com/amazon/carbonado/qe/TestJoinedQueryExecutor.java b/src/test/java/com/amazon/carbonado/qe/TestJoinedQueryExecutor.java index 423cd74..e1abca9 100644 --- a/src/test/java/com/amazon/carbonado/qe/TestJoinedQueryExecutor.java +++ b/src/test/java/com/amazon/carbonado/qe/TestJoinedQueryExecutor.java @@ -18,8 +18,12 @@ package com.amazon.carbonado.qe; +import java.util.Arrays; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; import junit.framework.TestSuite; @@ -27,14 +31,21 @@ import com.amazon.carbonado.Cursor; import com.amazon.carbonado.FetchException; import com.amazon.carbonado.Query; import com.amazon.carbonado.Repository; +import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; import com.amazon.carbonado.Storage; +import com.amazon.carbonado.cursor.ArraySortBuffer; +import com.amazon.carbonado.cursor.SortBuffer; + import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.FilterValues; +import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.Direction; import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.info.StorableInfo; import com.amazon.carbonado.info.StorableIntrospector; import com.amazon.carbonado.info.StorableProperty; @@ -69,10 +80,23 @@ public class TestJoinedQueryExecutor extends TestQueryExecutor { } public void testJoin() throws Exception { - QueryExecutor addressExecutor = addressExecutor(); + StorableInfo info = StorableIntrospector.examine(UserInfo.class); + Map> properties = info.getAllProperties(); + + RepositoryAccess repoAccess = new RepoAccess(); + + ChainedProperty targetToSourceProperty = + ChainedProperty.get(properties.get("address")); + + Filter targetFilter = Filter.filterFor(UserInfo.class, "address.state = ?"); + OrderingList targetOrdering = + OrderingList.get(UserInfo.class, "+address.country"); + + QueryExecutor userExecutor = JoinedQueryExecutor.build + (repoAccess, targetToSourceProperty, targetFilter, targetOrdering); - QueryExecutor userExecutor = new JoinedQueryExecutor - (mRepository, UserInfo.class, "address", addressExecutor); + //System.out.println(); + //userExecutor.printPlan(System.out, 0, null); assertEquals("address.state = ?", userExecutor.getFilter().toString()); assertEquals("+address.country", userExecutor.getOrdering().get(0).toString()); @@ -146,8 +170,13 @@ public class TestJoinedQueryExecutor extends TestQueryExecutor { // Now do a multi join, finding everyone with an explicit neighbor in IL. - userExecutor = new JoinedQueryExecutor - (mRepository, UserInfo.class, "address.neighbor", addressExecutor); + targetToSourceProperty = ChainedProperty.parse(info, "address.neighbor"); + + targetFilter = Filter.filterFor(UserInfo.class, "address.neighbor.state = ?"); + targetOrdering = OrderingList.get(UserInfo.class, "+address.neighbor.country"); + + userExecutor = JoinedQueryExecutor.build + (repoAccess, targetToSourceProperty, targetFilter, targetOrdering); assertEquals("address.neighbor.state = ?", userExecutor.getFilter().toString()); assertEquals("+address.neighbor.country", userExecutor.getOrdering().get(0).toString()); @@ -163,24 +192,88 @@ public class TestJoinedQueryExecutor extends TestQueryExecutor { cursor.close(); assertEquals(1L, userExecutor.count(values)); - } - protected QueryExecutor addressExecutor() throws Exception { - Storage addressStorage = mRepository.storageFor(UserAddress.class); + class RepoAccess implements RepositoryAccess { + public Repository getRootRepository() { + return mRepository; + } - QueryExecutor addressExecutor = new FullScanQueryExecutor - (new ScanQuerySupport(addressStorage.query())); + public StorageAccess storageAccessFor(Class type) { + return new StoreAccess(type); + } + } + + class StoreAccess implements StorageAccess, QueryExecutorFactory { + private final Class mType; + + StoreAccess(Class type) { + mType = type; + } + + public Class getStorableType() { + return mType; + } + + public QueryExecutorFactory getQueryExecutorFactory() { + return this; + } - addressExecutor = new FilteredQueryExecutor - (addressExecutor, Filter.filterFor(UserAddress.class, "state = ?")); + public QueryExecutor executor(Filter filter, OrderingList ordering) + throws RepositoryException + { + Storage storage = mRepository.storageFor(mType); - OrderingList ordering = OrderingList.get(UserAddress.class, "+country"); + QueryExecutor exec = new FullScanQueryExecutor + (new ScanQuerySupport(storage.query())); - addressExecutor = new SortedQueryExecutor - (null, addressExecutor, null, ordering); + if (filter != null) { + exec = new FilteredQueryExecutor(exec, filter); + } - return addressExecutor; + if (ordering != null && ordering.size() > 0) { + exec = new SortedQueryExecutor(null, exec, null, ordering); + } + + return exec; + } + + public Collection> getAllIndexes() { + StorableIndex[] indexes = new StorableIndex[0]; + return Arrays.asList(indexes); + } + + public Storage storageDelegate(StorableIndex index) { + return null; + } + + public SortBuffer createSortBuffer() { + return new ArraySortBuffer(); + } + + public long countAll() { + throw new UnsupportedOperationException(); + } + + public Cursor fetchAll() { + throw new UnsupportedOperationException(); + } + + public Cursor fetchOne(StorableIndex index, Object[] identityValues) { + throw new UnsupportedOperationException(); + } + + public Cursor fetchSubset(StorableIndex index, + Object[] identityValues, + BoundaryType rangeStartBoundary, + Object rangeStartValue, + BoundaryType rangeEndBoundary, + Object rangeEndValue, + boolean reverseRange, + boolean reverseOrder) + { + throw new UnsupportedOperationException(); + } } static class ScanQuerySupport implements FullScanQueryExecutor.Support { @@ -194,6 +287,10 @@ public class TestJoinedQueryExecutor extends TestQueryExecutor { return mQuery.getStorableType(); } + public long countAll() throws FetchException { + return mQuery.count(); + } + public Cursor fetchAll() throws FetchException { return mQuery.fetch(); } diff --git a/src/test/java/com/amazon/carbonado/qe/TestOrderingList.java b/src/test/java/com/amazon/carbonado/qe/TestOrderingList.java index ea9bc10..8af7a2e 100644 --- a/src/test/java/com/amazon/carbonado/qe/TestOrderingList.java +++ b/src/test/java/com/amazon/carbonado/qe/TestOrderingList.java @@ -184,6 +184,21 @@ public class TestOrderingList extends TestCase { assertTrue(list_1 == list_2); } + public void testSubList() throws Exception { + OrderingList list_1 = + OrderingList.get(StorableTestBasic.class, "date", "-intProp", "~stringProp"); + + assertEquals(0, list_1.subList(0, 0).size()); + assertEquals(list_1, list_1.subList(0, 3)); + + OrderingList sub = list_1.subList(0, 1); + assertEquals(1, sub.size()); + assertEquals("+date", sub.get(0).toString()); + + sub = list_1.subList(1, 3); + assertEquals(2, sub.size()); + } + public void testAsArray() throws Exception { OrderingList list = OrderingList.get(StorableTestBasic.class, "date", "intProp", "stringProp"); diff --git a/src/test/java/com/amazon/carbonado/qe/TestUnionQueryAnalyzer.java b/src/test/java/com/amazon/carbonado/qe/TestUnionQueryAnalyzer.java index e9cc8f9..7c92772 100644 --- a/src/test/java/com/amazon/carbonado/qe/TestUnionQueryAnalyzer.java +++ b/src/test/java/com/amazon/carbonado/qe/TestUnionQueryAnalyzer.java @@ -131,7 +131,8 @@ public class TestUnionQueryAnalyzer extends TestCase { assertTrue(res_1.handlesAnything()); assertEquals(Filter.filterFor(Shipment.class, "orderID = ?").bind(), res_1.getCompositeScore().getFilteringScore().getIdentityFilter()); - assertEquals(makeIndex(Shipment.class, "orderID"), res_1.getLocalIndex()); + assertEquals(makeIndex(Shipment.class, "orderID", "shipmentNotes"), + res_1.getLocalIndex()); assertEquals(null, res_1.getForeignIndex()); assertEquals(null, res_1.getForeignProperty()); assertEquals(1, res_1.getRemainderOrdering().size()); @@ -163,7 +164,8 @@ public class TestUnionQueryAnalyzer extends TestCase { assertTrue(res_1.handlesAnything()); assertTrue(res_1.getCompositeScore().getFilteringScore().hasRangeStart()); assertFalse(res_1.getCompositeScore().getFilteringScore().hasRangeEnd()); - assertEquals(makeIndex(Shipment.class, "orderID"), res_1.getLocalIndex()); + assertEquals(makeIndex(Shipment.class, "orderID", "shipmentNotes"), + res_1.getLocalIndex()); assertEquals(null, res_1.getForeignIndex()); assertEquals(null, res_1.getForeignProperty()); assertEquals(1, res_1.getRemainderOrdering().size()); @@ -205,7 +207,7 @@ public class TestUnionQueryAnalyzer extends TestCase { rangeFilters = res_1.getCompositeScore().getFilteringScore().getRangeEndFilters(); assertEquals(1, rangeFilters.size()); assertEquals(Filter.filterFor(Shipment.class, "orderID <= ?").bind(), rangeFilters.get(0)); - assertEquals(makeIndex(Shipment.class, "orderID"), res_1.getLocalIndex()); + assertEquals(makeIndex(Shipment.class, "orderID", "shipmentNotes"), res_1.getLocalIndex()); assertEquals(null, res_1.getForeignIndex()); assertEquals(null, res_1.getForeignProperty()); // Sort operation required because the "shipmentID" index was not chosen. @@ -249,7 +251,7 @@ public class TestUnionQueryAnalyzer extends TestCase { assertTrue(res_1.handlesAnything()); assertEquals(Filter.filterFor(Shipment.class, "orderID = ?").bind(), res_1.getCompositeScore().getFilteringScore().getIdentityFilter()); - assertEquals(makeIndex(Shipment.class, "orderID"), res_1.getLocalIndex()); + assertEquals(makeIndex(Shipment.class, "orderID", "shipmentNotes"), res_1.getLocalIndex()); assertEquals(null, res_1.getForeignIndex()); assertEquals(null, res_1.getForeignProperty()); assertEquals(1, res_1.getRemainderOrdering().size()); @@ -342,7 +344,7 @@ public class TestUnionQueryAnalyzer extends TestCase { IndexedQueryAnalyzer.Result res_1 = subResults.get(1); assertTrue(res_0.handlesAnything()); - assertEquals(makeIndex(Shipment.class, "orderID"), res_0.getLocalIndex()); + assertEquals(makeIndex(Shipment.class, "orderID", "shipmentNotes"), res_0.getLocalIndex()); assertEquals(null, res_0.getForeignIndex()); assertEquals(null, res_0.getForeignProperty()); assertEquals(1, res_0.getRemainderOrdering().size()); -- cgit v1.2.3